[!!!][TASK] Refactor property access in compiled fluid templates 48/39148/8
authorChristian Müller <christian@kitsunet.de>
Fri, 1 May 2015 10:21:19 +0000 (12:21 +0200)
committerNicole Cordes <typo3@cordes.co>
Sun, 3 May 2015 09:09:42 +0000 (11:09 +0200)
Using ObjectAccess::getPropertyInternal is pretty expensive if called
repeatedly as can happen in bigger templates. On rendering the compiled
template for the first time we can generate the used getter and/or array
access information and write that into the compiled template.
Subsequent usages of the compiled template will then have much faster
access to nested variables.

This change is breaking if you push mixed data to the template. For
example an array that contains both arrays and object and access those
with the same property path access. In this case the cached accessors
might be wrong.

Resolves: #66758
Releases: master
Change-Id: Icb368938e67725d3a30a2545a30b40f045199405
Reviewed-on: http://review.typo3.org/39148
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Jan Helke <typo3@helke.de>
Tested-by: Jan Helke <typo3@helke.de>
Reviewed-by: Nicole Cordes <typo3@cordes.co>
Tested-by: Nicole Cordes <typo3@cordes.co>
typo3/sysext/core/Documentation/Changelog/master/Breaking-66758-FluidRefactorPropertyAccess.rst [new file with mode: 0644]
typo3/sysext/fluid/Classes/Core/Compiler/AbstractCompiledTemplate.php
typo3/sysext/fluid/Classes/Core/Compiler/TemplateCompiler.php
typo3/sysext/fluid/Classes/Core/Parser/SyntaxTree/ObjectAccessorNode.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-66758-FluidRefactorPropertyAccess.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-66758-FluidRefactorPropertyAccess.rst
new file mode 100644 (file)
index 0000000..2e1bde7
--- /dev/null
@@ -0,0 +1,30 @@
+=======================================================================
+Breaking: #66758 - Refactor property access in compiled fluid templates
+=======================================================================
+
+Description
+===========
+
+Refactors property access in Fluid templates to use a closure that calculates the getters and/or array
+access keys to resolve the property path and self replaces this after the first rendering in the compiled
+template. This improves property access dramatically after the first request.
+
+Impact
+======
+
+This breaks if you access different types of data with the same path. For example if you have an array in
+which arrays and objects are nested on the same level.
+
+Example::
+
+  Array (available in Fluid as {data})
+    Object (getFoo())
+    Array['foo']
+
+  <f:for each="data" as="element">
+       {element.foo}
+  </f:for>
+
+This code would break with the change as the closure would determine that accessing {element.foo} needs to
+use the getter method `getFoo()` so accessing the second element which is an array would break as fluid would
+try to access `foo` as well with `getFoo()`.
\ No newline at end of file
index efcb4b3..e363739 100644 (file)
@@ -95,4 +95,17 @@ abstract class AbstractCompiledTemplate implements \TYPO3\CMS\Fluid\Core\Parser\
                return static::$defaultEncoding;
        }
 
+       /**
+        * @param array $replacements
+        */
+       public function replacePropertyAccessors($replacements, $file) {
+               $code = file_get_contents($file);
+
+               foreach ($replacements as $pattern => $replacementCode) {
+                       $code = preg_replace($pattern, $replacementCode, $code);
+               }
+
+               file_put_contents($file, $code);
+       }
+
 }
index c4ebb66..8cb0fdd 100644 (file)
@@ -86,6 +86,8 @@ class TemplateCompiler implements \TYPO3\CMS\Core\SingletonInterface {
                $templateCode = <<<EOD
 %s {
 
+public \$propertyAccessorReplacements = array();
+
 public function getVariableContainer() {
        // @todo
        return new \TYPO3\CMS\Fluid\Core\ViewHelper\TemplateVariableContainer();
@@ -139,6 +141,11 @@ public function %s(\TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface \$r
 
 %s
 
+       if (\$this->propertyAccessorReplacements !== array()) {
+               \$this->replacePropertyAccessors(\$this->propertyAccessorReplacements, __FILE__);
+               \$this->propertyAccessorReplacements = array();
+       }
+
 return %s;
 }
 
@@ -270,16 +277,32 @@ EOD;
        protected function convertObjectAccessorNode(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode $node) {
                $objectPathSegments = explode('.', $node->getObjectPath());
                $firstPathElement = array_shift($objectPathSegments);
+               $nodeIdentifier = md5(spl_object_hash($node) . $node->getObjectPath() . uniqid());
                if ($objectPathSegments === array()) {
                        return array(
                                'initialization' => '',
                                'execution' => sprintf('$currentVariableContainer->getOrNull(\'%s\')', $firstPathElement)
                        );
                } else {
-                       $executionCode = '\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::getPropertyPath($currentVariableContainer->getOrNull(\'%s\'), \'%s\', $renderingContext)';
+                       $executionCode = <<<EOD
+/** ###%s### */ call_user_func(function(\$self) use (\$currentVariableContainer, \$renderingContext) {
+       \$subject = \$currentVariableContainer->getOrNull('%s');
+
+       \$accessors = \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::getPropertyAccessors(\$subject, %s);
+
+       if (\$accessors !== NULL) {
+               \$self->propertyAccessorReplacements['~/\*\* ###%s### \*/ .* /\*\* ###%s### \*/~s'] = '\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::resolvePropertyAccessors(\$currentVariableContainer->getOrNull(\'%s\'), ' . var_export(\$accessors, TRUE) . ')';
+               return \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::resolvePropertyAccessors(\$subject, \$accessors);
+       } else {
+               \$self->propertyAccessorReplacements['~/\*\* ###%s### \*/ .* /\*\* ###%s### \*/~s'] = '\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::getPropertyPath(\$currentVariableContainer->getOrNull(\'%s\'), \'%s\', \$renderingContext)';
+               return \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::getPropertyPath(\$subject, '%s', \$renderingContext);
+       }
+}, \$self) /** ###%s### */
+
+EOD;
                        return array(
                                'initialization' => '',
-                               'execution' => sprintf($executionCode, $firstPathElement, implode('.', $objectPathSegments))
+                               'execution' => sprintf($executionCode, $nodeIdentifier, $firstPathElement, var_export($objectPathSegments, TRUE), $nodeIdentifier, $nodeIdentifier, $firstPathElement, $nodeIdentifier, $nodeIdentifier, $firstPathElement, implode('.', $objectPathSegments), implode('.', $objectPathSegments), $nodeIdentifier)
                        );
                }
        }
index a4ba127..0ff518c 100644 (file)
@@ -92,4 +92,68 @@ class ObjectAccessorNode extends \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\Abstrac
                }
                return $subject;
        }
+
+       /**
+        * get property accessors for a given property path.
+        *
+        * @param mixed $subject
+        * @param array $propertyPathSegments
+        * @return null|string
+        */
+       static public function getPropertyAccessors($subject, $propertyPathSegments) {
+               $accessors = array();
+               foreach ($propertyPathSegments as $pathSegment) {
+                       if ($subject === NULL || is_scalar($subject)) {
+                               return NULL;
+                       }
+
+                       if (is_array($subject)) {
+                               $accessors[] = array('t' => 'a', 'c' => $pathSegment);
+                       }
+
+                       if (is_object($subject)) {
+                               $getterMethodName = 'get' . ucfirst($pathSegment);
+                               if (!is_callable(array($subject, $getterMethodName))) {
+                                       $getterMethodName = 'is' . ucfirst($pathSegment);
+                               }
+                               if (!is_callable(array($subject, $getterMethodName))) {
+                                       $getterMethodName = 'has' . ucfirst($pathSegment);
+                               }
+                               if (!is_callable(array($subject, $getterMethodName))) {
+                                       return NULL;
+                               }
+
+                               $accessors[] = array('t' => 'o', 'c' => $getterMethodName);
+                       }
+
+                       $subject = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getPropertyInternal($subject, $pathSegment, FALSE, $exists);
+               }
+
+               return $accessors;
+       }
+
+       /**
+        * Resolves property accessors from compiled templates.
+        *
+        * @param mixed $subject
+        * @param array $accessors
+        * @return mixed
+        */
+       static public function resolvePropertyAccessors($subject, array $accessors) {
+               foreach ($accessors as $accessor) {
+                       if ($subject === NULL) {
+                               return NULL;
+                       }
+
+                       if ($accessor['t'] === 'a') {
+                               $subject = $subject[$accessor['c']];
+                       }
+
+                       if ($accessor['t'] === 'o') {
+                               $subject = $subject->{$accessor['c']}();
+                       }
+               }
+
+               return $subject;
+       }
 }