[FOLLOWUP] Make FormatsViewHelper compilable
[Packages/TYPO3.CMS.git] / typo3 / sysext / fluid / Classes / Core / Compiler / TemplateCompiler.php
1 <?php
2 namespace TYPO3\CMS\Fluid\Core\Compiler;
3
4 /* *
5 * This script is backported from the TYPO3 Flow package "TYPO3.Fluid". *
6 * *
7 * It is free software; you can redistribute it and/or modify it under *
8 * the terms of the GNU Lesser General Public License, either version 3 *
9 * of the License, or (at your option) any later version. *
10 * *
11 * The TYPO3 project - inspiring people to share! *
12 * */
13
14 class TemplateCompiler implements \TYPO3\CMS\Core\SingletonInterface {
15
16 const SHOULD_GENERATE_VIEWHELPER_INVOCATION = '##should_gen_viewhelper##';
17
18 /**
19 * @var \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
20 */
21 protected $templateCache;
22
23 /**
24 * @var int
25 */
26 protected $variableCounter = 0;
27
28 /**
29 * @var array
30 */
31 protected $syntaxTreeInstanceCache = array();
32
33 /**
34 * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $templateCache
35 * @return void
36 */
37 public function setTemplateCache(\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $templateCache) {
38 $this->templateCache = $templateCache;
39 }
40
41 /**
42 * @param string $identifier
43 * @return bool
44 */
45 public function has($identifier) {
46 $identifier = $this->sanitizeIdentifier($identifier);
47 return $this->templateCache->has($identifier);
48 }
49
50 /**
51 * @param string $identifier
52 * @return \TYPO3\CMS\Fluid\Core\Parser\ParsedTemplateInterface
53 */
54 public function get($identifier) {
55 $identifier = $this->sanitizeIdentifier($identifier);
56 if (!isset($this->syntaxTreeInstanceCache[$identifier])) {
57 $this->templateCache->requireOnce($identifier);
58 $templateClassName = 'FluidCache_' . $identifier;
59 $this->syntaxTreeInstanceCache[$identifier] = new $templateClassName();
60 }
61 return $this->syntaxTreeInstanceCache[$identifier];
62 }
63
64 /**
65 * @param string $identifier
66 * @param \TYPO3\CMS\Fluid\Core\Parser\ParsingState $parsingState
67 * @return void
68 */
69 public function store($identifier, \TYPO3\CMS\Fluid\Core\Parser\ParsingState $parsingState) {
70 $identifier = $this->sanitizeIdentifier($identifier);
71 $this->variableCounter = 0;
72 $generatedRenderFunctions = '';
73
74 if ($parsingState->getVariableContainer()->exists('sections')) {
75 $sections = $parsingState->getVariableContainer()->get('sections');
76 // @todo refactor to $parsedTemplate->getSections()
77 foreach ($sections as $sectionName => $sectionRootNode) {
78 $generatedRenderFunctions .= $this->generateCodeForSection($this->convertListOfSubNodes($sectionRootNode), 'section_' . sha1($sectionName), 'section ' . $sectionName);
79 }
80 }
81 $generatedRenderFunctions .= $this->generateCodeForSection($this->convertListOfSubNodes($parsingState->getRootNode()), 'render', 'Main Render function');
82 $convertedLayoutNameNode = $parsingState->hasLayout() ? $this->convert($parsingState->getLayoutNameNode()) : array('initialization' => '', 'execution' => 'NULL');
83
84 $classDefinition = 'class FluidCache_' . $identifier . ' extends \\TYPO3\\CMS\\Fluid\\Core\\Compiler\\AbstractCompiledTemplate';
85
86 $templateCode = <<<EOD
87 %s {
88
89 public function getVariableContainer() {
90 // @todo
91 return new \TYPO3\CMS\Fluid\Core\ViewHelper\TemplateVariableContainer();
92 }
93 public function getLayoutName(\TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
94 %s
95 return %s;
96 }
97 public function hasLayout() {
98 return %s;
99 }
100
101 %s
102
103 }
104 EOD;
105 $templateCode = sprintf($templateCode,
106 $classDefinition,
107 $convertedLayoutNameNode['initialization'],
108 $convertedLayoutNameNode['execution'],
109 ($parsingState->hasLayout() ? 'TRUE' : 'FALSE'),
110 $generatedRenderFunctions);
111 $this->templateCache->set($identifier, $templateCode);
112 }
113
114 /**
115 * Replaces special characters by underscores
116 *
117 * @see http://www.php.net/manual/en/language.variables.basics.php
118 * @param string $identifier
119 * @return string the sanitized identifier
120 */
121 protected function sanitizeIdentifier($identifier) {
122 return preg_replace('([^a-zA-Z0-9_\\x7f-\\xff])', '_', $identifier);
123 }
124
125 /**
126 * @param array $converted
127 * @param string $expectedFunctionName
128 * @param string $comment
129 * @return string
130 */
131 protected function generateCodeForSection(array $converted, $expectedFunctionName, $comment) {
132 $templateCode = <<<EOD
133 /**
134 * %s
135 */
136 public function %s(\TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
137 \$self = \$this;
138 \$currentVariableContainer = \$renderingContext->getTemplateVariableContainer();
139
140 %s
141
142 return %s;
143 }
144
145 EOD;
146 return sprintf($templateCode, $comment, $expectedFunctionName, $converted['initialization'], $converted['execution']);
147 }
148
149 /**
150 * Returns an array with two elements:
151 * - initialization: contains PHP code which is inserted *before* the actual rendering call. Must be valid, i.e. end with semi-colon.
152 * - execution: contains *a single PHP instruction* which needs to return the rendered output of the given element. Should NOT end with semi-colon.
153 *
154 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node
155 * @return array two-element array, see above
156 * @throws \TYPO3\CMS\Fluid\Exception
157 */
158 protected function convert(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node) {
159 if ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode) {
160 return $this->convertTextNode($node);
161 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NumericNode) {
162 return $this->convertNumericNode($node);
163 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode) {
164 return $this->convertViewHelperNode($node);
165 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode) {
166 return $this->convertObjectAccessorNode($node);
167 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode) {
168 return $this->convertArrayNode($node);
169 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\RootNode) {
170 return $this->convertListOfSubNodes($node);
171 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode) {
172 return $this->convertBooleanNode($node);
173 } else {
174 throw new \TYPO3\CMS\Fluid\Exception('Syntax tree node type "' . get_class($node) . '" is not supported.');
175 }
176 }
177
178 /**
179 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode $node
180 * @return array
181 * @see convert()
182 */
183 protected function convertTextNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode $node) {
184 return array(
185 'initialization' => '',
186 'execution' => '\'' . $this->escapeTextForUseInSingleQuotes($node->getText()) . '\''
187 );
188 }
189
190 /**
191 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NumericNode $node
192 * @return array
193 * @see convert()
194 */
195 protected function convertNumericNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NumericNode $node) {
196 return array(
197 'initialization' => '',
198 'execution' => $node->getValue()
199 );
200 }
201
202 /**
203 * Convert a single ViewHelperNode into its cached representation. If the ViewHelper implements the "Compilable" facet,
204 * the ViewHelper itself is asked for its cached PHP code representation. If not, a ViewHelper is built and then invoked.
205 *
206 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode $node
207 * @return array
208 * @see convert()
209 */
210 protected function convertViewHelperNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode $node) {
211 $initializationPhpCode = '// Rendering ViewHelper ' . $node->getViewHelperClassName() . LF;
212
213 // Build up $arguments array
214 $argumentsVariableName = $this->variableName('arguments');
215 $initializationPhpCode .= sprintf('%s = array();', $argumentsVariableName) . LF;
216
217 $alreadyBuiltArguments = array();
218 foreach ($node->getArguments() as $argumentName => $argumentValue) {
219 $converted = $this->convert($argumentValue);
220 $initializationPhpCode .= $converted['initialization'];
221 $initializationPhpCode .= sprintf('%s[\'%s\'] = %s;', $argumentsVariableName, $argumentName, $converted['execution']) . LF;
222 $alreadyBuiltArguments[$argumentName] = TRUE;
223 }
224
225 foreach ($node->getUninitializedViewHelper()->prepareArguments() as $argumentName => $argumentDefinition) {
226 if (!isset($alreadyBuiltArguments[$argumentName])) {
227 $initializationPhpCode .= sprintf('%s[\'%s\'] = %s;', $argumentsVariableName, $argumentName, var_export($argumentDefinition->getDefaultValue(), TRUE)) . LF;
228 }
229 }
230
231 // Build up closure which renders the child nodes
232 $renderChildrenClosureVariableName = $this->variableName('renderChildrenClosure');
233 $initializationPhpCode .= sprintf('%s = %s;', $renderChildrenClosureVariableName, $this->wrapChildNodesInClosure($node)) . LF;
234
235 if ($node->getUninitializedViewHelper() instanceof \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\CompilableInterface) {
236 // ViewHelper is compilable
237 $viewHelperInitializationPhpCode = '';
238 $convertedViewHelperExecutionCode = $node->getUninitializedViewHelper()->compile($argumentsVariableName, $renderChildrenClosureVariableName, $viewHelperInitializationPhpCode, $node, $this);
239 $initializationPhpCode .= $viewHelperInitializationPhpCode;
240 if ($convertedViewHelperExecutionCode !== self::SHOULD_GENERATE_VIEWHELPER_INVOCATION) {
241 return array(
242 'initialization' => $initializationPhpCode,
243 'execution' => $convertedViewHelperExecutionCode
244 );
245 }
246 }
247
248 // ViewHelper is not compilable, so we need to instanciate it directly and render it.
249 $viewHelperVariableName = $this->variableName('viewHelper');
250
251 $initializationPhpCode .= sprintf('%s = $self->getViewHelper(\'%s\', $renderingContext, \'%s\');', $viewHelperVariableName, $viewHelperVariableName, $node->getViewHelperClassName()) . LF;
252 $initializationPhpCode .= sprintf('%s->setArguments(%s);', $viewHelperVariableName, $argumentsVariableName) . LF;
253 $initializationPhpCode .= sprintf('%s->setRenderingContext($renderingContext);', $viewHelperVariableName) . LF;
254
255 $initializationPhpCode .= sprintf('%s->setRenderChildrenClosure(%s);', $viewHelperVariableName, $renderChildrenClosureVariableName) . LF;
256
257 $initializationPhpCode .= '// End of ViewHelper ' . $node->getViewHelperClassName() . LF;
258
259 return array(
260 'initialization' => $initializationPhpCode,
261 'execution' => sprintf('%s->initializeArgumentsAndRender()', $viewHelperVariableName)
262 );
263 }
264
265 /**
266 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode $node
267 * @return array
268 * @see convert()
269 */
270 protected function convertObjectAccessorNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode $node) {
271 $objectPathSegments = explode('.', $node->getObjectPath());
272 $firstPathElement = array_shift($objectPathSegments);
273 if ($objectPathSegments === array()) {
274 return array(
275 'initialization' => '',
276 'execution' => sprintf('$currentVariableContainer->getOrNull(\'%s\')', $firstPathElement)
277 );
278 } else {
279 $executionCode = '\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::getPropertyPath($currentVariableContainer->getOrNull(\'%s\'), \'%s\', $renderingContext)';
280 return array(
281 'initialization' => '',
282 'execution' => sprintf($executionCode, $firstPathElement, implode('.', $objectPathSegments))
283 );
284 }
285 }
286
287 /**
288 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode $node
289 * @return array
290 * @see convert()
291 */
292 protected function convertArrayNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode $node) {
293 $initializationPhpCode = '// Rendering Array' . LF;
294 $arrayVariableName = $this->variableName('array');
295
296 $initializationPhpCode .= sprintf('%s = array();', $arrayVariableName) . LF;
297
298 foreach ($node->getInternalArray() as $key => $value) {
299 if ($value instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode) {
300 $converted = $this->convert($value);
301 $initializationPhpCode .= $converted['initialization'];
302 $initializationPhpCode .= sprintf('%s[\'%s\'] = %s;', $arrayVariableName, $key, $converted['execution']) . LF;
303 } elseif (is_numeric($value)) {
304 // this case might happen for simple values
305 $initializationPhpCode .= sprintf('%s[\'%s\'] = %s;', $arrayVariableName, $key, $value) . LF;
306 } else {
307 // this case might happen for simple values
308 $initializationPhpCode .= sprintf('%s[\'%s\'] = \'%s\';', $arrayVariableName, $key, $this->escapeTextForUseInSingleQuotes($value)) . LF;
309 }
310 }
311 return array(
312 'initialization' => $initializationPhpCode,
313 'execution' => $arrayVariableName
314 );
315 }
316
317 /**
318 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node
319 * @return array
320 * @see convert()
321 */
322 public function convertListOfSubNodes(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node) {
323 switch (count($node->getChildNodes())) {
324 case 0:
325 return array(
326 'initialization' => '',
327 'execution' => 'NULL'
328 );
329 case 1:
330 $converted = $this->convert(current($node->getChildNodes()));
331
332 return $converted;
333 default:
334 $outputVariableName = $this->variableName('output');
335 $initializationPhpCode = sprintf('%s = \'\';', $outputVariableName) . LF;
336
337 foreach ($node->getChildNodes() as $childNode) {
338 $converted = $this->convert($childNode);
339
340 $initializationPhpCode .= $converted['initialization'] . LF;
341 $initializationPhpCode .= sprintf('%s .= %s;', $outputVariableName, $converted['execution']) . LF;
342 }
343
344 return array(
345 'initialization' => $initializationPhpCode,
346 'execution' => $outputVariableName
347 );
348 }
349 }
350
351 /**
352 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode $node
353 * @return array
354 * @see convert()
355 */
356 protected function convertBooleanNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode $node) {
357 $initializationPhpCode = '// Rendering Boolean node' . LF;
358 if ($node->getComparator() !== NULL) {
359 $convertedLeftSide = $this->convert($node->getLeftSide());
360 $convertedRightSide = $this->convert($node->getRightSide());
361
362 return array(
363 'initialization' => $initializationPhpCode . $convertedLeftSide['initialization'] . $convertedRightSide['initialization'],
364 'execution' => sprintf(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode::class . '::evaluateComparator(\'%s\', %s, %s)', $node->getComparator(), $convertedLeftSide['execution'], $convertedRightSide['execution'])
365 );
366 } else {
367 // simple case, no comparator.
368 $converted = $this->convert($node->getSyntaxTreeNode());
369 return array(
370 'initialization' => $initializationPhpCode . $converted['initialization'],
371 'execution' => sprintf(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode::class . '::convertToBoolean(%s)', $converted['execution'])
372 );
373 }
374 }
375
376 /**
377 * @param string $text
378 * @return string
379 */
380 protected function escapeTextForUseInSingleQuotes($text) {
381 return str_replace(array('\\', '\''), array('\\\\', '\\\''), $text);
382 }
383
384 /**
385 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node
386 * @return string
387 */
388 public function wrapChildNodesInClosure(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node) {
389 $convertedSubNodes = $this->convertListOfSubNodes($node);
390 if ($convertedSubNodes['execution'] === 'NULL') {
391 return 'function() {return NULL;}';
392 }
393
394 $closure = '';
395 $closure .= 'function() use ($renderingContext, $self) {' . LF;
396 $closure .= '$currentVariableContainer = $renderingContext->getTemplateVariableContainer();' . LF;
397 $closure .= $convertedSubNodes['initialization'];
398 $closure .= sprintf('return %s;', $convertedSubNodes['execution']) . LF;
399 $closure .= '}';
400 return $closure;
401 }
402
403 /**
404 * Returns a unique variable name by appending a global index to the given prefix
405 *
406 * @param string $prefix
407 * @return string
408 */
409 public function variableName($prefix) {
410 return '$' . $prefix . $this->variableCounter++;
411 }
412
413 }