浏览代码

[Form] Renderers are now created explicitely using FormFactory::createRenderer(). This improves performance on requests where a form does not need to be rendered

Bernhard Schussek 14 年之前
父节点
当前提交
fae319e77a
共有 36 个文件被更改,包括 371 次插入193 次删除
  1. 1 1
      src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddFormGuessersPass.php
  2. 46 0
      src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddFormRendererFactoriesPass.php
  3. 43 0
      src/Symfony/Bundle/FrameworkBundle/Form/ContainerAwareRendererFactoryLoader.php
  4. 2 0
      src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
  5. 29 0
      src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
  6. 3 3
      src/Symfony/Bundle/TwigBundle/Resources/views/div_layout.html.twig
  7. 3 3
      src/Symfony/Bundle/TwigBundle/Resources/views/div_plain_layout.html.twig
  8. 4 4
      src/Symfony/Bundle/TwigBundle/Resources/views/table_layout.html.twig
  9. 21 21
      src/Symfony/Bundle/TwigBundle/Resources/views/widgets.html.twig
  10. 5 5
      src/Symfony/Component/Form/FormFactory.php
  11. 5 12
      src/Symfony/Component/Form/Renderer/RendererVarBag.php
  12. 46 0
      src/Symfony/Component/Form/Renderer/Loader/ArrayRendererFactoryLoader.php
  13. 4 6
      src/Symfony/Component/Form/Renderer/Loader/RendererLoaderInterface.php
  14. 0 44
      src/Symfony/Component/Form/Renderer/Loader/ThemeRendererLoader.php
  15. 1 1
      src/Symfony/Component/Form/Renderer/Theme/FormThemeInterface.php
  16. 13 9
      src/Symfony/Component/Form/Renderer/Theme/PhpTheme.php
  17. 12 8
      src/Symfony/Component/Form/Renderer/Theme/TwigTheme.php
  18. 8 6
      src/Symfony/Component/Form/Renderer/Theme/TwigThemeFactory.php
  19. 39 39
      src/Symfony/Component/Form/Renderer/ThemeRenderer.php
  20. 59 0
      src/Symfony/Component/Form/Renderer/ThemeRendererFactory.php
  21. 0 1
      src/Symfony/Component/Form/Type/CheckboxType.php
  22. 3 2
      src/Symfony/Component/Form/Type/ChoiceType.php
  23. 1 2
      src/Symfony/Component/Form/Type/DateType.php
  24. 1 1
      src/Symfony/Component/Form/Type/FieldType.php
  25. 1 2
      src/Symfony/Component/Form/Type/FileType.php
  26. 1 1
      src/Symfony/Component/Form/Type/FormType.php
  27. 0 1
      src/Symfony/Component/Form/Type/MoneyType.php
  28. 0 2
      src/Symfony/Component/Form/Type/PasswordType.php
  29. 0 1
      src/Symfony/Component/Form/Type/RadioType.php
  30. 0 1
      src/Symfony/Component/Form/Type/TextType.php
  31. 0 1
      src/Symfony/Component/Form/Type/TimeType.php
  32. 4 4
      tests/Symfony/Tests/Component/Form/FormFactoryTest.php
  33. 2 2
      tests/Symfony/Tests/Component/Form/Renderer/Theme/AbstractThemeFunctionalTest.php
  34. 3 4
      tests/Symfony/Tests/Component/Form/Renderer/Theme/AbstractThemeTest.php
  35. 8 3
      tests/Symfony/Tests/Component/Form/Renderer/ThemeRendererTest.php
  36. 3 3
      tests/Symfony/Tests/Component/Form/Type/TestCase.php

+ 1 - 1
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddFormGuessersPass.php

@@ -35,6 +35,6 @@ class AddFormGuessersPass implements CompilerPassInterface
             $guessers[] = new Reference($serviceId);
         }
 
-        $container->getDefinition('form.factory')->setArgument(1, $guessers);
+        $container->getDefinition('form.factory')->setArgument(2, $guessers);
     }
 }

+ 46 - 0
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddFormRendererFactoriesPass.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+
+/**
+ * Adds all services with the tag "form.renderer.factory" as argument
+ * to the "form.renderer.factory.loader" service
+ *
+ * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
+ */
+class AddFormRendererFactoriesPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container)
+    {
+        if (!$container->hasDefinition('form.renderer.factory.loader')) {
+            return;
+        }
+
+        // Builds an array with service IDs as keys and tag aliases as values
+        $types = array();
+        $tags = $container->findTaggedServiceIds('form.renderer.factory');
+
+        foreach ($tags as $serviceId => $arguments) {
+            $alias = isset($arguments[0]['alias'])
+                ? $arguments[0]['alias']
+                : $serviceId;
+
+            // Flip, because we want tag aliases (= type identifiers) as keys
+            $types[$alias] = $serviceId;
+        }
+
+        $container->getDefinition('form.renderer.factory.loader')->setArgument(1, $types);
+    }
+}

+ 43 - 0
src/Symfony/Bundle/FrameworkBundle/Form/ContainerAwareRendererFactoryLoader.php

@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Form;
+
+use Symfony\Component\Form\Exception\FormException;
+use Symfony\Component\Form\Renderer\Loader\FormRendererFactoryLoaderInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class ContainerAwareRendererFactoryLoader implements FormRendererFactoryLoaderInterface
+{
+    private $container;
+
+    private $serviceIds;
+
+    public function __construct(ContainerInterface $container, array $serviceIds)
+    {
+        $this->container = $container;
+        $this->serviceIds = $serviceIds;
+    }
+
+    public function getRendererFactory($name)
+    {
+        if (!isset($this->serviceIds[$name])) {
+            throw new FormException(sprintf('No renderer factory exists with name "%s"', $name));
+        }
+
+        return $this->container->get($this->serviceIds[$name]);
+    }
+
+    public function hasRendererFactory($name)
+    {
+        return isset($this->serviceIds[$name]);
+    }
+}

+ 2 - 0
src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

@@ -15,6 +15,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintVal
 use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddFormTypesPass;
 use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddFormTypeLoadersPass;
 use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddFormGuessersPass;
+use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddFormRendererFactoriesPass;
 use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass;
 use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RegisterKernelListenersPass;
 use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingResolverPass;
@@ -81,6 +82,7 @@ class FrameworkBundle extends Bundle
         $container->addCompilerPass(new AddFormTypesPass());
         $container->addCompilerPass(new AddFormTypeLoadersPass());
         $container->addCompilerPass(new AddFormGuessersPass());
+        $container->addCompilerPass(new AddFormRendererFactoriesPass());
         $container->addCompilerPass(new AddClassesToCachePass());
         $container->addCompilerPass(new AddClassesToAutoloadMapPass());
         $container->addCompilerPass(new TranslatorPass());

+ 29 - 0
src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml

@@ -11,6 +11,8 @@
         <parameter key="form.type.loader.tagged.class">Symfony\Bundle\FrameworkBundle\Form\ContainerAwareTypeLoader</parameter>
         <parameter key="form.guesser.validator.class">Symfony\Component\Form\Type\Guesser\ValidatorTypeGuesser</parameter>
         <parameter key="form.csrf_provider.class">Symfony\Component\Form\CsrfProvider\SessionCsrfProvider</parameter>
+        <parameter key="form.renderer.factory.loader.class">Symfony\Bundle\FrameworkBundle\Form\ContainerAwareRendererFactoryLoader</parameter>
+        <parameter key="form.renderer.factory.theme.class">Symfony\Component\Form\Renderer\ThemeRendererFactory</parameter>
         <parameter key="form.theme.factory.twig.class">Symfony\Component\Form\Renderer\Theme\TwigThemeFactory</parameter>
         <parameter key="form.theme.factory.php.class">Symfony\Component\Form\Renderer\Theme\PhpThemeFactory</parameter>
         <parameter key="form.theme.factory.phpengine.class">Symfony\Bundle\FrameworkBundle\Form\PhpEngineThemeFactory</parameter>
@@ -31,6 +33,7 @@
         <!-- FormFactory -->
         <service id="form.factory" class="%form.factory.class%">
             <argument type="service" id="form.type.loader.chain" />
+            <argument type="service" id="form.renderer.factory.loader" />
             <!--
             All services with tag "form.guesser" are inserted here by 
             AddFormGuessersPass
@@ -50,6 +53,32 @@
             <argument>%form.csrf_protection.secret%</argument>
         </service>
         
+        <!-- RendererFactoryLoader -->
+        <service id="form.renderer.factory.loader" class="%form.renderer.factory.loader.class%">
+            <argument type="service" id="service_container" />
+            <!--
+            All services with tag "form.renderer.factory" are inserted here by 
+            AddFormRendererFactoriesPass
+            -->
+            <argument type="collection" />
+        </service>
+        
+        <!-- RendererFactories -->
+        <service id="form.renderer.factory.twig" class="%form.renderer.factory.theme.class%">
+            <tag name="form.renderer.factory" alias="twig" />
+            <argument type="service" id="form.theme.factory.twig" />
+        </service>
+        
+        <service id="form.renderer.factory.default" class="%form.renderer.factory.theme.class%">
+            <tag name="form.renderer.factory" alias="default" />
+            <argument type="service" id="form.theme.factory.default" />
+        </service>
+        
+        <service id="form.renderer.factory.php" class="%form.renderer.factory.theme.class%">
+            <tag name="form.renderer.factory" alias="php" />
+            <argument type="service" id="form.theme.factory.php" />
+        </service>
+        
         <!-- Themes -->
         <service id="form.theme.factory.twig" class="%form.theme.factory.twig.class%">
             <argument type="service" id="twig" />

+ 3 - 3
src/Symfony/Bundle/TwigBundle/Resources/views/div_layout.html.twig

@@ -1,6 +1,6 @@
 {% extends "TwigBundle::widgets.html.twig" %}
 
-{% block row %}
+{% block field__row %}
 {% spaceless %}
     <div>
         {{ renderer.label }}
@@ -8,11 +8,11 @@
         {{ renderer.widget }}
     </div>
 {% endspaceless %}
-{% endblock row %}
+{% endblock field__row %}
 
 {% block form__widget %}
 {% spaceless %}
-    {{ block('rows') }}
+    {{ block('field__rows') }}
     {{ renderer.rest }}
 {% endspaceless %}
 {% endblock form__widget %}

+ 3 - 3
src/Symfony/Bundle/TwigBundle/Resources/views/div_plain_layout.html.twig

@@ -1,6 +1,6 @@
 {% extends "widgets.html.twig" %}
 
-{% block row %}
+{% block field__row %}
 {% spaceless %}
     <div>
         {{ renderer.label }}
@@ -8,11 +8,11 @@
         {{ renderer.widget }}
     </div>
 {% endspaceless %}
-{% endblock row %}
+{% endblock field__row %}
 
 {% block form__widget %}
 {% spaceless %}
-    {{ block('rows') }}
+    {{ block('field__rows') }}
     {{ renderer.rest }}
 {% endspaceless %}
 {% endblock form__widget %}

+ 4 - 4
src/Symfony/Bundle/TwigBundle/Resources/views/table_layout.html.twig

@@ -1,6 +1,6 @@
 {% extends "TwigBundle::widgets.html.twig" %}
 
-{% block row %}
+{% block field__row %}
 {% spaceless %}
     <tr>
         <td>{{ renderer.label }}</td>
@@ -10,13 +10,13 @@
         </td>
     </tr>
 {% endspaceless %}
-{% endblock row %}
+{% endblock field__row %}
 
 {% block form__errors %}
 {% spaceless %}
     <tr>
         <td colspan="2">
-            {{ block('errors') }}
+            {{ block('field__errors') }}
         </td>
     </tr>
 {% endspaceless %}
@@ -29,7 +29,7 @@
 {% block form__widget %}
 {% spaceless %}
     <table>
-    {{ block('rows') }}
+    {{ block('field__rows') }}
     {{ renderer.rest }}
     </table>
 {% endspaceless %}

+ 21 - 21
src/Symfony/Bundle/TwigBundle/Resources/views/widgets.html.twig

@@ -1,19 +1,19 @@
-{% block rows %}
+{% block field__rows %}
 {% spaceless %}
     {{ renderer.errors }}
     {% for field in fields %}
         {{ field.row }}
     {% endfor %}
 {% endspaceless %}
-{% endblock rows %}
+{% endblock field__rows %}
 
-{% block enctype %}
+{% block field__enctype %}
 {% spaceless %}
     {% if multipart %}enctype="multipart/form-data"{% endif %}
 {% endspaceless %}
-{% endblock enctype %}
+{% endblock field__enctype %}
 
-{% block errors %}
+{% block field__errors %}
 {% spaceless %}
     {% if errors|length > 0 %}
     <ul>
@@ -23,9 +23,9 @@
     </ul>
     {% endif %}  	
 {% endspaceless %}
-{% endblock errors %}
+{% endblock field__errors %}
 
-{% block rest %}
+{% block field__rest %}
 {% spaceless %}
     {% for field in fields %}
         {% if not field.rendered %}
@@ -33,13 +33,13 @@
         {% endif %}
     {% endfor %}
 {% endspaceless %}
-{% endblock rest %}
+{% endblock field__rest %}
 
-{% block label %}
+{% block field__label %}
 {% spaceless %}
     <label for="{{ id }}">{% trans label %}</label>
 {% endspaceless %}
-{% endblock label %}
+{% endblock field__label %}
 
 {% block attributes %}
 {% spaceless %}
@@ -48,31 +48,31 @@
 {% endspaceless %}
 {% endblock attributes %}
 
-{% block widget %}
+{% block field__widget %}
 {% spaceless %}
     {% set type = type|default('text') %}
     <input type="{{ type }}" {{ block('attributes') }} value="{{ value }}" />
 {% endspaceless %}
-{% endblock widget %}
+{% endblock field__widget %}
 
 {% block text__widget %}
 {% spaceless %}
     {% set type = type|default('text') %}
-    {{ block('widget') }}
+    {{ block('field__widget') }}
 {% endspaceless %}
 {% endblock text__widget %}
 
 {% block password__widget %}
 {% spaceless %}
     {% set type = type|default('password') %}
-    {{ block('widget') }}
+    {{ block('field__widget') }}
 {% endspaceless %}
 {% endblock password__widget %}
 
 {% block hidden__widget %}
 {% spaceless %}
     {% set type = type|default('hidden') %}
-    {{ block('widget') }}
+    {{ block('field__widget') }}
 {% endspaceless %}
 {% endblock hidden__widget %}
 
@@ -170,34 +170,34 @@
 {% block number__widget %}
 {% spaceless %}
     {% set type = type|default('number') %}
-    {{ block('widget') }}
+    {{ block('field__widget') }}
 {% endspaceless %}
 {% endblock number__widget %}
 
 {% block integer__widget %}
 {% spaceless %}
     {% set type = type|default('number') %}
-    {{ block('widget') }}
+    {{ block('field__widget') }}
 {% endspaceless %}
 {% endblock integer__widget %}
 
 {% block money__widget %}
 {% spaceless %}
-    {{ money_pattern|replace({ '{{ widget }}': block('widget') })|raw }}
+    {{ money_pattern|replace({ '{{ widget }}': block('field__widget') })|raw }}
 {% endspaceless %}
 {% endblock money__widget %}
 
 {% block url__widget %}
 {% spaceless %}
     {% set type = type|default('url') %}
-    {{ block('widget') }}
+    {{ block('field__widget') }}
 {% endspaceless %}
 {% endblock url__widget %}
 
 {% block percent__widget %}
 {% spaceless %}
     {% set type = type|default('text') %}
-    {{ block('widget') }} %
+    {{ block('field__widget') }} %
 {% endspaceless %}
 {% endblock percent__widget %}
 
@@ -217,6 +217,6 @@
 
 {% block repeated__row %}
 {% spaceless %}
-    {{ block('rows') }}
+    {{ block('field__rows') }}
 {% endspaceless %}
 {% endblock repeated__row %}

+ 5 - 5
src/Symfony/Component/Form/FormFactory.php

@@ -17,17 +17,17 @@ use Symfony\Component\Form\Type\Guesser\TypeGuesserInterface;
 use Symfony\Component\Form\Type\Guesser\Guess;
 use Symfony\Component\Form\Exception\FormException;
 use Symfony\Component\Form\Exception\UnexpectedTypeException;
-use Symfony\Component\Form\Renderer\Loader\RendererLoaderInterface;
+use Symfony\Component\Form\Renderer\Loader\FormRendererFactoryLoaderInterface;
 
 class FormFactory implements FormFactoryInterface
 {
     private $typeLoader;
 
-    private $rendererLoader;
+    private $rendererFactoryLoader;
 
     private $guessers = array();
 
-    public function __construct(TypeLoaderInterface $typeLoader, RendererLoaderInterface $rendererLoader, array $guessers = array())
+    public function __construct(TypeLoaderInterface $typeLoader, FormRendererFactoryLoaderInterface $rendererFactoryLoader, array $guessers = array())
     {
         foreach ($guessers as $guesser) {
             if (!$guesser instanceof TypeGuesserInterface) {
@@ -36,7 +36,7 @@ class FormFactory implements FormFactoryInterface
         }
         $this->typeLoader = $typeLoader;
         $this->guessers = $guessers;
-        $this->rendererLoader = $rendererLoader;
+        $this->rendererFactoryLoader = $rendererFactoryLoader;
     }
 
     public function createBuilder($type, $name = null, array $options = array())
@@ -139,7 +139,7 @@ class FormFactory implements FormFactoryInterface
     {
         // TODO if $name === null, use default renderer
 
-        return $this->rendererLoader->getRenderer($name, $form);
+        return $this->rendererFactoryLoader->getRendererFactory($name)->create($form);
     }
 
     /**

+ 5 - 12
src/Symfony/Component/Form/Renderer/RendererVarBag.php

@@ -11,16 +11,9 @@
 
 namespace Symfony\Component\Form\Renderer;
 
-class RendererVarBag extends \ArrayObject
-{
-    public function offsetGet($name)
-    {
-        $value = parent::offsetGet($name);
-
-        if (is_callable($value)) {
-            $value = $value();
-        }
+use Symfony\Component\Form\FormInterface;
 
-        return $value;
-    }
-}
+interface FormRendererFactoryInterface
+{
+    function create(FormInterface $form);
+}

+ 46 - 0
src/Symfony/Component/Form/Renderer/Loader/ArrayRendererFactoryLoader.php

@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Renderer\Loader;
+
+use Symfony\Component\Form\Exception\FormException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\Renderer\FormRendererFactoryInterface;
+
+class ArrayRendererFactoryLoader implements FormRendererFactoryLoaderInterface
+{
+    private $factories;
+
+    public function __construct(array $factories)
+    {
+        foreach ($factories as $factory) {
+            if (!$factory instanceof FormRendererFactoryInterface) {
+                throw new UnexpectedTypeException($factory, 'Symfony\Component\Form\Renderer\FormRendererFactoryInterface');
+            }
+        }
+
+        $this->factories = $factories;
+    }
+
+    public function getRendererFactory($name)
+    {
+        if (!isset($this->factories[$name])) {
+            throw new FormException(sprintf('No renderer factory exists with name "%s"', $name));
+        }
+
+        return $this->factories[$name];
+    }
+
+    public function hasRendererFactory($name)
+    {
+        return isset($this->factories[$name]);
+    }
+}

+ 4 - 6
src/Symfony/Component/Form/Renderer/Loader/RendererLoaderInterface.php

@@ -11,11 +11,9 @@
 
 namespace Symfony\Component\Form\Renderer\Loader;
 
-use Symfony\Component\Form\FormInterface;
-
-interface RendererLoaderInterface
+interface FormRendererFactoryLoaderInterface
 {
-    function getRenderer($name, FormInterface $form);
+    function getRendererFactory($name);
 
-    function hasRenderer($name);
-}
+    function hasRendererFactory($name);
+}

+ 0 - 44
src/Symfony/Component/Form/Renderer/Loader/ThemeRendererLoader.php

@@ -1,44 +0,0 @@
-<?php
-
-/*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Component\Form\Renderer\Loader;
-
-use Symfony\Component\Form\FormInterface;
-use Symfony\Component\Form\Exception\FormException;
-use Symfony\Component\Form\Renderer\ThemeRenderer;
-use Symfony\Component\Form\Renderer\Theme\FormThemeFactoryInterface;
-
-class ThemeRendererLoader implements RendererLoaderInterface
-{
-    private $name;
-
-    private $themeFactory;
-
-    public function __construct($name, FormThemeFactoryInterface $themeFactory)
-    {
-        $this->name = $name;
-        $this->themeFactory = $themeFactory;
-    }
-
-    public function getRenderer($name, FormInterface $form)
-    {
-        if (!$name === $this->name) {
-            throw new FormException(sprintf('Unknown renderer name "%s"', $name));
-        }
-
-        return new ThemeRenderer($form, $this->themeFactory);
-    }
-
-    public function hasRenderer($name)
-    {
-        return $this->name === $name;
-    }
-}

+ 1 - 1
src/Symfony/Component/Form/Renderer/Theme/FormThemeInterface.php

@@ -13,5 +13,5 @@ namespace Symfony\Component\Form\Renderer\Theme;
 
 interface FormThemeInterface
 {
-    function render($form, $section, array $parameters);
+    function render(array $blocks, $section, array $parameters);
 }

+ 13 - 9
src/Symfony/Component/Form/Renderer/Theme/PhpTheme.php

@@ -23,7 +23,7 @@ class PhpTheme implements FormThemeInterface
 {
     /**
      * Charset to be used with htmlentities.
-     * 
+     *
      * @var string
      */
     private $charset;
@@ -33,17 +33,21 @@ class PhpTheme implements FormThemeInterface
         $this->charset = $charset;
     }
 
-    public function render($form, $section, array $parameters)
+    public function render(array $blocks, $section, array $parameters)
     {
-        if (method_exists($this, $form.'_'.$section)) {
-            $method = $form.'_'.$section;
-        } else if (method_exists($this, $section)) {
-            $method = $section;
-        } else {
-            throw new \BadMethodCallException("PhpTheme does not support to render the form block method '" . $method . "'.");
+        foreach ($blocks as $block) {
+            $method = $block.'_'.$section;
+
+            if (method_exists($this, $method)) {
+                return $this->$method($parameters);
+            }
         }
 
-        return $this->$method($parameters);
+        $blocks = array_map(function ($block) use ($section) {
+            return $block.'_'.$section;
+        }, $blocks);
+
+        throw new \BadMethodCallException(sprintf('PhpTheme does not support the form block methods "%s"', implode('", "', $blocks)));
     }
 
     protected function checkbox_widget($attr)

+ 12 - 8
src/Symfony/Component/Form/Renderer/Theme/TwigTheme.php

@@ -73,18 +73,22 @@ class TwigTheme implements FormThemeInterface
         return array_unique($names);
     }
 
-    public function render($form, $section, array $parameters)
+    public function render(array $blocks, $section, array $parameters)
     {
         $this->initialize();
 
-        if (isset($this->templatesByBlock[$form.'__'.$section])) {
-            $blockName = $form.'__'.$section;
-        } else if (isset($this->templatesByBlock[$section])) {
-            $blockName = $section;
-        } else {
-            throw new FormException(sprintf('The form theme is missing the "%s" block', $section));
+        foreach ($blocks as $block) {
+            $blockName = $block.'__'.$section;
+
+            if (isset($this->templatesByBlock[$blockName])) {
+                return $this->templatesByBlock[$blockName]->renderBlock($blockName, $parameters);
+            }
         }
 
-        return $this->templatesByBlock[$blockName]->renderBlock($blockName, $parameters);
+        $blocks = array_map(function ($block) use ($section) {
+            return $block.'__'.$section;
+        }, $blocks);
+
+        throw new FormException(sprintf('The form theme is missing the "%s" blocks', implode('", "', $blocks)));
     }
 }

+ 8 - 6
src/Symfony/Component/Form/Renderer/Theme/TwigThemeFactory.php

@@ -50,15 +50,17 @@ class TwigThemeFactory implements FormThemeFactoryInterface
      */
     public function create($template = null)
     {
-        if ($template === null) {
-            throw new FormException('Twig themes expect a template');
-        }
-
-        if (!is_string($template) && !$template instanceof \Twig_Template) {
+        if (null !== $template && !is_string($template) && !$template instanceof \Twig_Template) {
             throw new UnexpectedTypeException($template, 'string or Twig_Template');
         }
 
-        $templates = array_merge($this->fallbackTemplates, array($template));
+        $templates = $template
+            ? array_merge($this->fallbackTemplates, array($template))
+            : $this->fallbackTemplates;
+
+        if (count($templates) === 0) {
+            throw new FormException('Twig themes either need default templates or templates passed during creation');
+        }
 
         return new TwigTheme($this->environment, $templates);
     }

+ 39 - 39
src/Symfony/Component/Form/Renderer/ThemeRenderer.php

@@ -19,13 +19,13 @@ class ThemeRenderer implements FormRendererInterface, \ArrayAccess, \IteratorAgg
 {
     private $form;
 
-    private $block;
+    private $blockHistory = array();
 
     private $themeFactory;
 
     private $theme;
 
-    private $vars;
+    private $vars = array();
 
     /**
      * Is the form attached to this renderer rendered?
@@ -38,32 +38,25 @@ class ThemeRenderer implements FormRendererInterface, \ArrayAccess, \IteratorAgg
      */
     private $rendered = false;
 
+    private $parent;
+
     private $children = array();
 
-    public function __construct(FormInterface $form, FormThemeFactoryInterface $themeFactory, $template = null, ThemeRenderer $parent = null)
+    public function __construct(FormThemeFactoryInterface $themeFactory, $template = null)
     {
-        $this->form = $form;
         $this->themeFactory = $themeFactory;
-        $this->parent = $parent;
-        $this->vars = new RendererVarBag();
-
-        if (null !== $template) {
-            $this->setTemplate($template);
-        }
 
-        $types = (array)$form->getTypes();
-
-        foreach ($types as $type) {
-            $type->buildRenderer($this, $form);
-        }
+        $this->setTemplate($template);
+    }
 
-        foreach ($form as $key => $child) {
-            $this->children[$key] = new self($child, $themeFactory, null, $parent);
-        }
+    public function setParent(self $parent)
+    {
+        $this->parent = $parent;
+    }
 
-        foreach ($types as $type) {
-            $type->buildRendererBottomUp($this, $form);
-        }
+    public function setChildren(array $children)
+    {
+        $this->children = $children;
     }
 
     public function setTemplate($template)
@@ -83,12 +76,14 @@ class ThemeRenderer implements FormRendererInterface, \ArrayAccess, \IteratorAgg
 
     public function setBlock($block)
     {
-        $this->block = $block;
+        array_unshift($this->blockHistory, $block);
     }
 
     public function getBlock()
     {
-        return $this->block;
+        reset($this->blockHistory);
+
+        return current($this->block);
     }
 
     public function setVar($name, $value)
@@ -98,7 +93,6 @@ class ThemeRenderer implements FormRendererInterface, \ArrayAccess, \IteratorAgg
 
     public function setAttribute($name, $value)
     {
-        // handling through $this->changes not necessary
         $this->vars['attr'][$name] = $value;
     }
 
@@ -109,17 +103,15 @@ class ThemeRenderer implements FormRendererInterface, \ArrayAccess, \IteratorAgg
 
     public function getVar($name)
     {
-        // TODO exception handling
-        if (isset($this->vars[$name])) {
-            return $this->vars[$name];
+        if (!isset($this->vars[$name])) {
+            return null;
         }
-        return null;
+
+        return $this->vars[$name];
     }
 
     public function getVars()
     {
-        $this->initialize();
-
         return $this->vars;
     }
 
@@ -174,7 +166,7 @@ class ThemeRenderer implements FormRendererInterface, \ArrayAccess, \IteratorAgg
 
     protected function render($part, array $vars = array())
     {
-        return $this->theme->render($this->block, $part, array_replace(
+        return $this->theme->render($this->blockHistory, $part, array_replace(
             $this->vars,
             $vars
         ));
@@ -185,21 +177,29 @@ class ThemeRenderer implements FormRendererInterface, \ArrayAccess, \IteratorAgg
         return $this->parent;
     }
 
+    public function hasParent()
+    {
+        return null !== $this->parent;
+    }
+
     public function getChildren()
     {
         return $this->children;
     }
 
+    public function hasChildren()
+    {
+        return count($this->children) > 0;
+    }
+
     public function offsetGet($name)
     {
-        $this->initialize();
-        return $this->vars['fields'][$name];
+        return $this->children[$name];
     }
 
     public function offsetExists($name)
     {
-        $this->initialize();
-        return isset($this->vars['fields'][$name]);
+        return isset($this->children[$name]);
     }
 
     public function offsetSet($name, $value)
@@ -214,12 +214,12 @@ class ThemeRenderer implements FormRendererInterface, \ArrayAccess, \IteratorAgg
 
     public function getIterator()
     {
-        $this->initialize();
-
-        if (isset($this->vars['fields'])) {
+        if (isset($this->children)) {
             $this->rendered = true;
-            return new \ArrayIterator($this->vars['fields']);
+
+            return new \ArrayIterator($this->children);
         }
+
         return new \ArrayIterator(array());
     }
 }

+ 59 - 0
src/Symfony/Component/Form/Renderer/ThemeRendererFactory.php

@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Renderer;
+
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\Exception\FormException;
+use Symfony\Component\Form\Renderer\ThemeRenderer;
+use Symfony\Component\Form\Renderer\Theme\FormThemeFactoryInterface;
+
+class ThemeRendererFactory implements FormRendererFactoryInterface
+{
+    private $themeFactory;
+
+    private $defaultTemplate;
+
+    public function __construct(FormThemeFactoryInterface $themeFactory, $defaultTemplate = null)
+    {
+        $this->themeFactory = $themeFactory;
+        $this->defaultTemplate = $defaultTemplate;
+    }
+
+    public function create(FormInterface $form, ThemeRenderer $parent = null)
+    {
+        $renderer = new ThemeRenderer($this->themeFactory, $this->defaultTemplate);
+
+        if ($parent) {
+            $renderer->setParent($parent);
+        }
+
+        $types = (array)$form->getTypes();
+        $children = array();
+
+        foreach ($types as $type) {
+            $renderer->setBlock($type->getName());
+            $type->buildRenderer($renderer, $form);
+        }
+
+        foreach ($form as $key => $child) {
+            $children[$key] = $this->create($child, $renderer);
+        }
+
+        $renderer->setChildren($children);
+
+        foreach ($types as $type) {
+            $type->buildRendererBottomUp($renderer, $form);
+        }
+
+        return $renderer;
+    }
+}

+ 0 - 1
src/Symfony/Component/Form/Type/CheckboxType.php

@@ -26,7 +26,6 @@ class CheckboxType extends AbstractType
 
     public function buildRenderer(FormRendererInterface $renderer, FormInterface $form)
     {
-        $renderer->setBlock('checkbox');
         $renderer->setVar('value', $form->getAttribute('value'));
         $renderer->setVar('checked', (bool)$form->getData());
     }

+ 3 - 2
src/Symfony/Component/Form/Type/ChoiceType.php

@@ -38,7 +38,9 @@ class ChoiceType extends AbstractType
             }
         }
 
-        $builder->setAttribute('choice_list', $options['choice_list']);
+        $builder->setAttribute('choice_list', $options['choice_list'])
+            ->setAttribute('multiple', $options['multiple'])
+            ->setAttribute('expanded', $options['expanded']);
 
         if ($options['multiple'] && $options['expanded']) {
             $builder->setClientTransformer(new ArrayToChoicesTransformer($options['choice_list']));
@@ -54,7 +56,6 @@ class ChoiceType extends AbstractType
     {
         $choiceList = $form->getAttribute('choice_list');
 
-        $renderer->setBlock('choice');
         $renderer->setVar('multiple', $form->getAttribute('multiple'));
         $renderer->setVar('expanded', $form->getAttribute('expanded'));
         $renderer->setVar('choices', $choiceList->getOtherChoices());

+ 1 - 2
src/Symfony/Component/Form/Type/DateType.php

@@ -77,9 +77,8 @@ class DateType extends AbstractType
             ->setAttribute('widget', $options['widget']);
     }
 
-    public function buildRenderer(FormRendererInterface $renderer, FormInterface $form)
+    public function buildRendererBottomUp(FormRendererInterface $renderer, FormInterface $form)
     {
-        $renderer->setBlock('date');
         $renderer->setVar('widget', $form->getAttribute('widget'));
 
         if ($renderer->hasChildren()) {

+ 1 - 1
src/Symfony/Component/Form/Type/FieldType.php

@@ -77,7 +77,6 @@ class FieldType extends AbstractType
             $name = $form->getName();
         }
 
-        $renderer->setBlock('field');
         $renderer->setVar('renderer', $renderer);
         $renderer->setVar('id', $id);
         $renderer->setVar('name', $name);
@@ -90,6 +89,7 @@ class FieldType extends AbstractType
         $renderer->setVar('size', null);
         $renderer->setVar('label', ucfirst(strtolower(str_replace('_', ' ', $form->getName()))));
         $renderer->setVar('multipart', false);
+        $renderer->setVar('attr', array());
     }
 
     public function getDefaultOptions(array $options)

+ 1 - 2
src/Symfony/Component/Form/Type/FileType.php

@@ -48,9 +48,8 @@ class FileType extends AbstractType
             ->add('name', 'hidden');
     }
 
-    public function buildRenderer(FormRendererInterface $renderer, FormInterface $form)
+    public function buildRendererBottomUp(FormRendererInterface $renderer, FormInterface $form)
     {
-        $renderer->setBlock('file');
         $renderer->setVar('multipart', true);
         $renderer['file']->setVar('type', 'file');
     }

+ 1 - 1
src/Symfony/Component/Form/Type/FormType.php

@@ -51,8 +51,8 @@ class FormType extends AbstractType
             }
         }
 
-        $renderer->setBlock('form');
         $renderer->setVar('multipart', $multipart);
+        $renderer->setVar('fields', $renderer->getChildren());
     }
 
     public function getDefaultOptions(array $options)

+ 0 - 1
src/Symfony/Component/Form/Type/MoneyType.php

@@ -28,7 +28,6 @@ class MoneyType extends AbstractType
 
     public function buildRenderer(FormRendererInterface $renderer, FormInterface $form)
     {
-        $renderer->setBlock('money');
         $renderer->setVar('money_pattern', self::getPattern($form->getAttribute('currency')));
     }
 

+ 0 - 2
src/Symfony/Component/Form/Type/PasswordType.php

@@ -24,8 +24,6 @@ class PasswordType extends AbstractType
 
     public function buildRenderer(FormRendererInterface $renderer, FormInterface $form)
     {
-        $renderer->setBlock('password');
-
         if ($form->getAttribute('always_empty') || !$form->isBound()) {
             $renderer->setVar('value', '');
         }

+ 0 - 1
src/Symfony/Component/Form/Type/RadioType.php

@@ -26,7 +26,6 @@ class RadioType extends AbstractType
 
     public function buildRenderer(FormRendererInterface $renderer, FormInterface $form)
     {
-        $renderer->setBlock('radio');
         $renderer->setVar('value', $form->getAttribute('value'));
         $renderer->setVar('checked', (bool)$form->getData());
 

+ 0 - 1
src/Symfony/Component/Form/Type/TextType.php

@@ -25,7 +25,6 @@ class TextType extends AbstractType
     public function buildRenderer(FormRendererInterface $renderer, FormInterface $form)
     {
         $renderer->setVar('max_length', $form->getAttribute('max_length'));
-        $renderer->setBlock('text');
     }
 
     public function getParent(array $options)

+ 0 - 1
src/Symfony/Component/Form/Type/TimeType.php

@@ -73,7 +73,6 @@ class TimeType extends AbstractType
 
     public function buildRenderer(FormRendererInterface $renderer, FormInterface $form)
     {
-        $renderer->setBlock('time');
         $renderer->setVar('widget', $form->getAttribute('widget'));
         $renderer->setVar('with_seconds', $form->getAttribute('with_seconds'));
     }

+ 4 - 4
tests/Symfony/Tests/Component/Form/FormFactoryTest.php

@@ -20,15 +20,15 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
 {
     private $typeLoader;
 
-    private $rendererLoader;
+    private $rendererFactoryLoader;
 
     private $factory;
 
     protected function setUp()
     {
         $this->typeLoader = $this->getMock('Symfony\Component\Form\Type\Loader\TypeLoaderInterface');
-        $this->rendererLoader = $this->getMock('Symfony\Component\Form\Renderer\Loader\RendererLoaderInterface');
-        $this->factory = new FormFactory($this->typeLoader, $this->rendererLoader);
+        $this->rendererFactoryLoader = $this->getMock('Symfony\Component\Form\Renderer\Loader\FormRendererFactoryLoaderInterface');
+        $this->factory = new FormFactory($this->typeLoader, $this->rendererFactoryLoader);
     }
 
     public function testCreateBuilderForPropertyCreatesFieldWithHighestConfidence()
@@ -187,7 +187,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
     {
         return $this->getMockBuilder('Symfony\Component\Form\FormFactory')
             ->setMethods($methods)
-            ->setConstructorArgs(array($this->typeLoader, $this->rendererLoader, $guessers))
+            ->setConstructorArgs(array($this->typeLoader, $this->rendererFactoryLoader, $guessers))
             ->getMock();
     }
 }

+ 2 - 2
tests/Symfony/Tests/Component/Form/Renderer/Theme/AbstractThemeFunctionalTest.php

@@ -39,7 +39,7 @@ abstract class AbstractThemeFunctionalTest extends \PHPUnit_Framework_TestCase
         $template = $this->getTemplate();
         $csrfProvider = new DefaultCsrfProvider('foo');
         $validator = $this->getMock('Symfony\Component\Validator\ValidatorInterface');
-        $rendererLoader = $this->getMock('Symfony\Component\Form\Renderer\Loader\RendererLoaderInterface');
+        $rendererFactoryLoader = $this->getMock('Symfony\Component\Form\Renderer\Loader\FormRendererFactoryLoaderInterface');
         $storage = new \Symfony\Component\HttpFoundation\File\TemporaryStorage('foo', 1, \sys_get_temp_dir());
 
         // ok more than 2 lines, see DefaultFormFactory.php for proposed simplication
@@ -49,7 +49,7 @@ abstract class AbstractThemeFunctionalTest extends \PHPUnit_Framework_TestCase
             new MyTestFormConfig(),
             new MyTestSubFormConfig()
         )));
-        $this->factory = new FormFactory($loaderChain, $rendererLoader);
+        $this->factory = new FormFactory($loaderChain, $rendererFactoryLoader);
     }
 
     public function testFullFormRendering()

+ 3 - 4
tests/Symfony/Tests/Component/Form/Renderer/Theme/AbstractThemeTest.php

@@ -21,7 +21,6 @@ abstract class AbstractThemeTest extends \PHPUnit_Framework_TestCase
 
     public function setUp()
     {
-        $this->form = $this->getMock('Symfony\Tests\Component\Form\FormInterface');
         $this->themeFactory = $this->createThemeFactory();
     }
 
@@ -64,7 +63,7 @@ abstract class AbstractThemeTest extends \PHPUnit_Framework_TestCase
             'required' => true,
             'size' => 20,
             'attr' => array('accesskey' => 'G', 'title' => 'Foo'),
-            'renderer' => new ThemeRenderer($this->form, $this->themeFactory, null),
+            'renderer' => new ThemeRenderer($this->themeFactory, null),
         ));
 
         $this->assertEquals('test', $input->getAttribute('value'));
@@ -124,9 +123,9 @@ abstract class AbstractThemeTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals($matches, $nodeList->length);
     }
 
-    protected function renderAsDomElement($form, $section, $parameters)
+    protected function renderAsDomElement($block, $section, $parameters)
     {
-        $html = $this->themeFactory->create()->render($form, $section, $parameters);
+        $html = $this->themeFactory->create()->render(array($block), $section, $parameters);
         $dom = new \DomDocument('UTF-8');
         $dom->loadXml($html);
         return $dom->documentElement;

+ 8 - 3
tests/Symfony/Tests/Component/Form/Renderer/ThemeRendererTest.php

@@ -26,19 +26,24 @@ class ThemeRendererTest extends \PHPUnit_Framework_TestCase
 
     public function testArrayAccess()
     {
+        $fields = array(
+            'foo' => $this->getMock('Symfony\Tests\Component\Form\FormInterface'),
+            'bar' => $this->getMock('Symfony\Tests\Component\Form\FormInterface'),
+        );
+
         $themeFactory = $this->createThemeFactory();
         $renderer = new ThemeRenderer($themeFactory);
-        $renderer->setVar('fields', array('foo' => 'baz', 'bar' => 'baz'));
+        $renderer->setChildren($fields);
 
         $this->assertTrue(isset($renderer['foo']));
         $this->assertTrue(isset($renderer['bar']));
-        $this->assertEquals('baz', $renderer['bar']);
+        $this->assertSame($fields['bar'], $renderer['bar']);
     }
 
     public function testIterator()
     {
         $themeFactory = $this->createThemeFactory();
-        
+
         $renderer = new ThemeRenderer($themeFactory);
         $renderer->setVar('fields', array('foo' => 'baz', 'bar' => 'baz'));
 

+ 3 - 3
tests/Symfony/Tests/Component/Form/Type/TestCase.php

@@ -35,7 +35,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
 
     protected $typeLoader;
 
-    protected $rendererLoader;
+    protected $rendererFactoryLoader;
 
     protected function setUp()
     {
@@ -46,7 +46,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
         $this->csrfProvider = $this->getMock('Symfony\Component\Form\CsrfProvider\CsrfProviderInterface');
         $this->validator = $this->getMock('Symfony\Component\Validator\ValidatorInterface');
         $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
-        $this->rendererLoader = $this->getMock('Symfony\Component\Form\Renderer\Loader\RendererLoaderInterface');
+        $this->rendererFactoryLoader = $this->getMock('Symfony\Component\Form\Renderer\Loader\FormRendererFactoryLoaderInterface');
 
         $this->storage = $this->getMockBuilder('Symfony\Component\HttpFoundation\File\TemporaryStorage')
             ->disableOriginalConstructor()
@@ -59,7 +59,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
             $this->typeLoader->addLoader($loader);
         }
 
-        $this->factory = new FormFactory($this->typeLoader, $this->rendererLoader);
+        $this->factory = new FormFactory($this->typeLoader, $this->rendererFactoryLoader);
 
         $this->builder = new FormBuilder($this->dispatcher);
     }