[Form] Implemented a more intelligent caching strategy in FormHelper (PHP +100ms, Twig +100ms)

This commit is contained in:
Bernhard Schussek 2012-07-14 13:35:47 +02:00
parent 151b79a6ce
commit 216c539e41
1 changed files with 301 additions and 125 deletions

View File

@ -12,8 +12,8 @@
namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; namespace Symfony\Bundle\FrameworkBundle\Templating\Helper;
use Symfony\Component\Templating\Helper\Helper; use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Templating\EngineInterface; use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException; use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Extension\Core\View\ChoiceView;
@ -27,36 +27,68 @@ use Symfony\Component\Form\Util\FormUtil;
*/ */
class FormHelper extends Helper class FormHelper extends Helper
{ {
protected $engine; /**
* @var EngineInterface
*/
private $engine;
protected $csrfProvider; /**
* @var CsrfProviderInterface
*/
private $csrfProvider;
protected $varStack; /**
* @var array
*/
private $blockHierarchyMap = array();
protected $context; /**
* @var array
*/
private $currentHierarchyLevelMap = array();
protected $resources; /**
* @var array
*/
private $variableMap = array();
protected $themes; /**
* @var array
*/
private $stack = array();
protected $templates; /**
* @var array
*/
private $defaultThemes;
/**
* @var array
*/
private $themes = array();
/**
* @var array
*/
private $templateCache = array();
/**
* @var array
*/
private $templateHierarchyLevelCache = array();
/** /**
* Constructor. * Constructor.
* *
* @param EngineInterface $engine The templating engine * @param EngineInterface $engine The templating engine
* @param CsrfProviderInterface $csrfProvider The CSRF provider * @param CsrfProviderInterface $csrfProvider The CSRF provider
* @param array $resources An array of theme names * @param array $defaultThemes An array of theme names
*/ */
public function __construct(EngineInterface $engine, CsrfProviderInterface $csrfProvider = null, array $resources = array()) public function __construct(EngineInterface $engine, CsrfProviderInterface $csrfProvider = null, array $defaultThemes = array())
{ {
$this->engine = $engine; $this->engine = $engine;
$this->csrfProvider = $csrfProvider; $this->csrfProvider = $csrfProvider;
$this->resources = $resources; $this->defaultThemes = $defaultThemes;
$this->varStack = array();
$this->context = array();
$this->templates = array();
$this->themes = array();
} }
public function isChoiceGroup($label) public function isChoiceGroup($label)
@ -64,7 +96,7 @@ class FormHelper extends Helper
return FormUtil::isChoiceGroup($label); return FormUtil::isChoiceGroup($label);
} }
public function isChoiceSelected(FormView $view, ChoiceView $choice) public function isChoiceSelected(FormViewInterface $view, ChoiceView $choice)
{ {
return FormUtil::isChoiceSelected($choice->getValue(), $view->getVar('value')); return FormUtil::isChoiceSelected($choice->getValue(), $view->getVar('value'));
} }
@ -74,13 +106,14 @@ class FormHelper extends Helper
* *
* The theme format is "<Bundle>:<Controller>". * The theme format is "<Bundle>:<Controller>".
* *
* @param FormView $view A FormView instance * @param FormViewInterface $view A FormViewInterface instance
* @param string|array $themes A theme or an array of theme * @param string|array $themes A theme or an array of theme
*/ */
public function setTheme(FormView $view, $themes) public function setTheme(FormViewInterface $view, $themes)
{ {
$this->themes[$view->getVar('id')] = (array) $themes; $this->themes[$view->getVar('full_block_name')] = (array) $themes;
$this->templates = array(); $this->templateCache = array();
$this->templateHierarchyLevelCache = array();
} }
/** /**
@ -90,11 +123,11 @@ class FormHelper extends Helper
* *
* <form action="..." method="post" <?php echo $view['form']->enctype() ?>> * <form action="..." method="post" <?php echo $view['form']->enctype() ?>>
* *
* @param FormView $view The view for which to render the encoding type * @param FormViewInterface $view The view for which to render the encoding type
* *
* @return string The html markup * @return string The HTML markup
*/ */
public function enctype(FormView $view) public function enctype(FormViewInterface $view)
{ {
return $this->renderSection($view, 'enctype'); return $this->renderSection($view, 'enctype');
} }
@ -112,12 +145,12 @@ class FormHelper extends Helper
* *
* <?php echo view['form']->widget(array('separator' => '+++++)) ?> * <?php echo view['form']->widget(array('separator' => '+++++)) ?>
* *
* @param FormView $view The view for which to render the widget * @param FormViewInterface $view The view for which to render the widget
* @param array $variables Additional variables passed to the template * @param array $variables Additional variables passed to the template
* *
* @return string The html markup * @return string The HTML markup
*/ */
public function widget(FormView $view, array $variables = array()) public function widget(FormViewInterface $view, array $variables = array())
{ {
return $this->renderSection($view, 'widget', $variables); return $this->renderSection($view, 'widget', $variables);
} }
@ -125,12 +158,12 @@ class FormHelper extends Helper
/** /**
* Renders the entire form field "row". * Renders the entire form field "row".
* *
* @param FormView $view The view for which to render the row * @param FormViewInterface $view The view for which to render the row
* @param array $variables Additional variables passed to the template * @param array $variables Additional variables passed to the template
* *
* @return string The html markup * @return string The HTML markup
*/ */
public function row(FormView $view, array $variables = array()) public function row(FormViewInterface $view, array $variables = array())
{ {
return $this->renderSection($view, 'row', $variables); return $this->renderSection($view, 'row', $variables);
} }
@ -138,13 +171,13 @@ class FormHelper extends Helper
/** /**
* Renders the label of the given view. * Renders the label of the given view.
* *
* @param FormView $view The view for which to render the label * @param FormViewInterface $view The view for which to render the label
* @param string $label The label * @param string $label The label
* @param array $variables Additional variables passed to the template * @param array $variables Additional variables passed to the template
* *
* @return string The html markup * @return string The HTML markup
*/ */
public function label(FormView $view, $label = null, array $variables = array()) public function label(FormViewInterface $view, $label = null, array $variables = array())
{ {
if ($label !== null) { if ($label !== null) {
$variables += array('label' => $label); $variables += array('label' => $label);
@ -156,11 +189,11 @@ class FormHelper extends Helper
/** /**
* Renders the errors of the given view. * Renders the errors of the given view.
* *
* @param FormView $view The view to render the errors for * @param FormViewInterface $view The view to render the errors for
* *
* @return string The html markup * @return string The HTML markup
*/ */
public function errors(FormView $view) public function errors(FormViewInterface $view)
{ {
return $this->renderSection($view, 'errors'); return $this->renderSection($view, 'errors');
} }
@ -168,12 +201,12 @@ class FormHelper extends Helper
/** /**
* Renders views which have not already been rendered. * Renders views which have not already been rendered.
* *
* @param FormView $view The parent view * @param FormViewInterface $view The parent view
* @param array $variables An array of variables * @param array $variables An array of variables
* *
* @return string The html markup * @return string The HTML markup
*/ */
public function rest(FormView $view, array $variables = array()) public function rest(FormViewInterface $view, array $variables = array())
{ {
return $this->renderSection($view, 'rest', $variables); return $this->renderSection($view, 'rest', $variables);
} }
@ -200,6 +233,8 @@ class FormHelper extends Helper
* @param string $intention The intention of the protected action * @param string $intention The intention of the protected action
* *
* @return string A CSRF token * @return string A CSRF token
*
* @throws \BadMethodCallException When no CSRF provider was injected in the constructor.
*/ */
public function csrfToken($intention) public function csrfToken($intention)
{ {
@ -219,99 +254,154 @@ class FormHelper extends Helper
* 3. the type name is recursively replaced by the parent type name until a * 3. the type name is recursively replaced by the parent type name until a
* corresponding block is found * corresponding block is found
* *
* @param FormView $view The form view * @param FormViewInterface $view The form view
* @param string $section The section to render (i.e. 'row', 'widget', 'label', ...) * @param string $section The section to render (i.e. 'row', 'widget', 'label', ...)
* @param array $variables Additional variables * @param array $variables Additional variables
* *
* @return string The html markup * @return string The HTML markup
* *
* @throws FormException if no template block exists to render the given section of the view * @throws FormException if no template block exists to render the given section of the view
*/ */
protected function renderSection(FormView $view, $section, array $variables = array()) protected function renderSection(FormViewInterface $view, $section, array $variables = array())
{ {
$mainTemplate = in_array($section, array('row', 'widget')); $renderOnlyOnce = in_array($section, array('row', 'widget'));
if ($mainTemplate && $view->isRendered()) {
if ($renderOnlyOnce && $view->isRendered()) {
return ''; return '';
} }
$template = null; // The cache key for storing the variables and types
$mapKey = $view->getVar('full_block_name') . '_' . $section;
$custom = '_'.$view->getVar('id'); // In templates, we have to deal with two kinds of block hierarchies:
$rendering = $custom.$section; //
// +---------+ +---------+
// | Theme B | -------> | Theme A |
// +---------+ +---------+
//
// form_widget -------> form_widget
// ^
// |
// choice_widget -----> choice_widget
//
// The first kind of hierarchy is the theme hierarchy. This allows to
// override the block "choice_widget" from Theme A in the extending
// Theme B. This kind of inheritance needs to be supported by the
// template engine and, for example, offers "parent()" or similar
// functions to fall back from the custom to the parent implementation.
//
// The second kind of hierarchy is the form type hierarchy. This allows
// to implement a custom "choice_widget" block (no matter in which theme),
// or to fallback to the block of the parent type, which would be
// "form_widget" in this example (again, no matter in which theme).
// If the designer wants to explicitely fallback to "form_widget" in his
// custom "choice_widget", for example because he only wants to wrap
// a <div> around the original implementation, he can simply call the
// widget() function again to render the block for the parent type.
//
// The second kind is implemented in the following blocks.
if (!isset($this->blockHierarchyMap[$mapKey])) {
// INITIAL CALL
// Calculate the hierarchy of template blocks and start on
// the bottom level of the hierarchy (= "_<id>_<section>" block)
$blockHierarchy = array_map(function ($type) use ($section) {
return $type . '_' . $section;
}, $view->getVar('types'));
$blockHierarchy[] = $view->getVar('full_block_name') . '_' . $section;
$currentHierarchyLevel = count($blockHierarchy) - 1;
if (isset($this->varStack[$rendering])) { // The default variable scope contains all view variables, merged with
$typeIndex = $this->varStack[$rendering]['typeIndex'] - 1; // the variables passed explicitely to the helper
$types = $this->varStack[$rendering]['types'];
$variables = array_replace_recursive($this->varStack[$rendering]['variables'], $variables);
} else {
$types = $view->getVar('types');
$types[] = $view->getVar('full_block_name');
$typeIndex = count($types) - 1;
$variables = array_replace_recursive($view->getVars(), $variables); $variables = array_replace_recursive($view->getVars(), $variables);
$this->varStack[$rendering]['types'] = $types; } else {
// RECURSIVE CALL
// If a block recursively calls renderSection() again, resume rendering
// using the parent type in the hierarchy.
$blockHierarchy = $this->blockHierarchyMap[$mapKey];
$currentHierarchyLevel = $this->currentHierarchyLevelMap[$mapKey] - 1;
// Reuse the current scope and merge it with the explicitely passed variables
$variables = array_replace_recursive($this->variableMap[$mapKey], $variables);
} }
$this->varStack[$rendering]['variables'] = $variables; $cacheKey = $view->getVar('full_block_name');
$block = $blockHierarchy[$currentHierarchyLevel];
do { // Populate the cache if the template for the block is not known yet
$types[$typeIndex] .= '_'.$section; if (!isset($this->templateCache[$cacheKey][$block])) {
$template = $this->lookupTemplate($view, $types[$typeIndex]); $this->loadTemplateForBlockHierarchy($view, $blockHierarchy, $currentHierarchyLevel);
}
if ($template) { // Escape if no template exists for this block
if (!$this->templateCache[$cacheKey][$block]) {
throw new FormException(sprintf(
'Unable to render the form as none of the following blocks exist: "%s".',
implode('", "', array_reverse($blockHierarchy))
));
}
$this->varStack[$rendering]['typeIndex'] = $typeIndex; // If $block was previously rendered manually with renderBlock(), the template
// is cached but the hierarchy level is not. In this case, we know that the block
// exists at this very hierarchy level (renderBlock() does not traverse the hierarchy)
// so we can just set it.
if (!isset($this->templateHierarchyLevelCache[$cacheKey][$block])) {
$this->templateHierarchyLevelCache[$cacheKey][$block] = $currentHierarchyLevel;
}
$this->context[] = array( // In order to make recursive calls possible, we need to store the block hierarchy,
'variables' => $variables, // the current level of the hierarchy and the variables so that this method can
'view' => $view, // resume rendering one level higher of the hierarchy when it is called recursively.
); //
// We need to store these values in maps (associative arrays) because within a
// call to widget() another call to widget() can be made, but for a different view
// object. These nested calls should not override each other.
$this->blockHierarchyMap[$mapKey] = $blockHierarchy;
$this->currentHierarchyLevelMap[$mapKey] = $this->templateHierarchyLevelCache[$cacheKey][$block];
$this->variableMap[$mapKey] = $variables;
$html = $this->engine->render($template, $variables); // We also need to store the view and the variables so that we can render custom
// blocks with renderBlock() using the same themes and variables as in the outer
// block.
//
// A stack is sufficient for this purpose, because renderBlock() always accesses
// the immediate next outer scope, which is always stored at the end of the stack.
$this->stack[] = array($view, $variables);
array_pop($this->context); // Do the rendering
unset($this->varStack[$rendering]); $html = $this->engine->render($this->templateCache[$cacheKey][$block], $variables);
if ($mainTemplate) { // Clear the stack
array_pop($this->stack);
// Clear the maps
unset($this->blockHierarchyMap[$mapKey]);
unset($this->currentHierarchyLevelMap[$mapKey]);
unset($this->variableMap[$mapKey]);
if ($renderOnlyOnce) {
$view->setRendered(); $view->setRendered();
} }
return trim($html); return trim($html);
} }
} while (--$typeIndex >= 0);
throw new FormException(sprintf( public function renderBlock($block, $variables = array())
'Unable to render the form as none of the following blocks exist: "%s".',
implode('", "', array_reverse($types))
));
}
/**
* Render a block from a form element.
*
* @param string $name
* @param array $variables Additional variables (those would override the current context)
*
* @throws FormException if the block is not found
* @throws FormException if the method is called out of a form element (no context)
*/
public function renderBlock($name, $variables = array())
{ {
if (0 == count($this->context)) { if (0 == count($this->stack)) {
throw new FormException(sprintf('This method should only be called while rendering a form element.', $name)); throw new FormException('This method should only be called while rendering a form element.');
} }
$context = end($this->context); list($view, $scopeVariables) = end($this->stack);
$template = $this->lookupTemplate($context['view'], $name); $cacheKey = $view->getVar('full_block_name');
if (false === $template) { if (!isset($this->templateCache[$cacheKey][$block]) && !$this->loadTemplateForBlock($view, $block)) {
throw new FormException(sprintf('No block "%s" found while rendering the form.', $name)); throw new FormException(sprintf('No block "%s" found while rendering the form.', $block));
} }
$variables = array_replace_recursive($context['variables'], $variables); $variables = array_replace_recursive($scopeVariables, $variables);
return trim($this->engine->render($template, $variables)); return trim($this->engine->render($this->templateCache[$cacheKey][$block], $variables));
} }
public function humanize($text) public function humanize($text)
@ -325,41 +415,127 @@ class FormHelper extends Helper
} }
/** /**
* Returns the name of the template to use to render the block * Loads the cache with the template for a specific level of a block hierarchy.
* *
* @param FormView $view The form view * For example, the block hierarchy could be:
* @param string $block The name of the block
* *
* @return string|Boolean The template logical name or false when no template is found * <code>
* form_widget
* choice_widget
* entity_widget
* </code
*
* When loading this hierarchy at index 1, the method first tries to find the
* block "choice_widget" in any of the themes assigned to $view. If nothing is
* found, it then continues to look for "form_widget" and so on.
*
* This method both stores the template name and the level in the hierarchy at
* which the template was found in the cache. In the above example, if the
* template "MyBundle:choice_widget.html.php" was found at level 1, this template
* and the level "1" are stored. The stored level helps to resume rendering
* in recursive calls, where the parent block needs to be rendered (here the
* block "form_widget" at level 0).
*
* @param FormViewInterface $view The form view for finding the applying themes.
* @param array $blockHierarchy The block hierarchy, with the most specific block name at the end.
* @param integer $currentLevel The level in the block hierarchy that should be loaded.
*
* @return Boolean True if the cache could be populated successfully, false otherwise.
*/ */
protected function lookupTemplate(FormView $view, $block) private function loadTemplateForBlockHierarchy(FormViewInterface $view, array $blockHierarchy, $currentLevel)
{ {
$file = $block.'.html.php'; $cacheKey = $view->getVar('full_block_name');
$id = $view->getVar('id'); $block = $blockHierarchy[$currentLevel];
if (!isset($this->templates[$id][$block])) { // Try to find a template for that block
$template = false; if ($this->loadTemplateForBlock($view, $block)) {
// If loadTemplateForBlock() returns true, it was able to populate the
// cache. The only missing thing is to set the hierarchy level at which
// the template was found.
$this->templateHierarchyLevelCache[$cacheKey][$block] = $currentLevel;
$themes = $view->hasParent() ? array() : $this->resources; return true;
if (isset($this->themes[$id])) {
$themes = array_merge($themes, $this->themes[$id]);
} }
if ($currentLevel > 0) {
$parentLevel = $currentLevel - 1;
$parentBlock = $blockHierarchy[$parentLevel];
if ($this->loadTemplateForBlockHierarchy($view, $blockHierarchy, $parentLevel)) {
// Cache the shortcuts for further accesses
$this->templateCache[$cacheKey][$block] = $this->templateCache[$cacheKey][$parentBlock];
$this->templateHierarchyLevelCache[$cacheKey][$block] = $this->templateHierarchyLevelCache[$cacheKey][$parentBlock];
return true;
}
}
// Cache the result for further accesses
$this->templateCache[$cacheKey][$block] = false;
$this->templateHierarchyLevelCache[$cacheKey][$block] = false;
return false;
}
/**
* Loads the cache with the template for a given block name.
*
* The template is first searched in all the themes assigned to $view. If nothing
* is found, the search is continued in the themes of the parent view. Once arrived
* at the root view, if still nothing has been found, the default themes stored
* in this class are searched.
*
* @param FormViewInterface $view The form view for finding the applying themes.
* @param string $block The name of the block to load.
*
* @return Boolean True if the cache could be populated successfully, false otherwise.
*/
private function loadTemplateForBlock(FormViewInterface $view, $block)
{
// Recursively try to find the block in the themes assigned to $view,
// then of its parent form, then of the parent form of the parent and so on.
// When the root form is reached in this recursion, also the default
// themes are taken into account.
$cacheKey = $view->getVar('full_block_name');
// Check the default themes once we reach the root form without success
$themes = $view->hasParent() ? array() : $this->defaultThemes;
// Add the themes that have been registered for that specific element
if (isset($this->themes[$cacheKey])) {
$themes = array_merge($themes, $this->themes[$cacheKey]);
}
// Check each theme whether it contains the searched block
for ($i = count($themes) - 1; $i >= 0; --$i) { for ($i = count($themes) - 1; $i >= 0; --$i) {
if ($this->engine->exists($templateName = $themes[$i].':'.$file)) { if ($this->engine->exists($templateName = $themes[$i] . ':' . $block . '.html.php')) {
$template = $templateName; $this->templateCache[$cacheKey][$block] = $templateName;
break;
return true;
} }
} }
if (false === $template && $view->hasParent()) { // If we did not find anything in the themes of the current view, proceed
$template = $this->lookupTemplate($view->getParent(), $block); // with the themes of the parent view
if ($view->hasParent()) {
$parentCacheKey = $view->getParent()->getVar('full_block_name');
if (!isset($this->templateCache[$parentCacheKey][$block])) {
$this->loadTemplateForBlock($view->getParent(), $block);
} }
$this->templates[$id][$block] = $template; // If a template exists in the parent themes, cache that template name
// for the current theme as well to speed up further accesses
if ($this->templateCache[$parentCacheKey][$block]) {
$this->templateCache[$cacheKey][$block] = $this->templateCache[$parentCacheKey][$block];
return true;
}
} }
return $this->templates[$id][$block]; // Cache that we didn't find anything to speed up further accesses
$this->templateCache[$cacheKey][$block] = false;
return false;
} }
} }