diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php index a2b5afc1d5..46ed645b90 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @@ -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 ":". * - * @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 * *
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 * * 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
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 (= "__
" 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 + * + * form_widget + * choice_widget + * entity_widget + * 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; } }