[Form] Extracted common parts of FormHelper and FormExtension into separate classes

This commit is contained in:
Bernhard Schussek 2012-07-15 18:58:13 +02:00
parent 216c539e41
commit 629093ed25
57 changed files with 1572 additions and 856 deletions

View File

@ -1053,6 +1053,21 @@
$registry->addType($registry->resolveType(new MyFormType()));
```
* The method `renderBlock()` of the helper for the PHP Templating component was
deprecated and will be removed in Symfony 2.3. You should use `block()` instead.
Before:
```
<?php echo $view['form']->renderBlock('widget_attributes') ?>
```
After:
```
<?php echo $view['form']->block('widget_attributes') ?>
```
### Validator
* The methods `setMessage()`, `getMessageTemplate()` and

View File

@ -12,11 +12,9 @@
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Bridge\Twig\Form\TwigRendererInterface;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Util\FormUtil;
/**
* FormExtension extends Twig with form capabilities.
@ -26,21 +24,17 @@ use Symfony\Component\Form\Util\FormUtil;
*/
class FormExtension extends \Twig_Extension
{
protected $csrfProvider;
protected $resources;
protected $blocks;
protected $environment;
protected $themes;
protected $varStack;
protected $template;
/**
* This property is public so that it can be accessed directly from compiled
* templates without having to call a getter, which slightly decreases performance.
*
* @var \Symfony\Component\Form\FormRendererInterface
*/
public $renderer;
public function __construct(CsrfProviderInterface $csrfProvider = null, array $resources = array())
public function __construct(TwigRendererInterface $renderer)
{
$this->csrfProvider = $csrfProvider;
$this->themes = new \SplObjectStorage();
$this->varStack = array();
$this->blocks = new \SplObjectStorage();
$this->resources = $resources;
$this->renderer = $renderer;
}
/**
@ -48,25 +42,11 @@ class FormExtension extends \Twig_Extension
*/
public function initRuntime(\Twig_Environment $environment)
{
$this->environment = $environment;
$this->renderer->setEnvironment($environment);
}
/**
* Sets a theme for a given view.
*
* @param FormView $view A FormView instance
* @param array|string $resources An array of resource names|a resource name
*/
public function setTheme(FormView $view, $resources)
{
$this->themes->attach($view, (array) $resources);
$this->blocks = new \SplObjectStorage();
}
/**
* Returns the token parser instance to add to the existing list.
*
* @return array An array of Twig_TokenParser instances
* {@inheritdoc}
*/
public function getTokenParsers()
{
@ -76,305 +56,39 @@ class FormExtension extends \Twig_Extension
);
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return array(
'form_enctype' => new \Twig_Function_Method($this, 'renderEnctype', array('is_safe' => array('html'))),
'form_widget' => new \Twig_Function_Method($this, 'renderWidget', array('is_safe' => array('html'))),
'form_errors' => new \Twig_Function_Method($this, 'renderErrors', array('is_safe' => array('html'))),
'form_label' => new \Twig_Function_Method($this, 'renderLabel', array('is_safe' => array('html'))),
'form_row' => new \Twig_Function_Method($this, 'renderRow', array('is_safe' => array('html'))),
'form_rest' => new \Twig_Function_Method($this, 'renderRest', array('is_safe' => array('html'))),
'csrf_token' => new \Twig_Function_Method($this, 'getCsrfToken'),
'_form_is_choice_group' => new \Twig_Function_Method($this, 'isChoiceGroup', array('is_safe' => array('html'))),
'_form_is_choice_selected' => new \Twig_Function_Method($this, 'isChoiceSelected', array('is_safe' => array('html'))),
'form_enctype' => new \Twig_Function_Method($this, 'renderer->renderEnctype', array('is_safe' => array('html'))),
'form_widget' => new \Twig_Function_Method($this, 'renderer->renderWidget', array('is_safe' => array('html'))),
'form_errors' => new \Twig_Function_Method($this, 'renderer->renderErrors', array('is_safe' => array('html'))),
'form_label' => new \Twig_Function_Method($this, 'renderer->renderLabel', array('is_safe' => array('html'))),
'form_row' => new \Twig_Function_Method($this, 'renderer->renderRow', array('is_safe' => array('html'))),
'form_rest' => new \Twig_Function_Method($this, 'renderer->renderRest', array('is_safe' => array('html'))),
'csrf_token' => new \Twig_Function_Method($this, 'renderer->renderCsrfToken'),
'_form_is_choice_group' => new \Twig_Function_Method($this, 'renderer->isChoiceGroup', array('is_safe' => array('html'))),
'_form_is_choice_selected' => new \Twig_Function_Method($this, 'renderer->isChoiceSelected', array('is_safe' => array('html'))),
);
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return array(
'humanize' => new \Twig_Filter_Function(__NAMESPACE__.'\humanize'),
'humanize' => new \Twig_Filter_Method($this, 'renderer->humanize'),
);
}
public function isChoiceGroup($label)
{
return FormUtil::isChoiceGroup($label);
}
public function isChoiceSelected(FormView $view, ChoiceView $choice)
{
return FormUtil::isChoiceSelected($choice->getValue(), $view->getVar('value'));
}
/**
* Renders the HTML enctype in the form tag, if necessary
*
* Example usage in Twig templates:
*
* <form action="..." method="post" {{ form_enctype(form) }}>
*
* @param FormView $view The view for which to render the encoding type
*
* @return string The html markup
*/
public function renderEnctype(FormView $view)
{
return $this->render($view, 'enctype');
}
/**
* Renders a row for the view.
*
* @param FormView $view The view to render as a row
* @param array $variables An array of variables
*
* @return string The html markup
*/
public function renderRow(FormView $view, array $variables = array())
{
return $this->render($view, 'row', $variables);
}
/**
* Renders views which have not already been rendered.
*
* @param FormView $view The parent view
* @param array $variables An array of variables
*
* @return string The html markup
*/
public function renderRest(FormView $view, array $variables = array())
{
return $this->render($view, 'rest', $variables);
}
/**
* Renders the HTML for a given view
*
* Example usage in Twig:
*
* {{ form_widget(view) }}
*
* You can pass options during the call:
*
* {{ form_widget(view, {'attr': {'class': 'foo'}}) }}
*
* {{ form_widget(view, {'separator': '+++++'}) }}
*
* @param FormView $view The view to render
* @param array $variables Additional variables passed to the template
*
* @return string The html markup
*/
public function renderWidget(FormView $view, array $variables = array())
{
return $this->render($view, 'widget', $variables);
}
/**
* Renders the errors of the given view
*
* @param FormView $view The view to render the errors for
*
* @return string The html markup
*/
public function renderErrors(FormView $view)
{
return $this->render($view, 'errors');
}
/**
* Renders the label of the given view
*
* @param FormView $view The view to render the label for
* @param string $label Label name
* @param array $variables Additional variables passed to the template
*
* @return string The html markup
*/
public function renderLabel(FormView $view, $label = null, array $variables = array())
{
if ($label !== null) {
$variables += array('label' => $label);
}
return $this->render($view, 'label', $variables);
}
/**
* Renders a template.
*
* 1. This function first looks for a block named "_<view id>_<section>",
* 2. if such a block is not found the function will look for a block named
* "<type name>_<section>",
* 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 string $section The section to render (i.e. 'row', 'widget', 'label', ...)
* @param array $variables Additional variables
*
* @return string The html markup
*
* @throws FormException if no template block exists to render the given section of the view
*/
protected function render(FormView $view, $section, array $variables = array())
{
$mainTemplate = in_array($section, array('widget', 'row'));
if ($mainTemplate && $view->isRendered()) {
return '';
}
if (null === $this->template) {
$this->template = reset($this->resources);
if (!$this->template instanceof \Twig_Template) {
$this->template = $this->environment->loadTemplate($this->template);
}
}
$custom = '_'.$view->getVar('id');
$rendering = $custom.$section;
$blocks = $this->getBlocks($view);
if (isset($this->varStack[$rendering])) {
$typeIndex = $this->varStack[$rendering]['typeIndex'] - 1;
$types = $this->varStack[$rendering]['types'];
$this->varStack[$rendering]['variables'] = array_replace_recursive($this->varStack[$rendering]['variables'], $variables);
} else {
$types = $view->getVar('types');
$types[] = $view->getVar('full_block_name');
$typeIndex = count($types) - 1;
$this->varStack[$rendering] = array(
'variables' => array_replace_recursive($view->getVars(), $variables),
'types' => $types,
);
}
do {
$types[$typeIndex] .= '_'.$section;
if (isset($blocks[$types[$typeIndex]])) {
$this->varStack[$rendering]['typeIndex'] = $typeIndex;
$context = $this->environment->mergeGlobals($this->varStack[$rendering]['variables']);
// we do not call renderBlock here to avoid too many nested level calls (XDebug limits the level to 100 by default)
ob_start();
$this->template->displayBlock($types[$typeIndex], $context, $blocks);
$html = ob_get_clean();
if ($mainTemplate) {
$view->setRendered();
}
unset($this->varStack[$rendering]);
return $html;
}
} while (--$typeIndex >= 0);
throw new FormException(sprintf(
'Unable to render the form as none of the following blocks exist: "%s".',
implode('", "', array_reverse($types))
));
}
/**
* Returns a CSRF token.
*
* Use this helper for CSRF protection without the overhead of creating a
* form.
*
* <code>
* <input type="hidden" name="token" value="{{ csrf_token('rm_user_' ~ user.id) }}">
* </code>
*
* Check the token in your action using the same intention.
*
* <code>
* $csrfProvider = $this->get('form.csrf_provider');
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
* throw new \RuntimeException('CSRF attack detected.');
* }
* </code>
*
* @param string $intention The intention of the protected action
*
* @return string A CSRF token
*/
public function getCsrfToken($intention)
{
if (!$this->csrfProvider instanceof CsrfProviderInterface) {
throw new \BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.');
}
return $this->csrfProvider->generateCsrfToken($intention);
}
/**
* Returns the name of the extension.
*
* @return string The extension name
* {@inheritdoc}
*/
public function getName()
{
return 'form';
}
/**
* Returns the blocks used to render the view.
*
* Templates are looked for in the resources in the following order:
* * resources from the themes (and its parents)
* * resources from the themes of parent views (up to the root view)
* * default resources
*
* @param FormView $view The view
*
* @return array An array of Twig_TemplateInterface instances
*/
protected function getBlocks(FormView $view)
{
if (!$this->blocks->contains($view)) {
$rootView = !$view->hasParent();
$templates = $rootView ? $this->resources : array();
if (isset($this->themes[$view])) {
$templates = array_merge($templates, $this->themes[$view]);
}
$blocks = array();
foreach ($templates as $template) {
if (!$template instanceof \Twig_Template) {
$template = $this->environment->loadTemplate($template);
}
$templateBlocks = array();
do {
$templateBlocks = array_merge($template->getBlocks(), $templateBlocks);
} while (false !== $template = $template->getParent(array()));
$blocks = array_merge($blocks, $templateBlocks);
}
if (!$rootView) {
$blocks = array_merge($this->getBlocks($view->getParent()), $blocks);
}
$this->blocks->attach($view, $blocks);
} else {
$blocks = $this->blocks[$view];
}
return $blocks;
}
}
function humanize($text)
{
return ucfirst(trim(strtolower(preg_replace('/[_\s]+/', ' ', $text))));
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Form;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TwigRenderer extends FormRenderer implements TwigRendererInterface
{
/**
* @var TwigRendererEngineInterface
*/
private $engine;
public function __construct(TwigRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null)
{
parent::__construct($engine, $csrfProvider);
$this->engine = $engine;
}
/**
* {@inheritdoc}
*/
public function setEnvironment(\Twig_Environment $environment)
{
$this->engine->setEnvironment($environment);
}
}

View File

@ -0,0 +1,160 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Form;
use Symfony\Component\Form\AbstractRendererEngine;
use Symfony\Component\Form\FormViewInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TwigRendererEngine extends AbstractRendererEngine implements TwigRendererEngineInterface
{
/**
* @var \Twig_Environment
*/
private $environment;
/**
* @var \Twig_Template
*/
private $template;
/**
* {@inheritdoc}
*/
public function setEnvironment(\Twig_Environment $environment)
{
$this->environment = $environment;
}
/**
* {@inheritdoc}
*/
public function renderBlock(FormViewInterface $view, $resource, $block, array $variables = array())
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
$context = $this->environment->mergeGlobals($variables);
ob_start();
// By contract,This method can only be called after getting the resource
// (which is passed to the method). Getting a resource for the first time
// (with an empty cache) is guaranteed to invoke loadResourcesFromTheme(),
// where the property $template is initialized.
// We do not call renderBlock here to avoid too many nested level calls
// (XDebug limits the level to 100 by default)
$this->template->displayBlock($block, $context, $this->resources[$cacheKey]);
return ob_get_clean();
}
/**
* Loads the cache with the resource for a given block name.
*
* This implementation eagerly loads all blocks of the themes assigned to the given view
* and all of its ancestors views. This is necessary, because Twig receives the
* list of blocks later. At that point, all blocks must already be loaded, for the
* case that the function "block()" is used in the Twig template.
*
* @see getResourceForBlock()
*
* @param string $cacheKey The cache key of the form view.
* @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 resource could be loaded, false otherwise.
*/
protected function loadResourceForBlock($cacheKey, FormViewInterface $view, $block)
{
// Recursively try to find the block in the themes assigned to $view,
// then of its parent view, then of the parent view of the parent and so on.
// When the root view is reached in this recursion, also the default
// themes are taken into account.
// Check each theme whether it contains the searched block
if (isset($this->themes[$cacheKey])) {
for ($i = count($this->themes[$cacheKey]) - 1; $i >= 0; --$i) {
$this->loadResourcesFromTheme($cacheKey, $this->themes[$cacheKey][$i]);
// CONTINUE LOADING (see doc comment)
}
}
// Check the default themes once we reach the root view without success
if (!$view->hasParent()) {
for ($i = count($this->defaultThemes) - 1; $i >= 0; --$i) {
$this->loadResourcesFromTheme($cacheKey, $this->defaultThemes[$i]);
// CONTINUE LOADING (see doc comment)
}
}
// 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(self::CACHE_KEY_VAR);
if (!isset($this->resources[$parentCacheKey])) {
$this->loadResourceForBlock($parentCacheKey, $view->getParent(), $block);
}
// EAGER CACHE POPULATION (see doc comment)
foreach ($this->resources[$parentCacheKey] as $blockName => $resource) {
if (!isset($this->resources[$cacheKey][$blockName])) {
$this->resources[$cacheKey][$blockName] = $resource;
}
}
}
if (!isset($this->resources[$cacheKey][$block])) {
// Cache that we didn't find anything to speed up further accesses
$this->resources[$cacheKey][$block] = false;
}
return false !== $this->resources[$cacheKey][$block];
}
/**
* Loads the resources for all blocks in a theme.
*
* @param string $cacheKey The cache key for storing the resource.
* @param mixed $theme The theme to load the block from. This parameter
* is passed by reference, because it might be necessary
* to initialize the theme first. Any changes made to
* this variable will be kept and be available upon
* further calls to this method using the same theme.
*/
protected function loadResourcesFromTheme($cacheKey, &$theme)
{
if (!$theme instanceof \Twig_Template) {
/* @var \Twig_Template $theme */
$theme = $this->environment->loadTemplate($theme);
}
if (null === $this->template) {
// Store the first \Twig_Template instance that we find so that
// we can call displayBlock() later on. It doesn't matter *which*
// template we use for that, since we pass the used blocks manually
// anyway.
$this->template = $theme;
}
foreach ($theme->getBlocks() as $block => $blockData) {
if (!isset($this->resources[$cacheKey][$block])) {
// The resource given back is the key to the bucket that
// contains this block.
$this->resources[$cacheKey][$block] = $blockData;
}
}
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Form;
use Symfony\Component\Form\FormRendererEngineInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface TwigRendererEngineInterface extends FormRendererEngineInterface
{
/**
* Sets Twig's environment.
*
* @param \Twig_Environment $environment
*/
public function setEnvironment(\Twig_Environment $environment);
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Form;
use Symfony\Component\Form\FormRendererInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface TwigRendererInterface extends FormRendererInterface
{
/**
* Sets Twig's environment.
*
* @param \Twig_Environment $environment
*/
public function setEnvironment(\Twig_Environment $environment);
}

View File

@ -30,7 +30,7 @@ class FormThemeNode extends \Twig_Node
{
$compiler
->addDebugInfo($this)
->write('echo $this->env->getExtension(\'form\')->setTheme(')
->write('echo $this->env->getExtension(\'form\')->renderer->setTheme(')
->subcompile($this->getNode('form'))
->raw(', ')
->subcompile($this->getNode('resources'))

View File

@ -12,6 +12,8 @@
namespace Symfony\Bridge\Twig\Tests\Extension;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator;
use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader;
@ -20,6 +22,9 @@ use Symfony\Component\Form\Tests\AbstractDivLayoutTest;
class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
{
/**
* @var FormExtension
*/
protected $extension;
protected function setUp()
@ -42,20 +47,23 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
parent::setUp();
$loader = new StubFilesystemLoader(array(
__DIR__.'/../../../../../../src/Symfony/Bridge/Twig/Resources/views/Form',
__DIR__,
));
$this->extension = new FormExtension($this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
$rendererEngine = new TwigRendererEngine(array(
'form_div_layout.html.twig',
'custom_widgets.html.twig',
));
$renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
$this->extension = new FormExtension($renderer);
$loader = new StubFilesystemLoader(array(
__DIR__.'/../../Resources/views/Form',
__DIR__,
));
$environment = new \Twig_Environment($loader, array('strict_variables' => true));
$environment->addExtension($this->extension);
$environment->addExtension(new TranslationExtension(new StubTranslator()));
$environment->addGlobal('global', '');
$environment->addExtension($this->extension);
$this->extension->initRuntime($environment);
}
@ -99,37 +107,37 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
protected function renderEnctype(FormView $view)
{
return (string) $this->extension->renderEnctype($view);
return (string) $this->extension->renderer->renderEnctype($view);
}
protected function renderLabel(FormView $view, $label = null, array $vars = array())
{
return (string) $this->extension->renderLabel($view, $label, $vars);
return (string) $this->extension->renderer->renderLabel($view, $label, $vars);
}
protected function renderErrors(FormView $view)
{
return (string) $this->extension->renderErrors($view);
return (string) $this->extension->renderer->renderErrors($view);
}
protected function renderWidget(FormView $view, array $vars = array())
{
return (string) $this->extension->renderWidget($view, $vars);
return (string) $this->extension->renderer->renderWidget($view, $vars);
}
protected function renderRow(FormView $view, array $vars = array())
{
return (string) $this->extension->renderRow($view, $vars);
return (string) $this->extension->renderer->renderRow($view, $vars);
}
protected function renderRest(FormView $view, array $vars = array())
{
return (string) $this->extension->renderRest($view, $vars);
return (string) $this->extension->renderer->renderRest($view, $vars);
}
protected function setTheme(FormView $view, array $themes)
{
$this->extension->setTheme($view, $themes);
$this->extension->renderer->setTheme($view, $themes);
}
public static function themeBlockInheritanceProvider()

View File

@ -12,6 +12,8 @@
namespace Symfony\Bridge\Twig\Tests\Extension;
use Symfony\Component\Form\FormView;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Component\Form\Tests\AbstractTableLayoutTest;
@ -20,6 +22,9 @@ use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader;
class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
{
/**
* @var FormExtension
*/
protected $extension;
protected function setUp()
@ -42,20 +47,23 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
parent::setUp();
$loader = new StubFilesystemLoader(array(
__DIR__.'/../../../../../../src/Symfony/Bridge/Twig/Resources/views/Form',
__DIR__,
));
$this->extension = new FormExtension($this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
$rendererEngine = new TwigRendererEngine(array(
'form_table_layout.html.twig',
'custom_widgets.html.twig',
));
$renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
$this->extension = new FormExtension($renderer);
$loader = new StubFilesystemLoader(array(
__DIR__.'/../../Resources/views/Form',
__DIR__,
));
$environment = new \Twig_Environment($loader, array('strict_variables' => true));
$environment->addExtension($this->extension);
$environment->addExtension(new TranslationExtension(new StubTranslator()));
$environment->addGlobal('global', '');
$environment->addExtension($this->extension);
$this->extension->initRuntime($environment);
}
@ -69,36 +77,36 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
protected function renderEnctype(FormView $view)
{
return (string) $this->extension->renderEnctype($view);
return (string) $this->extension->renderer->renderEnctype($view);
}
protected function renderLabel(FormView $view, $label = null, array $vars = array())
{
return (string) $this->extension->renderLabel($view, $label, $vars);
return (string) $this->extension->renderer->renderLabel($view, $label, $vars);
}
protected function renderErrors(FormView $view)
{
return (string) $this->extension->renderErrors($view);
return (string) $this->extension->renderer->renderErrors($view);
}
protected function renderWidget(FormView $view, array $vars = array())
{
return (string) $this->extension->renderWidget($view, $vars);
return (string) $this->extension->renderer->renderWidget($view, $vars);
}
protected function renderRow(FormView $view, array $vars = array())
{
return (string) $this->extension->renderRow($view, $vars);
return (string) $this->extension->renderer->renderRow($view, $vars);
}
protected function renderRest(FormView $view, array $vars = array())
{
return (string) $this->extension->renderRest($view, $vars);
return (string) $this->extension->renderer->renderRest($view, $vars);
}
protected function setTheme(FormView $view, array $themes)
{
$this->extension->setTheme($view, $themes);
$this->extension->renderer->setTheme($view, $themes);
}
}

View File

@ -15,6 +15,8 @@
<parameter key="templating.helper.code.class">Symfony\Bundle\FrameworkBundle\Templating\Helper\CodeHelper</parameter>
<parameter key="templating.helper.translator.class">Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper</parameter>
<parameter key="templating.helper.form.class">Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper</parameter>
<parameter key="templating.form.engine.class">Symfony\Component\Form\Extension\Templating\TemplatingRendererEngine</parameter>
<parameter key="templating.form.renderer.class">Symfony\Component\Form\FormRenderer</parameter>
<parameter key="templating.globals.class">Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables</parameter>
<parameter key="templating.asset.path_package.class">Symfony\Bundle\FrameworkBundle\Templating\Asset\PathPackage</parameter>
<parameter key="templating.asset.url_package.class">Symfony\Component\Templating\Asset\UrlPackage</parameter>
@ -96,11 +98,19 @@
<service id="templating.helper.form" class="%templating.helper.form.class%">
<tag name="templating.helper" alias="form" />
<argument type="service" id="templating.form.renderer" />
</service>
<service id="templating.form.engine" class="%templating.form.engine.class%" public="false">
<argument type="service" id="templating.engine.php" />
<argument type="service" id="form.csrf_provider" on-invalid="null" />
<argument>%templating.helper.form.resources%</argument>
</service>
<service id="templating.form.renderer" class="%templating.form.renderer.class%" public="false">
<argument type="service" id="templating.form.engine" />
<argument type="service" id="form.csrf_provider" on-invalid="null" />
</service>
<service id="templating.globals" class="%templating.globals.class%">
<argument type="service" id="service_container" />
</service>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>

View File

@ -1,5 +1,5 @@
<input type="checkbox"
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>
<?php if ($value): ?> value="<?php echo $view->escape($value) ?>"<?php endif ?>
<?php if ($checked): ?> checked="checked"<?php endif ?>
/>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('choice_widget_options') ?>
<?php echo $view['form']->block('choice_widget_options') ?>

View File

@ -1,5 +1,5 @@
<?php if ($expanded): ?>
<?php echo $view['form']->renderBlock('choice_widget_expanded') ?>
<?php echo $view['form']->block('choice_widget_expanded') ?>
<?php else: ?>
<?php echo $view['form']->renderBlock('choice_widget_collapsed') ?>
<?php echo $view['form']->block('choice_widget_collapsed') ?>
<?php endif ?>

View File

@ -1,13 +1,13 @@
<select
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>
<?php if ($multiple): ?> multiple="multiple"<?php endif ?>
>
<?php if (null !== $empty_value): ?><option value=""><?php echo $view->escape($view['translator']->trans($empty_value, array(), $translation_domain)) ?></option><?php endif; ?>
<?php if (count($preferred_choices) > 0): ?>
<?php echo $view['form']->renderBlock('choice_widget_options', array('options' => $preferred_choices)) ?>
<?php echo $view['form']->block('choice_widget_options', array('options' => $preferred_choices)) ?>
<?php if (count($choices) > 0 && null !== $separator): ?>
<option disabled="disabled"><?php echo $separator ?></option>
<?php endif ?>
<?php endif ?>
<?php echo $view['form']->renderBlock('choice_widget_options', array('options' => $choices)) ?>
<?php echo $view['form']->block('choice_widget_options', array('options' => $choices)) ?>
</select>

View File

@ -1,4 +1,4 @@
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php foreach ($form as $child): ?>
<?php echo $view['form']->widget($child) ?>
<?php echo $view['form']->label($child) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('widget_container_attributes') ?>
<?php echo $view['form']->block('widget_container_attributes') ?>

View File

@ -1,7 +1,7 @@
<?php if ($widget == 'single_text'): ?>
<?php echo $view['form']->renderBlock('form_widget_simple'); ?>
<?php echo $view['form']->block('form_widget_simple'); ?>
<?php else: ?>
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php echo str_replace(array('{{ year }}', '{{ month }}', '{{ day }}'), array(
$view['form']->widget($form['year']),
$view['form']->widget($form['month']),

View File

@ -1,7 +1,7 @@
<?php if ($widget == 'single_text'): ?>
<?php echo $view['form']->renderBlock('form_widget_simple'); ?>
<?php echo $view['form']->block('form_widget_simple'); ?>
<?php else: ?>
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php echo $view['form']->widget($form['date']).' '.$view['form']->widget($form['time']) ?>
</div>
<?php endif ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : 'email')) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : 'email')) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_enctype') ?>
<?php echo $view['form']->block('form_enctype') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_errors') ?>
<?php echo $view['form']->block('form_errors') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_label') ?>
<?php echo $view['form']->block('form_label') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_rest') ?>
<?php echo $view['form']->block('form_rest') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_row') ?>
<?php echo $view['form']->block('form_row') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_rows') ?>
<?php echo $view['form']->block('form_rows') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple') ?>
<?php echo $view['form']->block('form_widget_simple') ?>

View File

@ -1,5 +1,5 @@
<?php if ($compound): ?>
<?php echo $view['form']->renderBlock('form_widget_compound')?>
<?php echo $view['form']->block('form_widget_compound')?>
<?php else: ?>
<?php echo $view['form']->renderBlock('form_widget_simple')?>
<?php echo $view['form']->block('form_widget_simple')?>
<?php endif ?>

View File

@ -1,4 +1,4 @@
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php if (!$form->hasParent() && $errors): ?>
<tr>
<td colspan="2">
@ -6,6 +6,6 @@
</td>
</tr>
<?php endif ?>
<?php echo $view['form']->renderBlock('form_rows') ?>
<?php echo $view['form']->block('form_rows') ?>
<?php echo $view['form']->rest($form) ?>
</div>

View File

@ -1,5 +1,5 @@
<input
type="<?php echo isset($type) ? $view->escape($type) : 'text' ?>"
<?php if (!empty($value)): ?>value="<?php echo $view->escape($value) ?>"<?php endif ?>
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>
/>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "hidden")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "hidden")) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "number")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "number")) ?>

View File

@ -1 +1 @@
<?php echo str_replace('{{ widget }}', $view['form']->renderBlock('form_widget_simple'), $money_pattern) ?>
<?php echo str_replace('{{ widget }}', $view['form']->block('form_widget_simple'), $money_pattern) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "text")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "text")) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "password")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "password")) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "text")) ?> %
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "text")) ?> %

View File

@ -1,5 +1,5 @@
<input type="radio"
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>
value="<?php echo $view->escape($value) ?>"
<?php if ($checked): ?> checked="checked"<?php endif ?>
/>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_rows') ?>
<?php echo $view['form']->block('form_rows') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "search")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "search")) ?>

View File

@ -1 +1 @@
<textarea <?php echo $view['form']->renderBlock('widget_attributes') ?>><?php echo $view->escape($value) ?></textarea>
<textarea <?php echo $view['form']->block('widget_attributes') ?>><?php echo $view->escape($value) ?></textarea>

View File

@ -1,7 +1,7 @@
<?php if ($widget == 'single_text'): ?>
<?php echo $view['form']->renderBlock('form_widget_simple'); ?>
<?php echo $view['form']->block('form_widget_simple'); ?>
<?php else: ?>
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php
// There should be no spaces between the colons and the widgets, that's why
// this block is written in a single PHP tag

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "url")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "url")) ?>

View File

@ -1,7 +1,7 @@
<table <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<table <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php if (!$form->hasParent()): ?>
<?php echo $view['form']->errors($form) ?>
<?php endif ?>
<?php echo $view['form']->renderBlock('form_rows') ?>
<?php echo $view['form']->block('form_rows') ?>
<?php echo $view['form']->rest($form) ?>
</table>

View File

@ -12,6 +12,7 @@
namespace Symfony\Bundle\FrameworkBundle\Templating\Helper;
use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Form\FormRendererInterface;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Form\Exception\FormException;
@ -28,77 +29,34 @@ use Symfony\Component\Form\Util\FormUtil;
class FormHelper extends Helper
{
/**
* @var EngineInterface
* @var FormRendererInterface
*/
private $engine;
private $renderer;
/**
* @var CsrfProviderInterface
* @param FormRendererInterface $renderer
*/
private $csrfProvider;
/**
* @var array
*/
private $blockHierarchyMap = array();
/**
* @var array
*/
private $currentHierarchyLevelMap = array();
/**
* @var array
*/
private $variableMap = array();
/**
* @var array
*/
private $stack = array();
/**
* @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 $defaultThemes An array of theme names
*/
public function __construct(EngineInterface $engine, CsrfProviderInterface $csrfProvider = null, array $defaultThemes = array())
public function __construct(FormRendererInterface $renderer)
{
$this->engine = $engine;
$this->csrfProvider = $csrfProvider;
$this->defaultThemes = $defaultThemes;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'form';
}
public function isChoiceGroup($label)
{
return FormUtil::isChoiceGroup($label);
return $this->renderer->isChoiceGroup($label);
}
public function isChoiceSelected(FormViewInterface $view, ChoiceView $choice)
{
return FormUtil::isChoiceSelected($choice->getValue(), $view->getVar('value'));
return $this->renderer->isChoiceSelected($view, $choice);
}
/**
@ -111,9 +69,7 @@ class FormHelper extends Helper
*/
public function setTheme(FormViewInterface $view, $themes)
{
$this->themes[$view->getVar('full_block_name')] = (array) $themes;
$this->templateCache = array();
$this->templateHierarchyLevelCache = array();
$this->renderer->setTheme($view, $themes);
}
/**
@ -129,7 +85,7 @@ class FormHelper extends Helper
*/
public function enctype(FormViewInterface $view)
{
return $this->renderSection($view, 'enctype');
return $this->renderer->renderEnctype($view);
}
/**
@ -146,44 +102,40 @@ class FormHelper extends Helper
* <?php echo view['form']->widget(array('separator' => '+++++)) ?>
*
* @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
*/
public function widget(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'widget', $variables);
return $this->renderer->renderWidget($view, $variables);
}
/**
* Renders the entire form field "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
*/
public function row(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'row', $variables);
return $this->renderer->renderRow($view, $variables);
}
/**
* Renders the label of the given view.
*
* @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
* @param string $label The label
* @param array $variables Additional variables passed to the template
*
* @return string The HTML markup
*/
public function label(FormViewInterface $view, $label = null, array $variables = array())
{
if ($label !== null) {
$variables += array('label' => $label);
}
return $this->renderSection($view, 'label', $variables);
return $this->renderer->renderLabel($view, $label, $variables);
}
/**
@ -195,20 +147,49 @@ class FormHelper extends Helper
*/
public function errors(FormViewInterface $view)
{
return $this->renderSection($view, 'errors');
return $this->renderer->renderErrors($view);
}
/**
* Renders views which have not already been rendered.
*
* @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
*/
public function rest(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'rest', $variables);
return $this->renderer->renderRest($view, $variables);
}
/**
* Alias of {@link block()}
*
* @param string $block The name of the block to render.
* @param array $variables The variable to pass to the template.
*
* @return string The HTML markup
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3. Use
* {@link block()} instead.
*/
public function renderBlock($block, array $variables = array())
{
return $this->block($block, $variables);
}
/**
* Renders a block of the template.
*
* @param string $block The name of the block to render.
* @param array $variables The variable to pass to the template.
*
* @return string The HTML markup
*/
public function block($block, array $variables = array())
{
return $this->renderer->renderBlock($block, $variables);
}
/**
@ -238,304 +219,11 @@ class FormHelper extends Helper
*/
public function csrfToken($intention)
{
if (!$this->csrfProvider instanceof CsrfProviderInterface) {
throw new \BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.');
}
return $this->csrfProvider->generateCsrfToken($intention);
}
/**
* Renders a template.
*
* 1. This function first looks for a block named "_<view id>_<section>",
* 2. if such a block is not found the function will look for a block named
* "<type name>_<section>",
* 3. the type name is recursively replaced by the parent type name until a
* corresponding block is found
*
* @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
*
* @throws FormException if no template block exists to render the given section of the view
*/
protected function renderSection(FormViewInterface $view, $section, array $variables = array())
{
$renderOnlyOnce = in_array($section, array('row', 'widget'));
if ($renderOnlyOnce && $view->isRendered()) {
return '';
}
// The cache key for storing the variables and types
$mapKey = $view->getVar('full_block_name') . '_' . $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;
// The default variable scope contains all view variables, merged with
// the variables passed explicitely to the helper
$variables = array_replace_recursive($view->getVars(), $variables);
} 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);
}
$cacheKey = $view->getVar('full_block_name');
$block = $blockHierarchy[$currentHierarchyLevel];
// Populate the cache if the template for the block is not known yet
if (!isset($this->templateCache[$cacheKey][$block])) {
$this->loadTemplateForBlockHierarchy($view, $blockHierarchy, $currentHierarchyLevel);
}
// 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))
));
}
// 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;
}
// 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;
// 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);
// Do the rendering
$html = $this->engine->render($this->templateCache[$cacheKey][$block], $variables);
// 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();
}
return trim($html);
}
public function renderBlock($block, $variables = array())
{
if (0 == count($this->stack)) {
throw new FormException('This method should only be called while rendering a form element.');
}
list($view, $scopeVariables) = end($this->stack);
$cacheKey = $view->getVar('full_block_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($scopeVariables, $variables);
return trim($this->engine->render($this->templateCache[$cacheKey][$block], $variables));
return $this->renderer->renderCsrfToken($intention);
}
public function humanize($text)
{
return ucfirst(trim(strtolower(preg_replace('/[_\s]+/', ' ', $text))));
}
public function getName()
{
return 'form';
}
/**
* Loads the cache with the template for a specific level of a block hierarchy.
*
* For example, the block hierarchy could be:
*
* <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.
*/
private function loadTemplateForBlockHierarchy(FormViewInterface $view, array $blockHierarchy, $currentLevel)
{
$cacheKey = $view->getVar('full_block_name');
$block = $blockHierarchy[$currentLevel];
// 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;
return true;
}
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;
return $this->renderer->humanize($text);
}
}

View File

@ -15,9 +15,11 @@ use Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper;
use Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper;
use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTemplateNameParser;
use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTranslator;
use Symfony\Component\Form\FormView;
use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\Loader\FilesystemLoader;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\Extension\Templating\TemplatingRendererEngine;
use Symfony\Component\Form\Tests\AbstractDivLayoutTest;
class FormHelperDivLayoutTest extends AbstractDivLayoutTest
@ -34,8 +36,10 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest
$loader = new FilesystemLoader(array());
$engine = new PhpEngine($templateNameParser, $loader);
$engine->addGlobal('global', '');
$rendererEngine = new TemplatingRendererEngine($engine, array('FrameworkBundle:Form'));
$renderer = new FormRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
$this->helper = new FormHelper($engine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array('FrameworkBundle:Form'));
$this->helper = new FormHelper($renderer);
$engine->setHelpers(array(
$this->helper,

View File

@ -16,9 +16,11 @@ use Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper;
use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTemplateNameParser;
use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTranslator;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\Extension\Templating\TemplatingRendererEngine;
use Symfony\Component\Form\Tests\AbstractTableLayoutTest;
use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\Loader\FilesystemLoader;
use Symfony\Component\Form\Tests\AbstractTableLayoutTest;
class FormHelperTableLayoutTest extends AbstractTableLayoutTest
{
@ -34,11 +36,13 @@ class FormHelperTableLayoutTest extends AbstractTableLayoutTest
$loader = new FilesystemLoader(array());
$engine = new PhpEngine($templateNameParser, $loader);
$engine->addGlobal('global', '');
$this->helper = new FormHelper($engine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
$rendererEngine = new TemplatingRendererEngine($engine, array(
'FrameworkBundle:Form',
'FrameworkBundle:FormTable'
));
$renderer = new FormRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
$this->helper = new FormHelper($renderer);
$engine->setHelpers(array(
$this->helper,

View File

@ -16,6 +16,8 @@
<parameter key="twig.extension.routing.class">Symfony\Bridge\Twig\Extension\RoutingExtension</parameter>
<parameter key="twig.extension.yaml.class">Symfony\Bridge\Twig\Extension\YamlExtension</parameter>
<parameter key="twig.extension.form.class">Symfony\Bridge\Twig\Extension\FormExtension</parameter>
<parameter key="twig.form.engine.class">Symfony\Bridge\Twig\Form\TwigRendererEngine</parameter>
<parameter key="twig.form.renderer.class">Symfony\Bridge\Twig\Form\TwigRenderer</parameter>
<parameter key="twig.translation.extractor.class">Symfony\Bridge\Twig\Translation\TwigExtractor</parameter>
<parameter key="twig.exception_listener.class">Symfony\Component\HttpKernel\EventListener\ExceptionListener</parameter>
</parameters>
@ -75,10 +77,18 @@
<service id="twig.extension.form" class="%twig.extension.form.class%" public="false">
<tag name="twig.extension" />
<argument type="service" id="form.csrf_provider" on-invalid="null" />
<argument type="service" id="twig.form.renderer" />
</service>
<service id="twig.form.engine" class="%twig.form.engine.class%" public="false">
<argument>%twig.form.resources%</argument>
</service>
<service id="twig.form.renderer" class="%twig.form.renderer.class%" public="false">
<argument type="service" id="twig.form.engine" />
<argument type="service" id="form.csrf_provider" on-invalid="null" />
</service>
<service id="twig.translation.extractor" class="%twig.translation.extractor.class%">
<argument type="service" id="twig" />
<tag name="translation.extractor" alias="twig" />

View File

@ -0,0 +1,202 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
/**
* Default implementation of {@link FormRendererEngineInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class AbstractRendererEngine implements FormRendererEngineInterface
{
/**
* The variable in {@link FormViewInterface} used as cache key.
*/
const CACHE_KEY_VAR = 'full_block_name';
/**
* @var array
*/
protected $defaultThemes;
/**
* @var array
*/
protected $themes = array();
/**
* @var array
*/
protected $resources = array();
/**
* @var array
*/
private $resourceHierarchyLevels = array();
/**
* Creates a new renderer engine.
*
* @param array $defaultThemes The default themes. The type of these
* themes is open to the implementation.
*/
public function __construct(array $defaultThemes = array())
{
$this->defaultThemes = $defaultThemes;
}
/**
* {@inheritdoc}
*/
public function setTheme(FormViewInterface $view, $themes)
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
// Do not cast, as casting turns objects into arrays of properties
$this->themes[$cacheKey] = is_array($themes) ? $themes : array($themes);
$this->resources[$cacheKey] = array();
$this->resourceHierarchyLevels[$cacheKey] = array();
}
/**
* {@inheritdoc}
*/
public function getResourceForBlock(FormViewInterface $view, $block)
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
if (!isset($this->resources[$cacheKey][$block])) {
$this->loadResourceForBlock($cacheKey, $view, $block);
}
return $this->resources[$cacheKey][$block];
}
/**
* {@inheritdoc}
*/
public function getResourceForBlockHierarchy(FormViewInterface $view, array $blockHierarchy, $hierarchyLevel)
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
$block = $blockHierarchy[$hierarchyLevel];
if (!isset($this->resources[$cacheKey][$block])) {
$this->loadResourceForBlockHierarchy($cacheKey, $view, $blockHierarchy, $hierarchyLevel);
}
return $this->resources[$cacheKey][$block];
}
/**
* {@inheritdoc}
*/
public function getResourceHierarchyLevel(FormViewInterface $view, array $blockHierarchy, $hierarchyLevel)
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
$block = $blockHierarchy[$hierarchyLevel];
if (!isset($this->resources[$cacheKey][$block])) {
$this->loadResourceForBlockHierarchy($cacheKey, $view, $blockHierarchy, $hierarchyLevel);
}
// If $block was previously rendered loaded with loadTemplateForBlock(), the template
// is cached but the hierarchy level is not. In this case, we know that the block
// exists at this very hierarchy level, so we can just set it.
if (!isset($this->resourceHierarchyLevels[$cacheKey][$block])) {
$this->resourceHierarchyLevels[$cacheKey][$block] = $hierarchyLevel;
}
return $this->resourceHierarchyLevels[$cacheKey][$block];
}
/**
* Loads the cache with the resource for a given block name.
*
* @see getResourceForBlock()
*
* @param string $cacheKey The cache key of the form view.
* @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 resource could be loaded, false otherwise.
*/
abstract protected function loadResourceForBlock($cacheKey, FormViewInterface $view, $block);
/**
* Loads the cache with the resource for a specific level of a block hierarchy.
*
* @see getResourceForBlockHierarchy()
*
* @param string $cacheKey The cache key used for storing the
* resource.
* @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 $hierarchyLevel The level in the block hierarchy that
* should be loaded.
*
* @return Boolean True if the resource could be loaded, false otherwise.
*/
private function loadResourceForBlockHierarchy($cacheKey, FormViewInterface $view, array $blockHierarchy, $hierarchyLevel)
{
$block = $blockHierarchy[$hierarchyLevel];
// Try to find a template for that block
if ($this->loadResourceForBlock($cacheKey, $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->resourceHierarchyLevels[$cacheKey][$block] = $hierarchyLevel;
return true;
}
if ($hierarchyLevel > 0) {
$parentLevel = $hierarchyLevel - 1;
$parentBlock = $blockHierarchy[$parentLevel];
// The next two if statements contain slightly duplicated code. This is by intention
// and tries to avoid execution of unnecessary checks in order to increase performance.
if (isset($this->resources[$cacheKey][$parentBlock])) {
// It may happen that the parent block is already loaded, but its level is not.
// In this case, the parent block must have been loaded by loadResourceForBlock(),
// which does not check the hierarchy of the block. Subsequently the block must have
// been found directly on the parent level.
if (!isset($this->resourceHierarchyLevels[$cacheKey][$parentBlock])) {
$this->resourceHierarchyLevels[$cacheKey][$parentBlock] = $parentLevel;
}
// Cache the shortcuts for further accesses
$this->resources[$cacheKey][$block] = $this->resources[$cacheKey][$parentBlock];
$this->resourceHierarchyLevels[$cacheKey][$block] = $this->resourceHierarchyLevels[$cacheKey][$parentBlock];
return true;
}
if ($this->loadResourceForBlockHierarchy($cacheKey, $view, $blockHierarchy, $parentLevel)) {
// Cache the shortcuts for further accesses
$this->resources[$cacheKey][$block] = $this->resources[$cacheKey][$parentBlock];
$this->resourceHierarchyLevels[$cacheKey][$block] = $this->resourceHierarchyLevels[$cacheKey][$parentBlock];
return true;
}
}
// Cache the result for further accesses
$this->resources[$cacheKey][$block] = false;
$this->resourceHierarchyLevels[$cacheKey][$block] = false;
return false;
}
}

View File

@ -31,7 +31,6 @@ CHANGELOG
* ArrayToChoicesTransformer to ChoicesToValuesTransformer
* ScalarToChoiceTransformer to ChoiceToValueTransformer
to be consistent with the naming in ChoiceListInterface.
* [BC BREAK] removed FormUtil::toArrayKey() and FormUtil::toArrayKeys().
They were merged into ChoiceList and have no public equivalent anymore.
* choice fields now throw a FormException if neither the "choices" nor the
"choice_list" option is set
@ -172,4 +171,11 @@ CHANGELOG
* ChoiceType now caches its created choice lists to improve performance
* [BC BREAK] Rows of a collection field cannot be themed individually anymore. All rows in the collection
field now have the same block names, which contains "entry" where it previously contained the row index.
* [BC BREAK] When registering a type through the DI extension, the tag alias has to match the actual type name.
* [BC BREAK] When registering a type through the DI extension, the tag alias has to match the actual type name.
* added FormRendererInterface, FormRendererEngineInterface and implementations of these interfaces
* [BC BREAK] removed the following methods from FormUtil:
* `toArrayKey`
* `toArrayKeys`
* `isChoiceGroup`
* `isChoiceSelected`
* added method `block` to FormHelper and deprecated `renderBlock` instead

View File

@ -0,0 +1,125 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Templating;
use Symfony\Component\Form\AbstractRendererEngine;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Templating\EngineInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TemplatingRendererEngine extends AbstractRendererEngine
{
/**
* @var EngineInterface
*/
private $engine;
public function __construct(EngineInterface $engine, array $defaultThemes = array())
{
parent::__construct($defaultThemes);
$this->engine = $engine;
}
/**
* {@inheritdoc}
*/
public function renderBlock(FormViewInterface $view, $resource, $block, array $variables = array())
{
return trim($this->engine->render($resource, $variables));
}
/**
* Loads the cache with the resource for a given block name.
*
* This implementation tries to load as few blocks as possible, since each block
* is represented by a template on the file system.
*
* @see getResourceForBlock()
*
* @param string $cacheKey The cache key of the form view.
* @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 resource could be loaded, false otherwise.
*/
protected function loadResourceForBlock($cacheKey, 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.
// Check each theme whether it contains the searched block
if (isset($this->themes[$cacheKey])) {
for ($i = count($this->themes[$cacheKey]) - 1; $i >= 0; --$i) {
if ($this->loadResourceFromTheme($cacheKey, $block, $this->themes[$cacheKey][$i])) {
return true;
}
}
}
// Check the default themes once we reach the root form without success
if (!$view->hasParent()) {
for ($i = count($this->defaultThemes) - 1; $i >= 0; --$i) {
if ($this->loadResourceFromTheme($cacheKey, $block, $this->defaultThemes[$i])) {
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(self::CACHE_KEY_VAR);
if (!isset($this->resources[$parentCacheKey][$block])) {
$this->loadResourceForBlock($parentCacheKey, $view->getParent(), $block);
}
// If a template exists in the parent themes, cache that template
// for the current theme as well to speed up further accesses
if ($this->resources[$parentCacheKey][$block]) {
$this->resources[$cacheKey][$block] = $this->resources[$parentCacheKey][$block];
return true;
}
}
// Cache that we didn't find anything to speed up further accesses
$this->resources[$cacheKey][$block] = false;
return false;
}
/**
* Tries to load the resource for a block from a theme.
*
* @param string $cacheKey The cache key for storing the resource.
* @param string $block The name of the block to load a resource for.
* @param mixed $theme The theme to load the block from.
*
* @return Boolean True if the resource could be loaded, false otherwise.
*/
protected function loadResourceFromTheme($cacheKey, $block, $theme)
{
if ($this->engine->exists($templateName = $theme . ':' . $block . '.html.php')) {
$this->resources[$cacheKey][$block] = $templateName;
return true;
}
return false;
}
}

View File

@ -0,0 +1,324 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
/**
* Renders a form into HTML using a rendering engine.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormRenderer implements FormRendererInterface
{
/**
* @var FormRendererEngineInterface
*/
private $engine;
/**
* @var CsrfProviderInterface
*/
private $csrfProvider;
/**
* @var array
*/
private $blockHierarchyMap = array();
/**
* @var array
*/
private $hierarchyLevelMap = array();
/**
* @var array
*/
private $variableMap = array();
/**
* @var array
*/
private $stack = array();
public function __construct(FormRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null)
{
$this->engine = $engine;
$this->csrfProvider = $csrfProvider;
}
/**
* {@inheritdoc}
*/
public function getEngine()
{
return $this->engine;
}
/**
* {@inheritdoc}
*/
public function setTheme(FormViewInterface $view, $themes)
{
$this->engine->setTheme($view, $themes);
}
/**
* {@inheritdoc}
*/
public function renderEnctype(FormViewInterface $view)
{
return $this->renderSection($view, 'enctype');
}
/**
* {@inheritdoc}
*/
public function renderRow(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'row', $variables);
}
/**
* {@inheritdoc}
*/
public function renderRest(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'rest', $variables);
}
/**
* {@inheritdoc}
*/
public function renderWidget(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'widget', $variables);
}
/**
* {@inheritdoc}
*/
public function renderErrors(FormViewInterface $view)
{
return $this->renderSection($view, 'errors');
}
/**
* {@inheritdoc}
*/
public function renderLabel(FormViewInterface $view, $label = null, array $variables = array())
{
if ($label !== null) {
$variables += array('label' => $label);
}
return $this->renderSection($view, 'label', $variables);
}
/**
* {@inheritdoc}
*/
public function renderCsrfToken($intention)
{
if (null === $this->csrfProvider) {
throw new \BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.');
}
return $this->csrfProvider->generateCsrfToken($intention);
}
/**
* {@inheritdoc}
*/
public function renderBlock($block, array $variables = array())
{
if (0 == count($this->stack)) {
throw new FormException('This method should only be called while rendering a form element.');
}
list($view, $scopeVariables) = end($this->stack);
$resource = $this->engine->getResourceForBlock($view, $block);
if (!$resource) {
throw new FormException(sprintf('No block "%s" found while rendering the form.', $block));
}
$variables = array_replace_recursive($scopeVariables, $variables);
return $this->engine->renderBlock($view, $resource, $block, $variables);
}
/**
* {@inheritdoc}
*/
public function isChoiceGroup($choice)
{
return is_array($choice) || $choice instanceof \Traversable;
}
/**
* {@inheritdoc}
*/
public function isChoiceSelected(FormViewInterface $view, ChoiceView $choice)
{
$value = $view->getVar('value');
$choiceValue = $choice->getValue();
if (is_array($value)) {
return false !== array_search($choiceValue, $value, true);
}
return $choiceValue === $value;
}
/**
* {@inheritdoc}
*/
public function humanize($text)
{
return ucfirst(trim(strtolower(preg_replace('/[_\s]+/', ' ', $text))));
}
/**
* Renders the given section of a form view.
*
* @param FormViewInterface $view The form view.
* @param string $section The name of the section to render.
* @param array $variables The variables to pass to the template.
*
* @return string The HTML markup.
*
* @throws Exception\FormException If no fitting template was found.
*/
protected function renderSection(FormViewInterface $view, $section, array $variables = array())
{
$renderOnlyOnce = in_array($section, array('row', 'widget'));
if ($renderOnlyOnce && $view->isRendered()) {
return '';
}
// The cache key for storing the variables and types
$mapKey = $uniqueBlockName = $view->getVar('full_block_name') . '_' . $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();
foreach ($view->getVar('types') as $type) {
$blockHierarchy[] = $type . '_' . $section;
}
$blockHierarchy[] = $uniqueBlockName;
$hierarchyLevel = count($blockHierarchy) - 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);
} else {
// RECURSIVE CALL
// If a block recursively calls renderSection() again, resume rendering
// using the parent type in the hierarchy.
$blockHierarchy = $this->blockHierarchyMap[$mapKey];
$hierarchyLevel = $this->hierarchyLevelMap[$mapKey] - 1;
// Reuse the current scope and merge it with the explicitely passed variables
$variables = array_replace_recursive($this->variableMap[$mapKey], $variables);
}
// Load the resource where this block can be found
$resource = $this->engine->getResourceForBlockHierarchy($view, $blockHierarchy, $hierarchyLevel);
// Update the current hierarchy level to the one at which the resource was
// found. For example, if looking for "choice_widget", but only a resource
// is found for its parent "form_widget", then the level is updated here
// to the parent level.
$hierarchyLevel = $this->engine->getResourceHierarchyLevel($view, $blockHierarchy, $hierarchyLevel);
// The actually existing block name in $resource
$block = $blockHierarchy[$hierarchyLevel];
// Escape if no resource exists for this block
if (!$resource) {
throw new FormException(sprintf(
'Unable to render the form as none of the following blocks exist: "%s".',
implode('", "', array_reverse($blockHierarchy))
));
}
// 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->hierarchyLevelMap[$mapKey] = $hierarchyLevel;
$this->variableMap[$mapKey] = $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);
// Do the rendering
$html = $this->engine->renderBlock($view, $resource, $block, $variables);
// Clear the stack
array_pop($this->stack);
// Clear the maps
unset($this->blockHierarchyMap[$mapKey]);
unset($this->hierarchyLevelMap[$mapKey]);
unset($this->variableMap[$mapKey]);
if ($renderOnlyOnce) {
$view->setRendered();
}
return $html;
}
}

View File

@ -0,0 +1,146 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
/**
* Adapter for rendering form templates with a specific templating engine.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface FormRendererEngineInterface
{
/**
* Sets the theme(s) to be used for rendering a view and its children.
*
* @param FormViewInterface $view The view to assign the theme(s) to.
* @param mixed $themes The theme(s). The type of these themes
* is open to the implementation.
*/
public function setTheme(FormViewInterface $view, $themes);
/**
* Returns the resource for a block name.
*
* The resource is first searched in the themes attached to $view, then
* in the themes of its parent view and so on, until a resource was found.
*
* The type of the resource is decided by the implementation. The resource
* is later passed to {@link renderBlock()} by the rendering algorithm.
*
* @param FormViewInterface $view The view for determining the used themes.
* First the themes attached directly to the
* view with {@link setTheme()} are considered,
* then the ones of its parent etc.
* @param string $block The name of the block to render.
*
* @return mixed The renderer resource or false, if none was found.
*/
public function getResourceForBlock(FormViewInterface $view, $block);
/**
* Returns the resource for a block hierarchy.
*
* A block hierarchy is an array which starts with the root of the hierarchy
* and continues with the child of that root, the child of that child etc.
* The following is an example for a block hierarchy:
*
* <code>
* form_widget
* text_widget
* url_widget
* </code>
*
* In this example, "url_widget" is the most specific block, while the other
* blocks are its ancestors in the hierarchy.
*
* The second parameter $hierarchyLevel determines the level of the hierarchy
* that should be rendered. For example, if $hierarchyLevel is 2 for the
* above hierarchy, the engine will first look for the block "url_widget",
* then, if that does not exist, for the block "text_widget" etc.
*
* The type of the resource is decided by the implementation. The resource
* is later passed to {@link renderBlock()} by the rendering algorithm.
*
* @param FormViewInterface $view The view for determining the used
* themes. First the themes attached
* directly to the view with
* {@link setTheme()} are considered,
* then the ones of its parent etc.
* @param array $blockHierarchy The block name hierarchy, with
* the root block at the beginning.
* @param integer $hierarchyLevel The level in the hierarchy at
* which to start looking. Level 0
* indicates the root block, i.e.
* the first element of $blockHierarchy.
*
* @return mixed The renderer resource or false, if none was found.
*/
public function getResourceForBlockHierarchy(FormViewInterface $view, array $blockHierarchy, $hierarchyLevel);
/**
* Returns the hierarchy level at which a resource can be found.
*
* A block hierarchy is an array which starts with the root of the hierarchy
* and continues with the child of that root, the child of that child etc.
* The following is an example for a block hierarchy:
*
* <code>
* form_widget
* text_widget
* url_widget
* </code>
*
* The second parameter $hierarchyLevel determines the level of the hierarchy
* that should be rendered.
*
* If we call this method with the hierarchy level 2, the engine will first
* look for a resource for block "url_widget". If such a resource exists,
* the method returns 2. Otherwise it tries to find a resource for block
* "text_widget" (at level 1) and, again, returns 1 if a resource was found.
* The method continues to look for resources until the root level was
* reached and nothing was found. In this case false is returned.
*
* The type of the resource is decided by the implementation. The resource
* is later passed to {@link renderBlock()} by the rendering algorithm.
*
* @param FormViewInterface $view The view for determining the used
* themes. First the themes attached
* directly to the view with
* {@link setTheme()} are considered,
* then the ones of its parent etc.
* @param array $blockHierarchy The block name hierarchy, with
* the root block at the beginning.
* @param integer $hierarchyLevel The level in the hierarchy at
* which to start looking. Level 0
* indicates the root block, i.e.
* the first element of $blockHierarchy.
*
* @return integer|Boolean The hierarchy level or false, if no resource was found.
*/
public function getResourceHierarchyLevel(FormViewInterface $view, array $blockHierarchy, $hierarchyLevel);
/**
* Renders a block in the given renderer resource.
*
* The resource can be obtained by calling {@link getResourceForBlock()}
* or {@link getResourceForBlockHierarchy()}. The type of the resource is
* decided by the implementation.
*
* @param FormViewInterface $view The view to render.
* @param mixed $resource The renderer resource.
* @param string $block The name of the block to render.
* @param array $variables The variables to pass to the template.
*
* @return string The HTML markup.
*/
public function renderBlock(FormViewInterface $view, $resource, $block, array $variables = array());
}

View File

@ -0,0 +1,180 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
/**
* Renders a form into HTML.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface FormRendererInterface
{
/**
* Returns the engine used by this renderer.
*
* @return FormRendererEngineInterface The renderer engine.
*/
public function getEngine();
/**
* Sets the theme(s) to be used for rendering a view and its children.
*
* @param FormViewInterface $view The view to assign the theme(s) to.
* @param mixed $themes The theme(s). The type of these themes
* is open to the implementation.
*/
public function setTheme(FormViewInterface $view, $themes);
/**
* Renders the HTML enctype in the form tag, if necessary.
*
* Example usage templates:
*
* <form action="..." method="post" <?php echo $renderer->renderEnctype($form) ?>>
*
* @param FormViewInterface $view The view for which to render the encoding type
*
* @return string The HTML markup
*/
public function renderEnctype(FormViewInterface $view);
/**
* Renders the entire row for a form field.
*
* A row typically contains the label, errors and widget of a field.
*
* @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
*/
public function renderRow(FormViewInterface $view, array $variables = array());
/**
* Renders views which have not already been rendered.
*
* @param FormViewInterface $view The parent view
* @param array $variables An array of variables
*
* @return string The HTML markup
*/
public function renderRest(FormViewInterface $view, array $variables = array());
/**
* Renders the HTML for a given view.
*
* Example usage:
*
* <?php echo $renderer->renderWidget($form) ?>
*
* You can pass options during the call:
*
* <?php echo $renderer->renderWidget($form, array('attr' => array('class' => 'foo'))) ?>
*
* <?php echo $renderer->renderWidget($form, array('separator' => '+++++)) ?>
*
* @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
*/
public function renderWidget(FormViewInterface $view, array $variables = array());
/**
* Renders the errors of the given view.
*
* @param FormViewInterface $view The view to render the errors for
*
* @return string The HTML markup
*/
public function renderErrors(FormViewInterface $view);
/**
* Renders the label of the given view.
*
* @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
*/
public function renderLabel(FormViewInterface $view, $label = null, array $variables = array());
/**
* Renders a named block of the form theme.
*
* @param string $block The name of the block.
* @param array $variables The variables to pass to the template.
*
* @return string The HTML markup
*/
public function renderBlock($block, array $variables = array());
/**
* Renders a CSRF token.
*
* Use this helper for CSRF protection without the overhead of creating a
* form.
*
* <code>
* <input type="hidden" name="token" value="<?php $renderer->renderCsrfToken('rm_user_'.$user->getId()) ?>">
* </code>
*
* Check the token in your action using the same intention.
*
* <code>
* $csrfProvider = $this->get('form.csrf_provider');
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
* throw new \RuntimeException('CSRF attack detected.');
* }
* </code>
*
* @param string $intention The intention of the protected action
*
* @return string A CSRF token
*/
public function renderCsrfToken($intention);
/**
* Returns whether the given choice is a group.
*
* @param mixed $choice A choice
*
* @return Boolean Whether the choice is a group
*/
public function isChoiceGroup($choice);
/**
* Returns whether the given choice is selected.
*
* @param FormViewInterface $view The view of the choice field
* @param ChoiceView $choice The choice to check
*
* @return Boolean Whether the choice is selected
*/
public function isChoiceSelected(FormViewInterface $view, ChoiceView $choice);
/**
* Makes a technical name human readable.
*
* Sequences of underscores are replaced by single spaces. The first letter
* of the resulting string is capitalized, while all other letters are
* turned to lowercase.
*
* @param string $text The text to humanize.
*
* @return string The humanized text.
*/
public function humanize($text);
}

View File

@ -545,7 +545,7 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
]
/following-sibling::div
[
./label
./label[.="child"]
/following-sibling::div
[
./div

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
class FormRendererTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $engine;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $csrfProvider;
/**
* @var FormRenderer
*/
private $renderer;
protected function setUp()
{
$this->engine = $this->getMock('Symfony\Component\Form\FormRendererEngineInterface');
$this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
$this->renderer = new FormRenderer($this->engine, $this->csrfProvider);
}
public function isChoiceGroupProvider()
{
return array(
array(false, 0),
array(false, '0'),
array(false, '1'),
array(false, 1),
array(false, ''),
array(false, null),
array(false, true),
array(true, array()),
);
}
/**
* @dataProvider isChoiceGroupProvider
*/
public function testIsChoiceGroup($expected, $value)
{
$this->assertSame($expected, $this->renderer->isChoiceGroup($value));
}
public function testIsChoiceGroupPart2()
{
$this->assertTrue($this->renderer->isChoiceGroup(new \SplFixedArray(1)));
}
public function isChoiceSelectedProvider()
{
// The commented cases should not be necessary anymore, because the
// choice lists should assure that both values passed here are always
// strings
return array(
// array(true, 0, 0),
array(true, '0', '0'),
array(true, '1', '1'),
// array(true, false, 0),
// array(true, true, 1),
array(true, '', ''),
// array(true, null, ''),
array(true, '1.23', '1.23'),
array(true, 'foo', 'foo'),
array(true, 'foo10', 'foo10'),
array(true, 'foo', array(1, 'foo', 'foo10')),
array(false, 10, array(1, 'foo', 'foo10')),
array(false, 0, array(1, 'foo', 'foo10')),
);
}
/**
* @dataProvider isChoiceSelectedProvider
*/
public function testIsChoiceSelected($expected, $choice, $value)
{
$view = new FormView('name');
$view->setVar('value', $value);
$choice = new ChoiceView($choice, $choice . ' label');
$this->assertSame($expected, $this->renderer->isChoiceSelected($view, $choice));
}
}

View File

@ -15,65 +15,6 @@ use Symfony\Component\Form\Util\FormUtil;
class FormUtilTest extends \PHPUnit_Framework_TestCase
{
public function isChoiceGroupProvider()
{
return array(
array(false, 0),
array(false, '0'),
array(false, '1'),
array(false, 1),
array(false, ''),
array(false, null),
array(false, true),
array(true, array()),
);
}
/**
* @dataProvider isChoiceGroupProvider
*/
public function testIsChoiceGroup($expected, $value)
{
$this->assertSame($expected, FormUtil::isChoiceGroup($value));
}
public function testIsChoiceGroupPart2()
{
$this->assertTrue(FormUtil::isChoiceGroup(new \SplFixedArray(1)));
}
public function isChoiceSelectedProvider()
{
// The commented cases should not be necessary anymore, because the
// choice lists should assure that both values passed here are always
// strings
return array(
// array(true, 0, 0),
array(true, '0', '0'),
array(true, '1', '1'),
// array(true, false, 0),
// array(true, true, 1),
array(true, '', ''),
// array(true, null, ''),
array(true, '1.23', '1.23'),
array(true, 'foo', 'foo'),
array(true, 'foo10', 'foo10'),
array(true, 'foo', array(1, 'foo', 'foo10')),
array(false, 10, array(1, 'foo', 'foo10')),
array(false, 0, array(1, 'foo', 'foo10')),
);
}
/**
* @dataProvider isChoiceSelectedProvider
*/
public function testIsChoiceSelected($expected, $choice, $value)
{
$this->assertSame($expected, FormUtil::isChoiceSelected($choice, $value));
}
public function singularifyProvider()
{
// see http://english-zone.com/spelling/plurals.html

View File

@ -191,35 +191,6 @@ abstract class FormUtil
return $plural;
}
/**
* Returns whether the given choice is a group.
*
* @param mixed $choice A choice
*
* @return Boolean Whether the choice is a group
*/
public static function isChoiceGroup($choice)
{
return is_array($choice) || $choice instanceof \Traversable;
}
/**
* Returns whether the given choice is selected.
*
* @param mixed $choice The choice
* @param mixed $value the value
*
* @return Boolean Whether the choice is selected
*/
public static function isChoiceSelected($choice, $value)
{
if (is_array($value)) {
return false !== array_search($choice, $value, true);
}
return $choice === $value;
}
/**
* Returns whether the given data is empty.
*