2012-07-15 17:58:13 +01:00
< ? 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 ;
2013-04-15 21:11:02 +01:00
use Symfony\Component\Form\Exception\LogicException ;
use Symfony\Component\Form\Exception\BadMethodCallException ;
2013-10-04 14:25:38 +01:00
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface ;
2018-04-02 11:03:31 +01:00
use Twig\Environment ;
2012-07-15 17:58:13 +01:00
/**
* Renders a form into HTML using a rendering engine .
*
* @ author Bernhard Schussek < bschussek @ gmail . com >
*/
class FormRenderer implements FormRendererInterface
{
2012-07-25 12:48:48 +01:00
const CACHE_KEY_VAR = 'unique_block_prefix' ;
2012-07-25 12:20:23 +01:00
2012-07-15 17:58:13 +01:00
private $engine ;
2013-10-04 14:25:38 +01:00
private $csrfTokenManager ;
2012-07-21 18:44:09 +01:00
private $blockNameHierarchyMap = array ();
2012-07-15 17:58:13 +01:00
private $hierarchyLevelMap = array ();
2012-07-21 18:44:09 +01:00
private $variableStack = array ();
2012-07-15 17:58:13 +01:00
2015-11-26 18:00:33 +00:00
public function __construct ( FormRendererEngineInterface $engine , CsrfTokenManagerInterface $csrfTokenManager = null )
2012-07-15 17:58:13 +01:00
{
$this -> engine = $engine ;
2013-10-04 14:25:38 +01:00
$this -> csrfTokenManager = $csrfTokenManager ;
2012-07-15 17:58:13 +01:00
}
/**
* { @ inheritdoc }
*/
public function getEngine ()
{
return $this -> engine ;
}
/**
* { @ inheritdoc }
*/
2017-10-19 16:12:11 +01:00
public function setTheme ( FormView $view , $themes , $useDefaultThemes = true )
2012-07-15 17:58:13 +01:00
{
2017-10-19 16:12:11 +01:00
$this -> engine -> setTheme ( $view , $themes , $useDefaultThemes );
2012-07-15 17:58:13 +01:00
}
/**
* { @ inheritdoc }
*/
2013-09-27 08:37:01 +01:00
public function renderCsrfToken ( $tokenId )
2012-07-15 17:58:13 +01:00
{
2013-10-04 14:25:38 +01:00
if ( null === $this -> csrfTokenManager ) {
throw new BadMethodCallException ( 'CSRF tokens can only be generated if a CsrfTokenManagerInterface is injected in FormRenderer::__construct().' );
2012-07-15 17:58:13 +01:00
}
2013-10-04 14:25:38 +01:00
return $this -> csrfTokenManager -> getToken ( $tokenId ) -> getValue ();
2012-07-15 17:58:13 +01:00
}
/**
* { @ inheritdoc }
*/
2012-07-21 18:44:09 +01:00
public function renderBlock ( FormView $view , $blockName , array $variables = array ())
2012-07-15 17:58:13 +01:00
{
2012-12-30 20:48:21 +00:00
$resource = $this -> engine -> getResourceForBlockName ( $view , $blockName );
if ( ! $resource ) {
2013-04-15 21:11:02 +01:00
throw new LogicException ( sprintf ( 'No block "%s" found while rendering the form.' , $blockName ));
2012-07-15 17:58:13 +01:00
}
2012-07-25 12:20:23 +01:00
$viewCacheKey = $view -> vars [ self :: CACHE_KEY_VAR ];
2012-07-15 17:58:13 +01:00
2012-12-30 20:48:21 +00:00
// The variables are cached globally for a view (instead of for the
// current suffix)
if ( ! isset ( $this -> variableStack [ $viewCacheKey ])) {
$this -> variableStack [ $viewCacheKey ] = array ();
2012-07-15 17:58:13 +01:00
2012-12-30 20:48:21 +00:00
// 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 ;
2012-07-15 17:58:13 +01:00
}
2012-07-16 15:44:31 +01:00
// 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 );
2012-07-15 17:58:13 +01:00
2012-07-25 12:20:23 +01:00
$this -> variableStack [ $viewCacheKey ][] = $variables ;
2012-07-15 17:58:13 +01:00
2012-07-21 18:44:09 +01:00
// Do the rendering
$html = $this -> engine -> renderBlock ( $view , $resource , $blockName , $variables );
// Clear the stack
2012-07-25 12:20:23 +01:00
array_pop ( $this -> variableStack [ $viewCacheKey ]);
2012-07-21 18:44:09 +01:00
2012-12-30 20:48:21 +00:00
if ( $varInit ) {
unset ( $this -> variableStack [ $viewCacheKey ]);
}
2012-07-21 18:44:09 +01:00
return $html ;
2012-07-15 17:58:13 +01:00
}
/**
2012-07-21 18:44:09 +01:00
* { @ inheritdoc }
2012-07-15 17:58:13 +01:00
*/
2012-07-21 18:44:09 +01:00
public function searchAndRenderBlock ( FormView $view , $blockNameSuffix , array $variables = array ())
2012-07-15 17:58:13 +01:00
{
2012-07-29 14:49:21 +01:00
$renderOnlyOnce = 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix ;
2012-07-15 17:58:13 +01:00
if ( $renderOnlyOnce && $view -> isRendered ()) {
return '' ;
}
// The cache key for storing the variables and types
2012-07-25 12:20:23 +01:00
$viewCacheKey = $view -> vars [ self :: CACHE_KEY_VAR ];
2013-04-02 10:39:57 +01:00
$viewAndSuffixCacheKey = $viewCacheKey . $blockNameSuffix ;
2012-07-15 17:58:13 +01:00
// 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).
2012-07-16 20:54:46 +01:00
// If the designer wants to explicitly fallback to "form_widget" in his
2012-07-15 17:58:13 +01:00
// 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.
2012-07-25 12:20:23 +01:00
if ( ! isset ( $this -> blockNameHierarchyMap [ $viewAndSuffixCacheKey ])) {
2012-07-15 17:58:13 +01:00
// INITIAL CALL
// Calculate the hierarchy of template blocks and start on
// the bottom level of the hierarchy (= "_<id>_<section>" block)
2012-07-21 18:44:09 +01:00
$blockNameHierarchy = array ();
2012-07-25 12:48:48 +01:00
foreach ( $view -> vars [ 'block_prefixes' ] as $blockNamePrefix ) {
2013-04-02 10:39:57 +01:00
$blockNameHierarchy [] = $blockNamePrefix . '_' . $blockNameSuffix ;
2012-07-15 17:58:13 +01:00
}
2012-07-21 18:44:09 +01:00
$hierarchyLevel = count ( $blockNameHierarchy ) - 1 ;
2012-07-15 17:58:13 +01:00
2012-07-25 12:20:23 +01:00
$hierarchyInit = true ;
2012-07-15 17:58:13 +01:00
} else {
// RECURSIVE CALL
2012-08-07 15:22:46 +01:00
// If a block recursively calls searchAndRenderBlock() again, resume rendering
2012-07-15 17:58:13 +01:00
// using the parent type in the hierarchy.
2012-07-25 12:20:23 +01:00
$blockNameHierarchy = $this -> blockNameHierarchyMap [ $viewAndSuffixCacheKey ];
$hierarchyLevel = $this -> hierarchyLevelMap [ $viewAndSuffixCacheKey ] - 1 ;
$hierarchyInit = false ;
}
2012-07-15 17:58:13 +01:00
2012-07-25 12:20:23 +01:00
// The variables are cached globally for a view (instead of for the
// current suffix)
if ( ! isset ( $this -> variableStack [ $viewCacheKey ])) {
2012-12-30 20:48:21 +00:00
$this -> variableStack [ $viewCacheKey ] = array ();
2012-07-25 12:20:23 +01:00
// The default variable scope contains all view variables, merged with
// the variables passed explicitly to the helper
$scopeVariables = $view -> vars ;
$varInit = true ;
} else {
2012-07-16 20:54:46 +01:00
// Reuse the current scope and merge it with the explicitly passed variables
2012-07-25 12:20:23 +01:00
$scopeVariables = end ( $this -> variableStack [ $viewCacheKey ]);
$varInit = false ;
2012-07-15 17:58:13 +01:00
}
// Load the resource where this block can be found
2012-07-21 18:44:09 +01:00
$resource = $this -> engine -> getResourceForBlockNameHierarchy ( $view , $blockNameHierarchy , $hierarchyLevel );
2012-07-15 17:58:13 +01:00
// 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.
2012-07-21 18:44:09 +01:00
$hierarchyLevel = $this -> engine -> getResourceHierarchyLevel ( $view , $blockNameHierarchy , $hierarchyLevel );
2012-07-15 17:58:13 +01:00
// The actually existing block name in $resource
2012-07-21 18:44:09 +01:00
$blockName = $blockNameHierarchy [ $hierarchyLevel ];
2012-07-15 17:58:13 +01:00
// Escape if no resource exists for this block
if ( ! $resource ) {
2016-06-19 10:04:47 +01:00
if ( count ( $blockNameHierarchy ) !== count ( array_unique ( $blockNameHierarchy ))) {
throw new LogicException ( sprintf ( 'Unable to render the form because the block names array contains duplicates: "%s".' , implode ( '", "' , array_reverse ( $blockNameHierarchy ))));
}
throw new LogicException ( sprintf ( 'Unable to render the form as none of the following blocks exist: "%s".' , implode ( '", "' , array_reverse ( $blockNameHierarchy ))));
2012-07-15 17:58:13 +01:00
}
2012-07-16 15:44:31 +01:00
// 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 );
2012-07-15 17:58:13 +01:00
// 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.
2012-07-25 12:20:23 +01:00
$this -> blockNameHierarchyMap [ $viewAndSuffixCacheKey ] = $blockNameHierarchy ;
$this -> hierarchyLevelMap [ $viewAndSuffixCacheKey ] = $hierarchyLevel ;
2012-07-15 17:58:13 +01:00
2012-07-25 12:20:23 +01:00
// 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 ;
2012-07-15 17:58:13 +01:00
// Do the rendering
2012-07-21 18:44:09 +01:00
$html = $this -> engine -> renderBlock ( $view , $resource , $blockName , $variables );
2012-07-15 17:58:13 +01:00
// Clear the stack
2012-07-25 12:20:23 +01:00
array_pop ( $this -> variableStack [ $viewCacheKey ]);
2012-07-15 17:58:13 +01:00
2012-07-25 12:20:23 +01:00
// Clear the caches if they were filled for the first time within
// this function call
if ( $hierarchyInit ) {
2015-03-06 19:37:36 +00:00
unset ( $this -> blockNameHierarchyMap [ $viewAndSuffixCacheKey ], $this -> hierarchyLevelMap [ $viewAndSuffixCacheKey ]);
2012-07-25 12:20:23 +01:00
}
if ( $varInit ) {
unset ( $this -> variableStack [ $viewCacheKey ]);
}
2012-07-15 17:58:13 +01:00
if ( $renderOnlyOnce ) {
$view -> setRendered ();
}
return $html ;
}
2012-07-21 18:44:09 +01:00
/**
* { @ inheritdoc }
*/
public function humanize ( $text )
{
2017-05-28 15:12:41 +01:00
return ucfirst ( strtolower ( trim ( preg_replace ( array ( '/([A-Z])/' , '/[_\s]+/' ), array ( '_$1' , ' ' ), $text ))));
2012-07-21 18:44:09 +01:00
}
2018-04-02 10:40:46 +01:00
/**
* @ internal
*/
public function encodeCurrency ( Environment $environment , $text , $widget = '' )
{
if ( 'UTF-8' === $charset = $environment -> getCharset ()) {
$text = htmlspecialchars ( $text , ENT_QUOTES | ( \defined ( 'ENT_SUBSTITUTE' ) ? ENT_SUBSTITUTE : 0 ), 'UTF-8' );
} else {
$text = htmlentities ( $text , ENT_QUOTES | ( \defined ( 'ENT_SUBSTITUTE' ) ? ENT_SUBSTITUTE : 0 ), 'UTF-8' );
$text = iconv ( 'UTF-8' , $charset , $text );
$widget = iconv ( 'UTF-8' , $charset , $widget );
}
return str_replace ( '{{ widget }}' , $widget , $text );
}
2012-07-15 17:58:13 +01:00
}