[!!!][TASK] Refactor property access in compiled fluid templates
[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 \$propertyAccessorReplacements = array();
90
91 public function getVariableContainer() {
92 // @todo
93 return new \TYPO3\CMS\Fluid\Core\ViewHelper\TemplateVariableContainer();
94 }
95 public function getLayoutName(\TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
96 %s
97 return %s;
98 }
99 public function hasLayout() {
100 return %s;
101 }
102
103 %s
104
105 }
106 EOD;
107 $templateCode = sprintf($templateCode,
108 $classDefinition,
109 $convertedLayoutNameNode['initialization'],
110 $convertedLayoutNameNode['execution'],
111 ($parsingState->hasLayout() ? 'TRUE' : 'FALSE'),
112 $generatedRenderFunctions);
113 $this->templateCache->set($identifier, $templateCode);
114 }
115
116 /**
117 * Replaces special characters by underscores
118 *
119 * @see http://www.php.net/manual/en/language.variables.basics.php
120 * @param string $identifier
121 * @return string the sanitized identifier
122 */
123 protected function sanitizeIdentifier($identifier) {
124 return preg_replace('([^a-zA-Z0-9_\\x7f-\\xff])', '_', $identifier);
125 }
126
127 /**
128 * @param array $converted
129 * @param string $expectedFunctionName
130 * @param string $comment
131 * @return string
132 */
133 protected function generateCodeForSection(array $converted, $expectedFunctionName, $comment) {
134 $templateCode = <<<EOD
135 /**
136 * %s
137 */
138 public function %s(\TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface \$renderingContext) {
139 \$self = \$this;
140 \$currentVariableContainer = \$renderingContext->getTemplateVariableContainer();
141
142 %s
143
144 if (\$this->propertyAccessorReplacements !== array()) {
145 \$this->replacePropertyAccessors(\$this->propertyAccessorReplacements, __FILE__);
146 \$this->propertyAccessorReplacements = array();
147 }
148
149 return %s;
150 }
151
152 EOD;
153 return sprintf($templateCode, $comment, $expectedFunctionName, $converted['initialization'], $converted['execution']);
154 }
155
156 /**
157 * Returns an array with two elements:
158 * - initialization: contains PHP code which is inserted *before* the actual rendering call. Must be valid, i.e. end with semi-colon.
159 * - execution: contains *a single PHP instruction* which needs to return the rendered output of the given element. Should NOT end with semi-colon.
160 *
161 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node
162 * @return array two-element array, see above
163 * @throws \TYPO3\CMS\Fluid\Exception
164 */
165 protected function convert(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node) {
166 if ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode) {
167 return $this->convertTextNode($node);
168 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NumericNode) {
169 return $this->convertNumericNode($node);
170 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode) {
171 return $this->convertViewHelperNode($node);
172 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode) {
173 return $this->convertObjectAccessorNode($node);
174 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode) {
175 return $this->convertArrayNode($node);
176 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\RootNode) {
177 return $this->convertListOfSubNodes($node);
178 } elseif ($node instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode) {
179 return $this->convertBooleanNode($node);
180 } else {
181 throw new \TYPO3\CMS\Fluid\Exception('Syntax tree node type "' . get_class($node) . '" is not supported.');
182 }
183 }
184
185 /**
186 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode $node
187 * @return array
188 * @see convert()
189 */
190 protected function convertTextNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode $node) {
191 return array(
192 'initialization' => '',
193 'execution' => '\'' . $this->escapeTextForUseInSingleQuotes($node->getText()) . '\''
194 );
195 }
196
197 /**
198 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NumericNode $node
199 * @return array
200 * @see convert()
201 */
202 protected function convertNumericNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NumericNode $node) {
203 return array(
204 'initialization' => '',
205 'execution' => $node->getValue()
206 );
207 }
208
209 /**
210 * Convert a single ViewHelperNode into its cached representation. If the ViewHelper implements the "Compilable" facet,
211 * the ViewHelper itself is asked for its cached PHP code representation. If not, a ViewHelper is built and then invoked.
212 *
213 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode $node
214 * @return array
215 * @see convert()
216 */
217 protected function convertViewHelperNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode $node) {
218 $initializationPhpCode = '// Rendering ViewHelper ' . $node->getViewHelperClassName() . LF;
219
220 // Build up $arguments array
221 $argumentsVariableName = $this->variableName('arguments');
222 $initializationPhpCode .= sprintf('%s = array();', $argumentsVariableName) . LF;
223
224 $alreadyBuiltArguments = array();
225 foreach ($node->getArguments() as $argumentName => $argumentValue) {
226 $converted = $this->convert($argumentValue);
227 $initializationPhpCode .= $converted['initialization'];
228 $initializationPhpCode .= sprintf('%s[\'%s\'] = %s;', $argumentsVariableName, $argumentName, $converted['execution']) . LF;
229 $alreadyBuiltArguments[$argumentName] = TRUE;
230 }
231
232 foreach ($node->getUninitializedViewHelper()->prepareArguments() as $argumentName => $argumentDefinition) {
233 if (!isset($alreadyBuiltArguments[$argumentName])) {
234 $initializationPhpCode .= sprintf('%s[\'%s\'] = %s;', $argumentsVariableName, $argumentName, var_export($argumentDefinition->getDefaultValue(), TRUE)) . LF;
235 }
236 }
237
238 // Build up closure which renders the child nodes
239 $renderChildrenClosureVariableName = $this->variableName('renderChildrenClosure');
240 $initializationPhpCode .= sprintf('%s = %s;', $renderChildrenClosureVariableName, $this->wrapChildNodesInClosure($node)) . LF;
241
242 if ($node->getUninitializedViewHelper() instanceof \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\CompilableInterface) {
243 // ViewHelper is compilable
244 $viewHelperInitializationPhpCode = '';
245 $convertedViewHelperExecutionCode = $node->getUninitializedViewHelper()->compile($argumentsVariableName, $renderChildrenClosureVariableName, $viewHelperInitializationPhpCode, $node, $this);
246 $initializationPhpCode .= $viewHelperInitializationPhpCode;
247 if ($convertedViewHelperExecutionCode !== self::SHOULD_GENERATE_VIEWHELPER_INVOCATION) {
248 return array(
249 'initialization' => $initializationPhpCode,
250 'execution' => $convertedViewHelperExecutionCode
251 );
252 }
253 }
254
255 // ViewHelper is not compilable, so we need to instanciate it directly and render it.
256 $viewHelperVariableName = $this->variableName('viewHelper');
257
258 $initializationPhpCode .= sprintf('%s = $self->getViewHelper(\'%s\', $renderingContext, \'%s\');', $viewHelperVariableName, $viewHelperVariableName, $node->getViewHelperClassName()) . LF;
259 $initializationPhpCode .= sprintf('%s->setArguments(%s);', $viewHelperVariableName, $argumentsVariableName) . LF;
260 $initializationPhpCode .= sprintf('%s->setRenderingContext($renderingContext);', $viewHelperVariableName) . LF;
261
262 $initializationPhpCode .= sprintf('%s->setRenderChildrenClosure(%s);', $viewHelperVariableName, $renderChildrenClosureVariableName) . LF;
263
264 $initializationPhpCode .= '// End of ViewHelper ' . $node->getViewHelperClassName() . LF;
265
266 return array(
267 'initialization' => $initializationPhpCode,
268 'execution' => sprintf('%s->initializeArgumentsAndRender()', $viewHelperVariableName)
269 );
270 }
271
272 /**
273 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode $node
274 * @return array
275 * @see convert()
276 */
277 protected function convertObjectAccessorNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode $node) {
278 $objectPathSegments = explode('.', $node->getObjectPath());
279 $firstPathElement = array_shift($objectPathSegments);
280 $nodeIdentifier = md5(spl_object_hash($node) . $node->getObjectPath() . uniqid());
281 if ($objectPathSegments === array()) {
282 return array(
283 'initialization' => '',
284 'execution' => sprintf('$currentVariableContainer->getOrNull(\'%s\')', $firstPathElement)
285 );
286 } else {
287 $executionCode = <<<EOD
288 /** ###%s### */ call_user_func(function(\$self) use (\$currentVariableContainer, \$renderingContext) {
289 \$subject = \$currentVariableContainer->getOrNull('%s');
290
291 \$accessors = \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::getPropertyAccessors(\$subject, %s);
292
293 if (\$accessors !== NULL) {
294 \$self->propertyAccessorReplacements['~/\*\* ###%s### \*/ .* /\*\* ###%s### \*/~s'] = '\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::resolvePropertyAccessors(\$currentVariableContainer->getOrNull(\'%s\'), ' . var_export(\$accessors, TRUE) . ')';
295 return \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::resolvePropertyAccessors(\$subject, \$accessors);
296 } else {
297 \$self->propertyAccessorReplacements['~/\*\* ###%s### \*/ .* /\*\* ###%s### \*/~s'] = '\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::getPropertyPath(\$currentVariableContainer->getOrNull(\'%s\'), \'%s\', \$renderingContext)';
298 return \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::getPropertyPath(\$subject, '%s', \$renderingContext);
299 }
300 }, \$self) /** ###%s### */
301
302 EOD;
303 return array(
304 'initialization' => '',
305 'execution' => sprintf($executionCode, $nodeIdentifier, $firstPathElement, var_export($objectPathSegments, TRUE), $nodeIdentifier, $nodeIdentifier, $firstPathElement, $nodeIdentifier, $nodeIdentifier, $firstPathElement, implode('.', $objectPathSegments), implode('.', $objectPathSegments), $nodeIdentifier)
306 );
307 }
308 }
309
310 /**
311 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode $node
312 * @return array
313 * @see convert()
314 */
315 protected function convertArrayNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode $node) {
316 $initializationPhpCode = '// Rendering Array' . LF;
317 $arrayVariableName = $this->variableName('array');
318
319 $initializationPhpCode .= sprintf('%s = array();', $arrayVariableName) . LF;
320
321 foreach ($node->getInternalArray() as $key => $value) {
322 if ($value instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode) {
323 $converted = $this->convert($value);
324 $initializationPhpCode .= $converted['initialization'];
325 $initializationPhpCode .= sprintf('%s[\'%s\'] = %s;', $arrayVariableName, $key, $converted['execution']) . LF;
326 } elseif (is_numeric($value)) {
327 // this case might happen for simple values
328 $initializationPhpCode .= sprintf('%s[\'%s\'] = %s;', $arrayVariableName, $key, $value) . LF;
329 } else {
330 // this case might happen for simple values
331 $initializationPhpCode .= sprintf('%s[\'%s\'] = \'%s\';', $arrayVariableName, $key, $this->escapeTextForUseInSingleQuotes($value)) . LF;
332 }
333 }
334 return array(
335 'initialization' => $initializationPhpCode,
336 'execution' => $arrayVariableName
337 );
338 }
339
340 /**
341 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node
342 * @return array
343 * @see convert()
344 */
345 public function convertListOfSubNodes(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node) {
346 switch (count($node->getChildNodes())) {
347 case 0:
348 return array(
349 'initialization' => '',
350 'execution' => 'NULL'
351 );
352 case 1:
353 $converted = $this->convert(current($node->getChildNodes()));
354
355 return $converted;
356 default:
357 $outputVariableName = $this->variableName('output');
358 $initializationPhpCode = sprintf('%s = \'\';', $outputVariableName) . LF;
359
360 foreach ($node->getChildNodes() as $childNode) {
361 $converted = $this->convert($childNode);
362
363 $initializationPhpCode .= $converted['initialization'] . LF;
364 $initializationPhpCode .= sprintf('%s .= %s;', $outputVariableName, $converted['execution']) . LF;
365 }
366
367 return array(
368 'initialization' => $initializationPhpCode,
369 'execution' => $outputVariableName
370 );
371 }
372 }
373
374 /**
375 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode $node
376 * @return array
377 * @see convert()
378 */
379 protected function convertBooleanNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode $node) {
380 $initializationPhpCode = '// Rendering Boolean node' . LF;
381 if ($node->getComparator() !== NULL) {
382 $convertedLeftSide = $this->convert($node->getLeftSide());
383 $convertedRightSide = $this->convert($node->getRightSide());
384
385 return array(
386 'initialization' => $initializationPhpCode . $convertedLeftSide['initialization'] . $convertedRightSide['initialization'],
387 'execution' => sprintf(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode::class . '::evaluateComparator(\'%s\', %s, %s)', $node->getComparator(), $convertedLeftSide['execution'], $convertedRightSide['execution'])
388 );
389 } else {
390 // simple case, no comparator.
391 $converted = $this->convert($node->getSyntaxTreeNode());
392 return array(
393 'initialization' => $initializationPhpCode . $converted['initialization'],
394 'execution' => sprintf(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode::class . '::convertToBoolean(%s)', $converted['execution'])
395 );
396 }
397 }
398
399 /**
400 * @param string $text
401 * @return string
402 */
403 protected function escapeTextForUseInSingleQuotes($text) {
404 return str_replace(array('\\', '\''), array('\\\\', '\\\''), $text);
405 }
406
407 /**
408 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node
409 * @return string
410 */
411 public function wrapChildNodesInClosure(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $node) {
412 $convertedSubNodes = $this->convertListOfSubNodes($node);
413 if ($convertedSubNodes['execution'] === 'NULL') {
414 return 'function() {return NULL;}';
415 }
416
417 $closure = '';
418 $closure .= 'function() use ($renderingContext, $self) {' . LF;
419 $closure .= '$currentVariableContainer = $renderingContext->getTemplateVariableContainer();' . LF;
420 $closure .= $convertedSubNodes['initialization'];
421 $closure .= sprintf('return %s;', $convertedSubNodes['execution']) . LF;
422 $closure .= '}';
423 return $closure;
424 }
425
426 /**
427 * Returns a unique variable name by appending a global index to the given prefix
428 *
429 * @param string $prefix
430 * @return string
431 */
432 public function variableName($prefix) {
433 return '$' . $prefix . $this->variableCounter++;
434 }
435
436 }