[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;
use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
@ -27,36 +27,68 @@ use Symfony\Component\Form\Util\FormUtil;
*/
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.
*
* @param EngineInterface $engine The templating engine
* @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->csrfProvider = $csrfProvider;
$this->resources = $resources;
$this->varStack = array();
$this->context = array();
$this->templates = array();
$this->themes = array();
$this->defaultThemes = $defaultThemes;
}
public function isChoiceGroup($label)
@ -64,7 +96,7 @@ class FormHelper extends Helper
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'));
}
@ -74,13 +106,14 @@ class FormHelper extends Helper
*
* 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
*/
public function setTheme(FormView $view, $themes)
public function setTheme(FormViewInterface $view, $themes)
{
$this->themes[$view->getVar('id')] = (array) $themes;
$this->templates = array();
$this->themes[$view->getVar('full_block_name')] = (array) $themes;
$this->templateCache = array();
$this->templateHierarchyLevelCache = array();
}
/**
@ -90,11 +123,11 @@ class FormHelper extends Helper
*
* <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');
}
@ -112,12 +145,12 @@ class FormHelper extends Helper
*
* <?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
*
* @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);
}
@ -125,12 +158,12 @@ class FormHelper extends Helper
/**
* 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
*
* @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);
}
@ -138,13 +171,13 @@ class FormHelper extends Helper
/**
* 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 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) {
$variables += array('label' => $label);
@ -156,11 +189,11 @@ class FormHelper extends Helper
/**
* 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');
}
@ -168,12 +201,12 @@ class FormHelper extends Helper
/**
* 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
*
* @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);
}
@ -200,6 +233,8 @@ class FormHelper extends Helper
* @param string $intention The intention of the protected action
*
* @return string A CSRF token
*
* @throws \BadMethodCallException When no CSRF provider was injected in the constructor.
*/
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
* 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 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
*/
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'));
if ($mainTemplate && $view->isRendered()) {
$renderOnlyOnce = in_array($section, array('row', 'widget'));
return '';
if ($renderOnlyOnce && $view->isRendered()) {
return '';
}
$template = null;
// The cache key for storing the variables and types
$mapKey = $view->getVar('full_block_name') . '_' . $section;
$custom = '_'.$view->getVar('id');
$rendering = $custom.$section;
// In templates, we have to deal with two kinds of block hierarchies:
//
// +---------+ +---------+
// | 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])) {
$typeIndex = $this->varStack[$rendering]['typeIndex'] - 1;
$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;
// The default variable scope contains all view variables, merged with
// the variables passed explicitely to the helper
$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 {
$types[$typeIndex] .= '_'.$section;
$template = $this->lookupTemplate($view, $types[$typeIndex]);
// Populate the cache if the template for the block is not known yet
if (!isset($this->templateCache[$cacheKey][$block])) {
$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(
'variables' => $variables,
'view' => $view,
);
// In order to make recursive calls possible, we need to store the block hierarchy,
// the current level of the hierarchy and the variables so that this method can
// 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);
unset($this->varStack[$rendering]);
// Do the rendering
$html = $this->engine->render($this->templateCache[$cacheKey][$block], $variables);
if ($mainTemplate) {
$view->setRendered();
}
// Clear the stack
array_pop($this->stack);
return trim($html);
}
} while (--$typeIndex >= 0);
// Clear the maps
unset($this->blockHierarchyMap[$mapKey]);
unset($this->currentHierarchyLevelMap[$mapKey]);
unset($this->variableMap[$mapKey]);
throw new FormException(sprintf(
'Unable to render the form as none of the following blocks exist: "%s".',
implode('", "', array_reverse($types))
));
if ($renderOnlyOnce) {
$view->setRendered();
}
return trim($html);
}
/**
* 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())
public function renderBlock($block, $variables = array())
{
if (0 == count($this->context)) {
throw new FormException(sprintf('This method should only be called while rendering a form element.', $name));
if (0 == count($this->stack)) {
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) {
throw new FormException(sprintf('No block "%s" found while rendering the form.', $name));
if (!isset($this->templateCache[$cacheKey][$block]) && !$this->loadTemplateForBlock($view, $block)) {
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)
@ -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
* @param string $block The name of the block
* For example, the block hierarchy could be:
*
* @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';
$id = $view->getVar('id');
$cacheKey = $view->getVar('full_block_name');
$block = $blockHierarchy[$currentLevel];
if (!isset($this->templates[$id][$block])) {
$template = false;
// Try to find a template for that block
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;
if (isset($this->themes[$id])) {
$themes = array_merge($themes, $this->themes[$id]);
}
for ($i = count($themes) - 1; $i >= 0; --$i) {
if ($this->engine->exists($templateName = $themes[$i].':'.$file)) {
$template = $templateName;
break;
}
}
if (false === $template && $view->hasParent()) {
$template = $this->lookupTemplate($view->getParent(), $block);
}
$this->templates[$id][$block] = $template;
return true;
}
return $this->templates[$id][$block];
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) {
if ($this->engine->exists($templateName = $themes[$i] . ':' . $block . '.html.php')) {
$this->templateCache[$cacheKey][$block] = $templateName;
return true;
}
}
// If we did not find anything in the themes of the current view, proceed
// 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);
}
// 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;
}
}
// Cache that we didn't find anything to speed up further accesses
$this->templateCache[$cacheKey][$block] = false;
return false;
}
}