This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.
symfony/src/Symfony/Component/Form/FormRenderer.php
Fabien Potencier 6cd873447d Merge branch '2.3' into 2.6
* 2.3:
  replaced the last remaining is_integer() call
  [2.3] [Config] [Console] [DependencyInjection] [DomCrawler] [Form] [HttpKernel] [PropertyAccess] [Security] [Translation] [Yaml] static code analysis, code cleanup
  [Validator] Added missing galician (gl) translations
  [travis] Tests Security sub-components
  [travis] Tests Security sub-components
  CS fixes
  [travis] test with php nightly

Conflicts:
	src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php
2015-03-07 08:40:15 +01:00

321 lines
12 KiB
PHP

<?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\Exception\LogicException;
use Symfony\Component\Form\Exception\BadMethodCallException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
/**
* Renders a form into HTML using a rendering engine.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormRenderer implements FormRendererInterface
{
const CACHE_KEY_VAR = 'unique_block_prefix';
/**
* @var FormRendererEngineInterface
*/
private $engine;
/**
* @var CsrfTokenManagerInterface
*/
private $csrfTokenManager;
/**
* @var array
*/
private $blockNameHierarchyMap = array();
/**
* @var array
*/
private $hierarchyLevelMap = array();
/**
* @var array
*/
private $variableStack = array();
/**
* Constructor.
*
* @param FormRendererEngineInterface $engine
* @param CsrfTokenManagerInterface|null $csrfTokenManager
*
* @throws UnexpectedTypeException
*/
public function __construct(FormRendererEngineInterface $engine, $csrfTokenManager = null)
{
if ($csrfTokenManager instanceof CsrfProviderInterface) {
$csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager);
} elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) {
throw new UnexpectedTypeException($csrfTokenManager, 'CsrfProviderInterface or CsrfTokenManagerInterface or null');
}
$this->engine = $engine;
$this->csrfTokenManager = $csrfTokenManager;
}
/**
* {@inheritdoc}
*/
public function getEngine()
{
return $this->engine;
}
/**
* {@inheritdoc}
*/
public function setTheme(FormView $view, $themes)
{
$this->engine->setTheme($view, $themes);
}
/**
* {@inheritdoc}
*/
public function renderCsrfToken($tokenId)
{
if (null === $this->csrfTokenManager) {
throw new BadMethodCallException('CSRF tokens can only be generated if a CsrfTokenManagerInterface is injected in FormRenderer::__construct().');
}
return $this->csrfTokenManager->getToken($tokenId)->getValue();
}
/**
* {@inheritdoc}
*/
public function renderBlock(FormView $view, $blockName, array $variables = array())
{
$resource = $this->engine->getResourceForBlockName($view, $blockName);
if (!$resource) {
throw new LogicException(sprintf('No block "%s" found while rendering the form.', $blockName));
}
$viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
// The variables are cached globally for a view (instead of for the
// current suffix)
if (!isset($this->variableStack[$viewCacheKey])) {
$this->variableStack[$viewCacheKey] = array();
// The default variable scope contains all view variables, merged with
// the variables passed explicitly to the helper
$scopeVariables = $view->vars;
$varInit = true;
} else {
// Reuse the current scope and merge it with the explicitly passed variables
$scopeVariables = end($this->variableStack[$viewCacheKey]);
$varInit = false;
}
// Merge the passed with the existing attributes
if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
$variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
}
// Merge the passed with the exist *label* attributes
if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
$variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
}
// Do not use array_replace_recursive(), otherwise array variables
// cannot be overwritten
$variables = array_replace($scopeVariables, $variables);
$this->variableStack[$viewCacheKey][] = $variables;
// Do the rendering
$html = $this->engine->renderBlock($view, $resource, $blockName, $variables);
// Clear the stack
array_pop($this->variableStack[$viewCacheKey]);
if ($varInit) {
unset($this->variableStack[$viewCacheKey]);
}
return $html;
}
/**
* {@inheritdoc}
*/
public function searchAndRenderBlock(FormView $view, $blockNameSuffix, array $variables = array())
{
$renderOnlyOnce = 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix;
if ($renderOnlyOnce && $view->isRendered()) {
return '';
}
// The cache key for storing the variables and types
$viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
$viewAndSuffixCacheKey = $viewCacheKey.$blockNameSuffix;
// 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 explicitly 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->blockNameHierarchyMap[$viewAndSuffixCacheKey])) {
// INITIAL CALL
// Calculate the hierarchy of template blocks and start on
// the bottom level of the hierarchy (= "_<id>_<section>" block)
$blockNameHierarchy = array();
foreach ($view->vars['block_prefixes'] as $blockNamePrefix) {
$blockNameHierarchy[] = $blockNamePrefix.'_'.$blockNameSuffix;
}
$hierarchyLevel = count($blockNameHierarchy) - 1;
$hierarchyInit = true;
} else {
// RECURSIVE CALL
// If a block recursively calls searchAndRenderBlock() again, resume rendering
// using the parent type in the hierarchy.
$blockNameHierarchy = $this->blockNameHierarchyMap[$viewAndSuffixCacheKey];
$hierarchyLevel = $this->hierarchyLevelMap[$viewAndSuffixCacheKey] - 1;
$hierarchyInit = false;
}
// The variables are cached globally for a view (instead of for the
// current suffix)
if (!isset($this->variableStack[$viewCacheKey])) {
$this->variableStack[$viewCacheKey] = array();
// The default variable scope contains all view variables, merged with
// the variables passed explicitly to the helper
$scopeVariables = $view->vars;
$varInit = true;
} else {
// Reuse the current scope and merge it with the explicitly passed variables
$scopeVariables = end($this->variableStack[$viewCacheKey]);
$varInit = false;
}
// Load the resource where this block can be found
$resource = $this->engine->getResourceForBlockNameHierarchy($view, $blockNameHierarchy, $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, $blockNameHierarchy, $hierarchyLevel);
// The actually existing block name in $resource
$blockName = $blockNameHierarchy[$hierarchyLevel];
// Escape if no resource exists for this block
if (!$resource) {
throw new LogicException(sprintf(
'Unable to render the form as none of the following blocks exist: "%s".',
implode('", "', array_reverse($blockNameHierarchy))
));
}
// Merge the passed with the existing attributes
if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
$variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
}
// Merge the passed with the exist *label* attributes
if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
$variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
}
// Do not use array_replace_recursive(), otherwise array variables
// cannot be overwritten
$variables = array_replace($scopeVariables, $variables);
// 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->blockNameHierarchyMap[$viewAndSuffixCacheKey] = $blockNameHierarchy;
$this->hierarchyLevelMap[$viewAndSuffixCacheKey] = $hierarchyLevel;
// We also need to store the variables for the view so that we can render other
// blocks for the same view using the same variables as in the outer block.
$this->variableStack[$viewCacheKey][] = $variables;
// Do the rendering
$html = $this->engine->renderBlock($view, $resource, $blockName, $variables);
// Clear the stack
array_pop($this->variableStack[$viewCacheKey]);
// Clear the caches if they were filled for the first time within
// this function call
if ($hierarchyInit) {
unset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey], $this->hierarchyLevelMap[$viewAndSuffixCacheKey]);
}
if ($varInit) {
unset($this->variableStack[$viewCacheKey]);
}
if ($renderOnlyOnce) {
$view->setRendered();
}
return $html;
}
/**
* {@inheritdoc}
*/
public function humanize($text)
{
return ucfirst(trim(strtolower(preg_replace(array('/([A-Z])/', '/[_\s]+/'), array('_$1', ' '), $text))));
}
}