From 1972a916539d0bb877df9b16d373c272edd20cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=B6nthal?= Date: Thu, 5 Sep 2013 13:39:15 +0200 Subject: [PATCH 1/5] [Form] Added form debug collector --- .../FrameworkExtension.php | 1 + .../Resources/config/collectors.xml | 6 ++ .../Resources/config/form_debug.xml | 22 ++++++ .../Resources/views/Collector/form.html.twig | 56 +++++++++++++ .../DataCollector/Collector/FormCollector.php | 79 +++++++++++++++++++ .../DataCollector/DataCollectorExtension.php | 43 ++++++++++ .../EventListener/DataCollectorSubscriber.php | 75 ++++++++++++++++++ .../Type/DataCollectorTypeExtension.php | 50 ++++++++++++ .../Collector/FormCollectorTest.php | 46 +++++++++++ .../DataCollectorExtensionTest.php | 47 +++++++++++ .../DataCollectorSubscriberTest.php | 71 +++++++++++++++++ .../Type/DataCollectorTypeExtensionTest.php | 46 +++++++++++ 12 files changed, 542 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorSubscriber.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/DataCollector/EventListener/DataCollectorSubscriberTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 32487952c8..f00c240d2a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -218,6 +218,7 @@ class FrameworkExtension extends Extension return; } + $loader->load('form_debug.xml'); $loader->load('profiling.xml'); $loader->load('collectors.xml'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml index 0e07cdb5d9..ee956f0fa0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml @@ -13,6 +13,7 @@ Symfony\Component\HttpKernel\DataCollector\TimeDataCollector Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector + Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector @@ -53,5 +54,10 @@ + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml new file mode 100644 index 0000000000..a03ea495b5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml @@ -0,0 +1,22 @@ + + + + + + Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension + Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorSubscriber + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig new file mode 100644 index 0000000000..7ee053d189 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -0,0 +1,56 @@ +{% extends app.request.isXmlHttpRequest ? 'WebProfilerBundle:Profiler:ajax_layout.html.twig' : 'WebProfilerBundle:Profiler:layout.html.twig' %} + +{% block toolbar %} + {% if collector.dataCount %} + {% set icon %} + Forms + {{ collector.dataCount }} + {% endset %} + + {% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %} + {% endif %} +{% endblock %} + +{% block menu %} + + + Forms + {% if collector.dataCount %} + {{ collector.dataCount }} + {% endif %} + +{% endblock %} + +{% block panel %} +

Invalid Forms

+ + {% for formName, fields in collector.data %} +

{{ formName }}

+ + + + + + + + {% for fieldName, field in fields %} + + + + + + + {% endfor %} +
FieldTypeValueMessages
{{ fieldName }}{{ field.type }}{{ field.value }} +
    + {% for errorMessage in field.errors %} +
  • + {{ errorMessage.message }} +
  • + {% endfor %} +
+
+ {% else %} + No invalid form{% if collector.dataCount > 1 %}s{% endif %} detected for this request. + {% endfor %} +{% endblock %} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php b/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php new file mode 100644 index 0000000000..5cb0080df7 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\Collector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector as BaseCollector; + +/** + * DataCollector for Form Validation Failures. + * + * @author Robert Schönthal + */ +class FormCollector extends BaseCollector +{ + /** + * {@inheritDoc} + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + //nothing to do, everything is added with addError() + } + + /** + * Adds a Form-Error to the Collector. + * + * @param FormInterface $form + */ + public function addError(FormInterface $form) + { + $storeData = array( + 'root' => $form->getRoot()->getName(), + 'name' => (string)$form->getPropertyPath(), + 'type' => $form->getConfig()->getType()->getName(), + 'errors' => $form->getErrors(), + 'value' => $this->varToString($form->getViewData()) + ); + + $this->data[$storeData['root']][$storeData['name']] = $storeData; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'form'; + } + + /** + * Returns all collected Data. + * + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * Returns the number of invalid Forms. + * + * @return integer + */ + public function getDataCount() + { + return count($this->data); + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php new file mode 100644 index 0000000000..c35a1a0b74 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\AbstractExtension; + +/** + * DataCollectorExtension for collecting Form Validation Failures. + * + * @author Robert Schönthal + */ +class DataCollectorExtension extends AbstractExtension +{ + /** + * @var EventSubscriberInterface + */ + private $eventSubscriber; + + public function __construct(EventSubscriberInterface $eventSubscriber) + { + $this->eventSubscriber = $eventSubscriber; + } + + /** + * {@inheritDoc} + */ + protected function loadTypeExtensions() + { + return array( + new Type\DataCollectorTypeExtension($this->eventSubscriber) + ); + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorSubscriber.php b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorSubscriber.php new file mode 100644 index 0000000000..be343eed1f --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorSubscriber.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; + +/** + * EventSubscriber for adding Form Validation Failures to the DataCollector. + * + * @author Robert Schönthal + */ +class DataCollectorSubscriber implements EventSubscriberInterface +{ + /** + * @var FormCollector + */ + private $collector; + + public function __construct(FormCollector $collector) + { + $this->collector = $collector; + } + + /** + * {@inheritDoc} + */ + public static function getSubscribedEvents() + { + return array(FormEvents::POST_SUBMIT => array('addToProfiler', -255)); + } + + /** + * Searches for invalid Form-Data and adds them to the Collector. + * + * @param FormEvent $event The event object + */ + public function addToProfiler(FormEvent $event) + { + $form = $event->getForm(); + + if ($form->isRoot()) { + $this->addErrors($form); + } + } + + /** + * Adds an invalid Form-Element to the Collector. + * + * @param FormInterface $form + */ + private function addErrors(FormInterface $form) + { + if ($form->getErrors()) { + $this->collector->addError($form); + } + + //recursively add all child errors + foreach ($form->all() as $field) { + $this->addErrors($field); + } + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php new file mode 100644 index 0000000000..8bc849b74b --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\Type; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\FormBuilderInterface; + +/** + * DataCollector Type Extension for collecting invalid Forms. + * + * @author Robert Schönthal + */ +class DataCollectorTypeExtension extends AbstractTypeExtension +{ + /** + * @var EventSubscriberInterface + */ + private $eventSubscriber; + + public function __construct(EventSubscriberInterface $eventSubscriber) + { + $this->eventSubscriber = $eventSubscriber; + } + + /** + * {@inheritDoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventSubscriber($this->eventSubscriber); + } + + /** + * {@inheritDoc} + */ + public function getExtendedType() + { + return 'form'; + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php new file mode 100644 index 0000000000..99b585d888 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\DataCollector\Collector; + +use Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector; + +/** + * @covers Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector + */ +class FormCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testAddError() + { + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + + $type = $this->getMock('Symfony\Component\Form\FormTypeInterface'); + $type->expects($this->atLeastOnce())->method('getName')->will($this->returnValue('fizz')); + + $config = $this->getMock('Symfony\Component\Form\FormConfigInterface'); + $config->expects($this->atLeastOnce())->method('getType')->will($this->returnValue($type)); + + $form->expects($this->atLeastOnce())->method('getRoot')->will($this->returnSelf()); + $form->expects($this->atLeastOnce())->method('getPropertyPath')->will($this->returnValue('bar')); + $form->expects($this->atLeastOnce())->method('getName')->will($this->returnValue('foo')); + $form->expects($this->atLeastOnce())->method('getViewData')->will($this->returnValue('bazz')); + $form->expects($this->atLeastOnce())->method('getErrors')->will($this->returnValue(array('foo'))); + $form->expects($this->atLeastOnce())->method('getConfig')->will($this->returnValue($config)); + + + $c = new FormCollector(); + $c->addError($form); + + $this->assertInternalType('array', $c->getData()); + $this->assertEquals(1, $c->getDataCount()); + $this->assertEquals(array('foo' => array('bar' => array('value' => 'bazz', 'root' => 'foo', 'type' => 'fizz', 'name' => 'bar', 'errors' => array('foo')))), $c->getData()); + } +} + \ No newline at end of file diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php new file mode 100644 index 0000000000..14ab0eac1b --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\DataCollector; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\DataCollector\DataCollectorExtension; + +/** + * @covers Symfony\Component\Form\Extension\DataCollector\DataCollectorExtension + */ +class DataCollectorExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var DataCollectorExtension + */ + private $extension; + + /** + * @var EventSubscriberInterface + */ + private $eventSubscriber; + + public function setUp() + { + $this->eventSubscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface'); + $this->extension = new DataCollectorExtension($this->eventSubscriber); + } + + public function testLoadTypeExtensions() + { + $typeExtensions = $this->extension->getTypeExtensions('form'); + + $this->assertInternalType('array', $typeExtensions); + $this->assertCount(1, $typeExtensions); + $this->assertInstanceOf('Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension', array_shift($typeExtensions)); + } +} + \ No newline at end of file diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/EventListener/DataCollectorSubscriberTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/EventListener/DataCollectorSubscriberTest.php new file mode 100644 index 0000000000..9bd8cdf6da --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/EventListener/DataCollectorSubscriberTest.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\DataCollector\EventListener; + +use Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector; +use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorSubscriber; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * @covers Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorSubscriber + */ +class DataCollectorSubscriberTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var DataCollectorSubscriber + */ + private $eventSubscriber; + + /** + * @var FormCollector + */ + private $collector; + + public function setUp() + { + $this->collector = $this->getMockBuilder('Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector')->setMethods(array('addError'))->getMock(); + $this->eventSubscriber = new DataCollectorSubscriber($this->collector); + } + + public function testSubscribedEvents() + { + $events = DataCollectorSubscriber::getSubscribedEvents(); + + $this->assertInternalType('array', $events); + $this->assertEquals(array(FormEvents::POST_SUBMIT => array('addToProfiler', -255)), $events); + } + + public function testAddToProfilerWithSubForm() + { + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $form->expects($this->atLeastOnce())->method('isRoot')->will($this->returnValue(false)); + + $formEvent = new FormEvent($form, array()); + + $this->collector->expects($this->never())->method('addError'); + $this->eventSubscriber->addToProfiler($formEvent); + } + + public function testAddToProfilerWithInValidForm() + { + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $form->expects($this->atLeastOnce())->method('isRoot')->will($this->returnValue(true)); + $form->expects($this->atLeastOnce())->method('getErrors')->will($this->returnValue(array('foo'))); + $form->expects($this->once())->method('all')->will($this->returnValue(array())); + + $formEvent = new FormEvent($form, array()); + + $this->collector->expects($this->atLeastOnce())->method('addError')->with($form); + $this->eventSubscriber->addToProfiler($formEvent); + } +} + \ No newline at end of file diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php new file mode 100644 index 0000000000..4039bf6ee6 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\DataCollector\Type; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension; + +class DataCollectorTypeExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var DataCollectorTypeExtension + */ + private $extension; + + /** + * @var EventSubscriberInterface + */ + private $eventSubscriber; + + public function setUp() + { + $this->eventSubscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface'); + $this->extension = new DataCollectorTypeExtension($this->eventSubscriber); + } + + public function testGetExtendedType() + { + $this->assertEquals('form', $this->extension->getExtendedType()); + } + + public function testBuildForm() + { + $builder = $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); + $builder->expects($this->atLeastOnce())->method('addEventSubscriber')->with($this->eventSubscriber); + + $this->extension->buildForm($builder, array()); + } +} From a994a5d4104652c641cc1346af36445656383c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=B6nthal?= Date: Mon, 16 Sep 2013 09:26:48 +0200 Subject: [PATCH 2/5] [Form] Merged subsriber/collector, also collect valid forms --- .../Resources/config/form_debug.xml | 4 - .../Resources/views/Collector/form.html.twig | 22 +++--- .../DataCollector/Collector/FormCollector.php | 65 ++++++++++++++-- .../DataCollector/DataCollectorExtension.php | 2 +- .../EventListener/DataCollectorSubscriber.php | 75 ------------------- .../Collector/FormCollectorTest.php | 34 ++++++--- .../DataCollectorSubscriberTest.php | 71 ------------------ 7 files changed, 95 insertions(+), 178 deletions(-) delete mode 100644 src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorSubscriber.php delete mode 100644 src/Symfony/Component/Form/Tests/Extension/DataCollector/EventListener/DataCollectorSubscriberTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml index a03ea495b5..b0c0e3ff65 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml @@ -6,16 +6,12 @@ Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension - Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorSubscriber - - - diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig index 7ee053d189..ab9db1f28e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -1,13 +1,13 @@ -{% extends app.request.isXmlHttpRequest ? 'WebProfilerBundle:Profiler:ajax_layout.html.twig' : 'WebProfilerBundle:Profiler:layout.html.twig' %} +{% extends '@WebProfiler/Profiler/layout.html.twig' %} {% block toolbar %} - {% if collector.dataCount %} + {% if collector.data|length %} {% set icon %} Forms - {{ collector.dataCount }} + {% if collector.errorCount %}{{ collector.errorCount }}{% else %}{{ collector.data|length }}{% endif %} {% endset %} - {% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %} + {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %} {% endif %} {% endblock %} @@ -15,17 +15,18 @@ Forms - {% if collector.dataCount %} - {{ collector.dataCount }} - {% endif %} + {% if collector.data|length %} + {{ collector.data|length }} + {% endif %} {% endblock %} {% block panel %} -

Invalid Forms

+

Form{% if collector.data|length > 1 %}s{% endif %}

{% for formName, fields in collector.data %}

{{ formName }}

+ {% if fields %} @@ -50,7 +51,10 @@ {% endfor %}
Field
+ {% else %} + This form is valid. + {% endif %} {% else %} - No invalid form{% if collector.dataCount > 1 %}s{% endif %} detected for this request. + No forms were submitted for this request. {% endfor %} {% endblock %} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php b/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php index 5cb0080df7..82564e4068 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php @@ -11,18 +11,29 @@ namespace Symfony\Component\Form\Extension\DataCollector\Collector; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector as BaseCollector; /** - * DataCollector for Form Validation Failures. + * DataCollector for Form Validation. * * @author Robert Schönthal */ -class FormCollector extends BaseCollector +class FormCollector extends BaseCollector implements EventSubscriberInterface { + /** + * {@inheritDoc} + */ + public static function getSubscribedEvents() + { + return array(FormEvents::POST_SUBMIT => array('collectForm', -255)); + } + /** * {@inheritDoc} */ @@ -31,16 +42,48 @@ class FormCollector extends BaseCollector //nothing to do, everything is added with addError() } + /** + * Collects Form-Validation-Data and adds them to the Collector. + * + * @param FormEvent $event The event object + */ + public function collectForm(FormEvent $event) + { + $form = $event->getForm(); + + if ($form->isRoot()) { + $this->data[$form->getName()] = array(); + $this->addForm($form); + } + } + + /** + * Adds an Form-Element to the Collector. + * + * @param FormInterface $form + */ + private function addForm(FormInterface $form) + { + if ($form->getErrors()) { + $this->addError($form); + } + + // recursively add all child-errors + foreach ($form->all() as $field) { + $this->addForm($field); + } + } + /** * Adds a Form-Error to the Collector. * * @param FormInterface $form */ - public function addError(FormInterface $form) + private function addError(FormInterface $form) { $storeData = array( 'root' => $form->getRoot()->getName(), - 'name' => (string)$form->getPropertyPath(), + 'name' => (string) $form->getPropertyPath(), 'type' => $form->getConfig()->getType()->getName(), 'errors' => $form->getErrors(), 'value' => $this->varToString($form->getViewData()) @@ -68,12 +111,20 @@ class FormCollector extends BaseCollector } /** - * Returns the number of invalid Forms. + * Returns the number of Forms with Errors. * * @return integer */ - public function getDataCount() + public function getErrorCount() { - return count($this->data); + $errorCount = 0; + + foreach ($this->data as $form) { + if (count($form)) { + $errorCount++; + } + } + + return $errorCount; } } diff --git a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php index c35a1a0b74..f9f8209188 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php @@ -40,4 +40,4 @@ class DataCollectorExtension extends AbstractExtension new Type\DataCollectorTypeExtension($this->eventSubscriber) ); } -} +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorSubscriber.php b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorSubscriber.php deleted file mode 100644 index be343eed1f..0000000000 --- a/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorSubscriber.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Extension\DataCollector\EventListener; - -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\FormInterface; - -/** - * EventSubscriber for adding Form Validation Failures to the DataCollector. - * - * @author Robert Schönthal - */ -class DataCollectorSubscriber implements EventSubscriberInterface -{ - /** - * @var FormCollector - */ - private $collector; - - public function __construct(FormCollector $collector) - { - $this->collector = $collector; - } - - /** - * {@inheritDoc} - */ - public static function getSubscribedEvents() - { - return array(FormEvents::POST_SUBMIT => array('addToProfiler', -255)); - } - - /** - * Searches for invalid Form-Data and adds them to the Collector. - * - * @param FormEvent $event The event object - */ - public function addToProfiler(FormEvent $event) - { - $form = $event->getForm(); - - if ($form->isRoot()) { - $this->addErrors($form); - } - } - - /** - * Adds an invalid Form-Element to the Collector. - * - * @param FormInterface $form - */ - private function addErrors(FormInterface $form) - { - if ($form->getErrors()) { - $this->collector->addError($form); - } - - //recursively add all child errors - foreach ($form->all() as $field) { - $this->addErrors($field); - } - } -} diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php index 99b585d888..20bf9878df 100644 --- a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php @@ -11,15 +11,23 @@ namespace Symfony\Component\Form\Tests\Extension\DataCollector\Collector; use Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; -/** - * @covers Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector - */ class FormCollectorTest extends \PHPUnit_Framework_TestCase { - public function testAddError() + public function testSubscribedEvents() + { + $events = FormCollector::getSubscribedEvents(); + + $this->assertInternalType('array', $events); + $this->assertEquals(array(FormEvents::POST_SUBMIT => array('collectForm', -255)), $events); + } + + public function testCollect() { $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $subForm = $this->getMock('Symfony\Component\Form\Test\FormInterface'); $type = $this->getMock('Symfony\Component\Form\FormTypeInterface'); $type->expects($this->atLeastOnce())->method('getName')->will($this->returnValue('fizz')); @@ -27,19 +35,23 @@ class FormCollectorTest extends \PHPUnit_Framework_TestCase $config = $this->getMock('Symfony\Component\Form\FormConfigInterface'); $config->expects($this->atLeastOnce())->method('getType')->will($this->returnValue($type)); - $form->expects($this->atLeastOnce())->method('getRoot')->will($this->returnSelf()); - $form->expects($this->atLeastOnce())->method('getPropertyPath')->will($this->returnValue('bar')); + $form->expects($this->atLeastOnce())->method('all')->will($this->returnValue(array($subForm))); + $form->expects($this->atLeastOnce())->method('isRoot')->will($this->returnValue(true)); $form->expects($this->atLeastOnce())->method('getName')->will($this->returnValue('foo')); - $form->expects($this->atLeastOnce())->method('getViewData')->will($this->returnValue('bazz')); - $form->expects($this->atLeastOnce())->method('getErrors')->will($this->returnValue(array('foo'))); - $form->expects($this->atLeastOnce())->method('getConfig')->will($this->returnValue($config)); + $subForm->expects($this->atLeastOnce())->method('all')->will($this->returnValue(array())); + $subForm->expects($this->atLeastOnce())->method('getErrors')->will($this->returnValue(array('foo'))); + $subForm->expects($this->atLeastOnce())->method('getRoot')->will($this->returnValue($form)); + $subForm->expects($this->atLeastOnce())->method('getConfig')->will($this->returnValue($config)); + $subForm->expects($this->atLeastOnce())->method('getPropertyPath')->will($this->returnValue('bar')); + $subForm->expects($this->atLeastOnce())->method('getViewData')->will($this->returnValue('bazz')); + $event = new FormEvent($form, array()); $c = new FormCollector(); - $c->addError($form); + $c->collectForm($event); $this->assertInternalType('array', $c->getData()); - $this->assertEquals(1, $c->getDataCount()); + $this->assertEquals(1, $c->getErrorCount()); $this->assertEquals(array('foo' => array('bar' => array('value' => 'bazz', 'root' => 'foo', 'type' => 'fizz', 'name' => 'bar', 'errors' => array('foo')))), $c->getData()); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/EventListener/DataCollectorSubscriberTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/EventListener/DataCollectorSubscriberTest.php deleted file mode 100644 index 9bd8cdf6da..0000000000 --- a/src/Symfony/Component/Form/Tests/Extension/DataCollector/EventListener/DataCollectorSubscriberTest.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Tests\Extension\DataCollector\EventListener; - -use Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector; -use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorSubscriber; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; - -/** - * @covers Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorSubscriber - */ -class DataCollectorSubscriberTest extends \PHPUnit_Framework_TestCase -{ - /** - * @var DataCollectorSubscriber - */ - private $eventSubscriber; - - /** - * @var FormCollector - */ - private $collector; - - public function setUp() - { - $this->collector = $this->getMockBuilder('Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector')->setMethods(array('addError'))->getMock(); - $this->eventSubscriber = new DataCollectorSubscriber($this->collector); - } - - public function testSubscribedEvents() - { - $events = DataCollectorSubscriber::getSubscribedEvents(); - - $this->assertInternalType('array', $events); - $this->assertEquals(array(FormEvents::POST_SUBMIT => array('addToProfiler', -255)), $events); - } - - public function testAddToProfilerWithSubForm() - { - $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); - $form->expects($this->atLeastOnce())->method('isRoot')->will($this->returnValue(false)); - - $formEvent = new FormEvent($form, array()); - - $this->collector->expects($this->never())->method('addError'); - $this->eventSubscriber->addToProfiler($formEvent); - } - - public function testAddToProfilerWithInValidForm() - { - $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); - $form->expects($this->atLeastOnce())->method('isRoot')->will($this->returnValue(true)); - $form->expects($this->atLeastOnce())->method('getErrors')->will($this->returnValue(array('foo'))); - $form->expects($this->once())->method('all')->will($this->returnValue(array())); - - $formEvent = new FormEvent($form, array()); - - $this->collector->expects($this->atLeastOnce())->method('addError')->with($form); - $this->eventSubscriber->addToProfiler($formEvent); - } -} - \ No newline at end of file From 56d78eda563330d68444f3d6d6363df53a581ad7 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 17 Sep 2013 11:39:17 +0200 Subject: [PATCH 3/5] [Form] Decoupled methods of ResolvedFormType so that they can be overridden individually by decorators --- src/Symfony/Component/Form/Button.php | 11 +- src/Symfony/Component/Form/Form.php | 18 +- src/Symfony/Component/Form/FormFactory.php | 8 +- .../Component/Form/ResolvedFormType.php | 33 +- .../Component/Form/Tests/AbstractFormTest.php | 4 +- .../Component/Form/Tests/CompoundFormTest.php | 60 ++++ .../Component/Form/Tests/FormFactoryTest.php | 172 ++++++--- .../Form/Tests/ResolvedFormTypeTest.php | 327 +++++++++++------- 8 files changed, 437 insertions(+), 196 deletions(-) diff --git a/src/Symfony/Component/Form/Button.php b/src/Symfony/Component/Form/Button.php index aecbabaebe..cecf27b696 100644 --- a/src/Symfony/Component/Form/Button.php +++ b/src/Symfony/Component/Form/Button.php @@ -412,7 +412,16 @@ class Button implements \IteratorAggregate, FormInterface $parent = $this->parent->createView(); } - return $this->config->getType()->createView($this, $parent); + + $type = $this->config->getType(); + $options = $this->config->getOptions(); + + $view = $type->createView($this, $parent); + + $type->buildView($view, $this, $options); + $type->finishView($view, $this, $options); + + return $view; } /** diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index f3324986c8..e03e329ff8 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -969,7 +969,23 @@ class Form implements \IteratorAggregate, FormInterface $parent = $this->parent->createView(); } - return $this->config->getType()->createView($this, $parent); + $type = $this->config->getType(); + $options = $this->config->getOptions(); + + // The methods createView(), buildView() and finishView() are called + // explicitly here in order to be able to override either of them + // in a custom resolved form type. + $view = $type->createView($this, $parent); + + $type->buildView($view, $this, $options); + + foreach ($this->children as $name => $child) { + $view->children[$name] = $child->createView($view); + } + + $type->finishView($view, $this, $options); + + return $view; } /** diff --git a/src/Symfony/Component/Form/FormFactory.php b/src/Symfony/Component/Form/FormFactory.php index a5fd9cc0c6..d76e73101b 100644 --- a/src/Symfony/Component/Form/FormFactory.php +++ b/src/Symfony/Component/Form/FormFactory.php @@ -84,7 +84,13 @@ class FormFactory implements FormFactoryInterface throw new UnexpectedTypeException($type, 'string, Symfony\Component\Form\ResolvedFormTypeInterface or Symfony\Component\Form\FormTypeInterface'); } - return $type->createBuilder($this, $name, $options); + $builder = $type->createBuilder($this, $name, $options); + + // Explicitly call buildForm() in order to be able to override either + // createBuilder() or buildForm() in the resolved form type + $type->buildForm($builder, $builder->getOptions()); + + return $builder; } /** diff --git a/src/Symfony/Component/Form/ResolvedFormType.php b/src/Symfony/Component/Form/ResolvedFormType.php index 47d43553cd..c2fed95859 100644 --- a/src/Symfony/Component/Form/ResolvedFormType.php +++ b/src/Symfony/Component/Form/ResolvedFormType.php @@ -114,8 +114,6 @@ class ResolvedFormType implements ResolvedFormTypeInterface $builder = $this->newBuilder($name, $dataClass, $factory, $options); $builder->setType($this); - $this->buildForm($builder, $options); - return $builder; } @@ -124,28 +122,12 @@ class ResolvedFormType implements ResolvedFormTypeInterface */ public function createView(FormInterface $form, FormView $parent = null) { - $options = $form->getConfig()->getOptions(); - - $view = $this->newView($parent); - - $this->buildView($view, $form, $options); - - foreach ($form as $name => $child) { - /* @var FormInterface $child */ - $view->children[$name] = $child->createView($view); - } - - $this->finishView($view, $form, $options); - - return $view; + return $this->newView($parent); } /** * Configures a form builder for the type hierarchy. * - * This method is protected in order to allow implementing classes - * to change or call it in re-implementations of {@link createBuilder()}. - * * @param FormBuilderInterface $builder The builder to configure. * @param array $options The options used for the configuration. */ @@ -166,10 +148,7 @@ class ResolvedFormType implements ResolvedFormTypeInterface /** * Configures a form view for the type hierarchy. * - * This method is protected in order to allow implementing classes - * to change or call it in re-implementations of {@link createView()}. - * - * It is called before the children of the view are built. + * This method is called before the children of the view are built. * * @param FormView $view The form view to configure. * @param FormInterface $form The form corresponding to the view. @@ -192,10 +171,7 @@ class ResolvedFormType implements ResolvedFormTypeInterface /** * Finishes a form view for the type hierarchy. * - * This method is protected in order to allow implementing classes - * to change or call it in re-implementations of {@link createView()}. - * - * It is called after the children of the view have been built. + * This method is called after the children of the view have been built. * * @param FormView $view The form view to configure. * @param FormInterface $form The form corresponding to the view. @@ -218,9 +194,6 @@ class ResolvedFormType implements ResolvedFormTypeInterface /** * Returns the configured options resolver used for this type. * - * This method is protected in order to allow implementing classes - * to change or call it in re-implementations of {@link createBuilder()}. - * * @return \Symfony\Component\OptionsResolver\OptionsResolverInterface The options resolver. */ public function getOptionsResolver() diff --git a/src/Symfony/Component/Form/Tests/AbstractFormTest.php b/src/Symfony/Component/Form/Tests/AbstractFormTest.php index 28f0e389e5..083176e670 100644 --- a/src/Symfony/Component/Form/Tests/AbstractFormTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractFormTest.php @@ -60,9 +60,9 @@ abstract class AbstractFormTest extends \PHPUnit_Framework_TestCase * * @return FormBuilder */ - protected function getBuilder($name = 'name', EventDispatcherInterface $dispatcher = null, $dataClass = null) + protected function getBuilder($name = 'name', EventDispatcherInterface $dispatcher = null, $dataClass = null, array $options = array()) { - return new FormBuilder($name, $dataClass, $dispatcher ?: $this->dispatcher, $this->factory); + return new FormBuilder($name, $dataClass, $dispatcher ?: $this->dispatcher, $this->factory, $options); } /** diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index 7d9126a988..225cc93391 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Forms; +use Symfony\Component\Form\FormView; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer; @@ -806,6 +807,65 @@ class CompoundFormTest extends AbstractFormTest $this->assertEquals("name:\n ERROR: Error!\nfoo:\n No errors\n", $parent->getErrorsAsString()); } + // Basic cases are covered in SimpleFormTest + public function testCreateViewWithChildren() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $options = array('a' => 'Foo', 'b' => 'Bar'); + $field1 = $this->getMockForm('foo'); + $field2 = $this->getMockForm('bar'); + $view = new FormView(); + $field1View = new FormView(); + $field2View = new FormView(); + + $this->form = $this->getBuilder('form', null, null, $options) + ->setCompound(true) + ->setDataMapper($this->getDataMapper()) + ->setType($type) + ->getForm(); + $this->form->add($field1); + $this->form->add($field2); + + $test = $this; + + $assertChildViewsEqual = function (array $childViews) use ($test) { + return function (FormView $view) use ($test, $childViews) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertSame($childViews, $view->children); + }; + }; + + // First create the view + $type->expects($this->once()) + ->method('createView') + ->will($this->returnValue($view)); + + // Then build it for the form itself + $type->expects($this->once()) + ->method('buildView') + ->with($view, $this->form, $options) + ->will($this->returnCallback($assertChildViewsEqual(array()))); + + // Then add the first child form + $field1->expects($this->once()) + ->method('createView') + ->will($this->returnValue($field1View)); + + // Then the second child form + $field2->expects($this->once()) + ->method('createView') + ->will($this->returnValue($field2View)); + + // Again build the view for the form itself. This time the child views + // exist. + $type->expects($this->once()) + ->method('finishView') + ->with($view, $this->form, $options) + ->will($this->returnCallback($assertChildViewsEqual(array('foo' => $field1View, 'bar' => $field2View)))); + + $this->assertSame($view, $this->form->createView()); + } + protected function createForm() { return $this->getBuilder() diff --git a/src/Symfony/Component/Form/Tests/FormFactoryTest.php b/src/Symfony/Component/Form/Tests/FormFactoryTest.php index bb24079450..cdd06e1594 100644 --- a/src/Symfony/Component/Form/Tests/FormFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/FormFactoryTest.php @@ -46,6 +46,11 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase */ private $resolvedTypeFactory; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $builder; + /** * @var FormFactory */ @@ -57,6 +62,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $this->guesser1 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); $this->guesser2 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); $this->registry = $this->getMock('Symfony\Component\Form\FormRegistryInterface'); + $this->builder = $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); $this->factory = new FormFactory($this->registry, $this->resolvedTypeFactory); $this->registry->expects($this->any()) @@ -70,6 +76,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase public function testCreateNamedBuilderWithTypeName() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); $this->registry->expects($this->once()) @@ -80,14 +87,23 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', null, $options)); + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', 'type', null, $options)); } public function testCreateNamedBuilderWithTypeInstance() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $type = new FooType(); $resolvedType = $this->getMockResolvedType(); @@ -99,14 +115,23 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options)); + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', $type, null, $options)); } public function testCreateNamedBuilderWithTypeInstanceWithParentType() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $type = new FooSubType(); $resolvedType = $this->getMockResolvedType(); $parentResolvedType = $this->getMockResolvedType(); @@ -124,14 +149,23 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options)); + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', $type, null, $options)); } public function testCreateNamedBuilderWithTypeInstanceWithParentTypeInstance() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $type = new FooSubTypeWithParentInstance(); $resolvedType = $this->getMockResolvedType(); $parentResolvedType = $this->getMockResolvedType(); @@ -149,28 +183,46 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options)); + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', $type, null, $options)); } public function testCreateNamedBuilderWithResolvedTypeInstance() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $resolvedType, null, $options)); + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', $resolvedType, null, $options)); } public function testCreateNamedBuilderFillsDataOption() { $givenOptions = array('a' => '1', 'b' => '2'); $expectedOptions = array_merge($givenOptions, array('data' => 'DATA')); + $resolvedOptions = array('a' => '2', 'b' => '3', 'data' => 'DATA'); $resolvedType = $this->getMockResolvedType(); $this->registry->expects($this->once()) @@ -181,14 +233,23 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $expectedOptions) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', 'DATA', $givenOptions)); + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', 'type', 'DATA', $givenOptions)); } public function testCreateNamedBuilderDoesNotOverrideExistingDataOption() { $options = array('a' => '1', 'b' => '2', 'data' => 'CUSTOM'); + $resolvedOptions = array('a' => '2', 'b' => '3', 'data' => 'CUSTOM'); $resolvedType = $this->getMockResolvedType(); $this->registry->expects($this->once()) @@ -199,9 +260,17 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', 'DATA', $options)); + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', 'type', 'DATA', $options)); } /** @@ -216,8 +285,8 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase public function testCreateUsesTypeNameIfTypeGivenAsString() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); - $builder = $this->getMockFormBuilder(); $this->registry->expects($this->once()) ->method('getType') @@ -227,9 +296,17 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'TYPE', $options) - ->will($this->returnValue($builder)); + ->will($this->returnValue($this->builder)); - $builder->expects($this->once()) + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->builder->expects($this->once()) ->method('getForm') ->will($this->returnValue('FORM')); @@ -239,8 +316,8 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase public function testCreateUsesTypeNameIfTypeGivenAsObject() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); - $builder = $this->getMockFormBuilder(); $resolvedType->expects($this->once()) ->method('getName') @@ -249,9 +326,17 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'TYPE', $options) - ->will($this->returnValue($builder)); + ->will($this->returnValue($this->builder)); - $builder->expects($this->once()) + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->builder->expects($this->once()) ->method('getForm') ->will($this->returnValue('FORM')); @@ -261,8 +346,8 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase public function testCreateNamed() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); - $builder = $this->getMockFormBuilder(); $this->registry->expects($this->once()) ->method('getType') @@ -272,9 +357,17 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue($builder)); + ->will($this->returnValue($this->builder)); - $builder->expects($this->once()) + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->builder->expects($this->once()) ->method('getForm') ->will($this->returnValue('FORM')); @@ -294,9 +387,9 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase ->with('firstName', 'text', null, array()) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); + $this->builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderForPropertyCreatesFormWithHighestConfidence() @@ -326,9 +419,9 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase ->with('firstName', 'password', null, array('max_length' => 7)) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); + $this->builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderCreatesTextFormIfNoGuess() @@ -345,9 +438,9 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase ->with('firstName', 'text') ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); + $this->builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testOptionsCanBeOverridden() @@ -368,14 +461,14 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase ->with('firstName', 'text', null, array('max_length' => 11)) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty( + $this->builder = $factory->createBuilderForProperty( 'Application\Author', 'firstName', null, array('max_length' => 11) ); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderUsesMaxLengthIfFound() @@ -403,12 +496,12 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase ->with('firstName', 'text', null, array('max_length' => 20)) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty( + $this->builder = $factory->createBuilderForProperty( 'Application\Author', 'firstName' ); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderUsesRequiredSettingWithHighestConfidence() @@ -436,12 +529,12 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase ->with('firstName', 'text', null, array('required' => false)) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty( + $this->builder = $factory->createBuilderForProperty( 'Application\Author', 'firstName' ); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderUsesPatternIfFound() @@ -469,12 +562,12 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase ->with('firstName', 'text', null, array('pattern' => '[a-zA-Z]')) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty( + $this->builder = $factory->createBuilderForProperty( 'Application\Author', 'firstName' ); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } private function getMockFactory(array $methods = array()) @@ -494,9 +587,4 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase { return $this->getMock('Symfony\Component\Form\FormTypeInterface'); } - - private function getMockFormBuilder() - { - return $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); - } } diff --git a/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php b/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php index de19e9783e..cf2275f8d4 100644 --- a/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php +++ b/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php @@ -36,12 +36,71 @@ class ResolvedFormTypeTest extends \PHPUnit_Framework_TestCase * @var \PHPUnit_Framework_MockObject_MockObject */ private $dataMapper; + private $parentType; + private $type; + private $extension1; + private $extension2; + private $parentResolvedType; + private $resolvedType; protected function setUp() { $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); $this->dataMapper = $this->getMock('Symfony\Component\Form\DataMapperInterface'); + $this->parentType = $this->getMockFormType(); + $this->type = $this->getMockFormType(); + $this->extension1 = $this->getMockFormTypeExtension(); + $this->extension2 = $this->getMockFormTypeExtension(); + $this->parentResolvedType = new ResolvedFormType($this->parentType); + $this->resolvedType = new ResolvedFormType($this->type, array($this->extension1, $this->extension2), $this->parentResolvedType); + } + + public function testGetOptionsResolver() + { + if (version_compare(\PHPUnit_Runner_Version::id(), '3.7', '<')) { + $this->markTestSkipped('This test requires PHPUnit 3.7.'); + } + + $test = $this; + $i = 0; + + $assertIndexAndAddOption = function ($index, $option, $default) use (&$i, $test) { + return function (OptionsResolverInterface $resolver) use (&$i, $test, $index, $option, $default) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals($index, $i, 'Executed at index '.$index); + + ++$i; + + $resolver->setDefaults(array($option => $default)); + }; + }; + + // First the default options are generated for the super type + $this->parentType->expects($this->once()) + ->method('setDefaultOptions') + ->will($this->returnCallback($assertIndexAndAddOption(0, 'a', 'a_default'))); + + // The form type itself + $this->type->expects($this->once()) + ->method('setDefaultOptions') + ->will($this->returnCallback($assertIndexAndAddOption(1, 'b', 'b_default'))); + + // And its extensions + $this->extension1->expects($this->once()) + ->method('setDefaultOptions') + ->will($this->returnCallback($assertIndexAndAddOption(2, 'c', 'c_default'))); + + $this->extension2->expects($this->once()) + ->method('setDefaultOptions') + ->will($this->returnCallback($assertIndexAndAddOption(3, 'd', 'd_default'))); + + $givenOptions = array('a' => 'a_custom', 'c' => 'c_custom'); + $resolvedOptions = array('a' => 'a_custom', 'b' => 'b_default', 'c' => 'c_custom', 'd' => 'd_default'); + + $resolver = $this->resolvedType->getOptionsResolver(); + + $this->assertEquals($resolvedOptions, $resolver->resolve($givenOptions)); } public function testCreateBuilder() @@ -50,13 +109,70 @@ class ResolvedFormTypeTest extends \PHPUnit_Framework_TestCase $this->markTestSkipped('This test requires PHPUnit 3.7.'); } - $parentType = $this->getMockFormType(); - $type = $this->getMockFormType(); - $extension1 = $this->getMockFormTypeExtension(); - $extension2 = $this->getMockFormTypeExtension(); + $givenOptions = array('a' => 'a_custom', 'c' => 'c_custom'); + $resolvedOptions = array('a' => 'a_custom', 'b' => 'b_default', 'c' => 'c_custom', 'd' => 'd_default'); + $optionsResolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); - $parentResolvedType = new ResolvedFormType($parentType); - $resolvedType = new ResolvedFormType($type, array($extension1, $extension2), $parentResolvedType); + $this->resolvedType = $this->getMockBuilder('Symfony\Component\Form\ResolvedFormType') + ->setConstructorArgs(array($this->type, array($this->extension1, $this->extension2), $this->parentResolvedType)) + ->setMethods(array('getOptionsResolver')) + ->getMock(); + + $this->resolvedType->expects($this->once()) + ->method('getOptionsResolver') + ->will($this->returnValue($optionsResolver)); + + $optionsResolver->expects($this->once()) + ->method('resolve') + ->with($givenOptions) + ->will($this->returnValue($resolvedOptions)); + + $factory = $this->getMockFormFactory(); + $builder = $this->resolvedType->createBuilder($factory, 'name', $givenOptions); + + $this->assertSame($this->resolvedType, $builder->getType()); + $this->assertSame($resolvedOptions, $builder->getOptions()); + $this->assertNull($builder->getDataClass()); + } + + public function testCreateBuilderWithDataClassOption() + { + if (version_compare(\PHPUnit_Runner_Version::id(), '3.7', '<')) { + $this->markTestSkipped('This test requires PHPUnit 3.7.'); + } + + $givenOptions = array('data_class' => 'Foo'); + $resolvedOptions = array('data_class' => '\stdClass'); + $optionsResolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + + $this->resolvedType = $this->getMockBuilder('Symfony\Component\Form\ResolvedFormType') + ->setConstructorArgs(array($this->type, array($this->extension1, $this->extension2), $this->parentResolvedType)) + ->setMethods(array('getOptionsResolver')) + ->getMock(); + + $this->resolvedType->expects($this->once()) + ->method('getOptionsResolver') + ->will($this->returnValue($optionsResolver)); + + $optionsResolver->expects($this->once()) + ->method('resolve') + ->with($givenOptions) + ->will($this->returnValue($resolvedOptions)); + + $factory = $this->getMockFormFactory(); + $builder = $this->resolvedType->createBuilder($factory, 'name', $givenOptions); + + $this->assertSame($this->resolvedType, $builder->getType()); + $this->assertSame($resolvedOptions, $builder->getOptions()); + $this->assertSame('\stdClass', $builder->getDataClass()); + } + + + public function testBuildForm() + { + if (version_compare(\PHPUnit_Runner_Version::id(), '3.7', '<')) { + $this->markTestSkipped('This test requires PHPUnit 3.7.'); + } $test = $this; $i = 0; @@ -70,169 +186,142 @@ class ResolvedFormTypeTest extends \PHPUnit_Framework_TestCase }; }; - $assertIndexAndAddOption = function ($index, $option, $default) use ($assertIndex) { - $assertIndex = $assertIndex($index); + $options = array('a' => 'Foo', 'b' => 'Bar'); + $builder = $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); - return function (OptionsResolverInterface $resolver) use ($assertIndex, $index, $option, $default) { - $assertIndex(); - - $resolver->setDefaults(array($option => $default)); - }; - }; - - // First the default options are generated for the super type - $parentType->expects($this->once()) - ->method('setDefaultOptions') - ->will($this->returnCallback($assertIndexAndAddOption(0, 'a', 'a_default'))); - - // The form type itself - $type->expects($this->once()) - ->method('setDefaultOptions') - ->will($this->returnCallback($assertIndexAndAddOption(1, 'b', 'b_default'))); - - // And its extensions - $extension1->expects($this->once()) - ->method('setDefaultOptions') - ->will($this->returnCallback($assertIndexAndAddOption(2, 'c', 'c_default'))); - - $extension2->expects($this->once()) - ->method('setDefaultOptions') - ->will($this->returnCallback($assertIndexAndAddOption(3, 'd', 'd_default'))); - - $givenOptions = array('a' => 'a_custom', 'c' => 'c_custom'); - $resolvedOptions = array('a' => 'a_custom', 'b' => 'b_default', 'c' => 'c_custom', 'd' => 'd_default'); - - // Then the form is built for the super type - $parentType->expects($this->once()) + // First the form is built for the super type + $this->parentType->expects($this->once()) ->method('buildForm') - ->with($this->anything(), $resolvedOptions) - ->will($this->returnCallback($assertIndex(4))); + ->with($builder, $options) + ->will($this->returnCallback($assertIndex(0))); // Then the type itself - $type->expects($this->once()) + $this->type->expects($this->once()) ->method('buildForm') - ->with($this->anything(), $resolvedOptions) - ->will($this->returnCallback($assertIndex(5))); + ->with($builder, $options) + ->will($this->returnCallback($assertIndex(1))); // Then its extensions - $extension1->expects($this->once()) + $this->extension1->expects($this->once()) ->method('buildForm') - ->with($this->anything(), $resolvedOptions) - ->will($this->returnCallback($assertIndex(6))); + ->with($builder, $options) + ->will($this->returnCallback($assertIndex(2))); - $extension2->expects($this->once()) + $this->extension2->expects($this->once()) ->method('buildForm') - ->with($this->anything(), $resolvedOptions) - ->will($this->returnCallback($assertIndex(7))); + ->with($builder, $options) + ->will($this->returnCallback($assertIndex(3))); - $factory = $this->getMockFormFactory(); - $builder = $resolvedType->createBuilder($factory, 'name', $givenOptions); - - $this->assertSame($resolvedType, $builder->getType()); + $this->resolvedType->buildForm($builder, $options); } public function testCreateView() { - $parentType = $this->getMockFormType(); - $type = $this->getMockFormType(); - $field1Type = $this->getMockFormType(); - $field2Type = $this->getMockFormType(); - $extension1 = $this->getMockFormTypeExtension(); - $extension2 = $this->getMockFormTypeExtension(); + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); - $parentResolvedType = new ResolvedFormType($parentType); - $resolvedType = new ResolvedFormType($type, array($extension1, $extension2), $parentResolvedType); - $field1ResolvedType = new ResolvedFormType($field1Type); - $field2ResolvedType = new ResolvedFormType($field2Type); + $view = $this->resolvedType->createView($form); + $this->assertInstanceOf('Symfony\Component\Form\FormView', $view); + $this->assertNull($view->parent); + } + + public function testCreateViewWithParent() + { + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $parentView = $this->getMock('Symfony\Component\Form\FormView'); + + $view = $this->resolvedType->createView($form, $parentView); + + $this->assertInstanceOf('Symfony\Component\Form\FormView', $view); + $this->assertSame($parentView, $view->parent); + } + + public function testBuildView() + { $options = array('a' => '1', 'b' => '2'); - $form = $this->getBuilder('name', $options) - ->setCompound(true) - ->setDataMapper($this->dataMapper) - ->setType($resolvedType) - ->add($this->getBuilder('foo')->setType($field1ResolvedType)) - ->add($this->getBuilder('bar')->setType($field2ResolvedType)) - ->getForm(); + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $view = $this->getMock('Symfony\Component\Form\FormView'); $test = $this; $i = 0; - $assertIndexAndNbOfChildViews = function ($index, $nbOfChildViews) use (&$i, $test) { - return function (FormView $view) use (&$i, $test, $index, $nbOfChildViews) { + $assertIndex = function ($index) use (&$i, $test) { + return function () use (&$i, $test, $index) { /* @var \PHPUnit_Framework_TestCase $test */ $test->assertEquals($index, $i, 'Executed at index '.$index); - $test->assertCount($nbOfChildViews, $view); ++$i; }; }; // First the super type - $parentType->expects($this->once()) + $this->parentType->expects($this->once()) ->method('buildView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(0, 0))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(0))); // Then the type itself - $type->expects($this->once()) + $this->type->expects($this->once()) ->method('buildView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(1, 0))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(1))); // Then its extensions - $extension1->expects($this->once()) + $this->extension1->expects($this->once()) ->method('buildView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(2, 0))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(2))); - $extension2->expects($this->once()) + $this->extension2->expects($this->once()) ->method('buildView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(3, 0))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(3))); - // Now the first child form - $field1Type->expects($this->once()) - ->method('buildView') - ->will($this->returnCallback($assertIndexAndNbOfChildViews(4, 0))); - $field1Type->expects($this->once()) + $this->resolvedType->buildView($view, $form, $options); + } + + public function testFinishView() + { + $options = array('a' => '1', 'b' => '2'); + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $view = $this->getMock('Symfony\Component\Form\FormView'); + + $test = $this; + $i = 0; + + $assertIndex = function ($index) use (&$i, $test) { + return function () use (&$i, $test, $index) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals($index, $i, 'Executed at index '.$index); + + ++$i; + }; + }; + + // First the super type + $this->parentType->expects($this->once()) ->method('finishView') - ->will($this->returnCallback($assertIndexAndNbOfChildViews(5, 0))); - - // And the second child form - $field2Type->expects($this->once()) - ->method('buildView') - ->will($this->returnCallback($assertIndexAndNbOfChildViews(6, 0))); - $field2Type->expects($this->once()) - ->method('finishView') - ->will($this->returnCallback($assertIndexAndNbOfChildViews(7, 0))); - - // Again first the parent - $parentType->expects($this->once()) - ->method('finishView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(8, 2))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(0))); // Then the type itself - $type->expects($this->once()) + $this->type->expects($this->once()) ->method('finishView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(9, 2))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(1))); // Then its extensions - $extension1->expects($this->once()) + $this->extension1->expects($this->once()) ->method('finishView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(10, 2))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(2))); - $extension2->expects($this->once()) + $this->extension2->expects($this->once()) ->method('finishView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(11, 2))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(3))); - $parentView = new FormView(); - $view = $resolvedType->createView($form, $parentView); - - $this->assertSame($parentView, $view->parent); + $this->resolvedType->finishView($view, $form, $options); } /** From f56c5774a8ae0717c264f61af2ec0f9aa9f125f7 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 25 Sep 2013 15:54:05 +0200 Subject: [PATCH 4/5] [HttpKernel] Extracted value exporting logic of DataCollector into a separate ValueExporter class --- .../DataCollector/DataCollector.php | 40 ++++--------- .../DataCollector/Util/ValueExporter.php | 59 +++++++++++++++++++ 2 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php index 7d9c28943b..bf2c97f8e6 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php @@ -11,17 +11,26 @@ namespace Symfony\Component\HttpKernel\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter; +use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporterInterface; + /** * DataCollector. * * Children of this class must store the collected data in the data property. * * @author Fabien Potencier + * @author Bernhard Schussek */ abstract class DataCollector implements DataCollectorInterface, \Serializable { protected $data; + /** + * @var ValueExporter + */ + private $valueExporter; + public function serialize() { return serialize($this->data); @@ -41,35 +50,10 @@ abstract class DataCollector implements DataCollectorInterface, \Serializable */ protected function varToString($var) { - if (is_object($var)) { - return sprintf('Object(%s)', get_class($var)); + if (null === $this->valueExporter) { + $this->valueExporter = new ValueExporter(); } - if (is_array($var)) { - $a = array(); - foreach ($var as $k => $v) { - $a[] = sprintf('%s => %s', $k, $this->varToString($v)); - } - - return sprintf("Array(%s)", implode(', ', $a)); - } - - if (is_resource($var)) { - return sprintf('Resource(%s)', get_resource_type($var)); - } - - if (null === $var) { - return 'null'; - } - - if (false === $var) { - return 'false'; - } - - if (true === $var) { - return 'true'; - } - - return (string) $var; + $this->valueExporter->exportValue($var); } } diff --git a/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php b/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php new file mode 100644 index 0000000000..f3aeb80cb2 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\DataCollector\Util; + +/** + * @author Bernhard Schussek + */ +class ValueExporter +{ + /** + * Converts a PHP value to a string. + * + * @param mixed $value The PHP value + * + * @return string The string representation of the given value + */ + public function exportValue($value) + { + if (is_object($value)) { + return sprintf('Object(%s)', get_class($value)); + } + + if (is_array($value)) { + $a = array(); + foreach ($value as $k => $v) { + $a[] = sprintf('%s => %s', $k, $this->exportValue($v)); + } + + return sprintf("Array(%s)", implode(', ', $a)); + } + + if (is_resource($value)) { + return sprintf('Resource(%s)', get_resource_type($value)); + } + + if (null === $value) { + return 'null'; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; + } + + return (string) $value; + } +} From 89509d9847d0665f244df5776b838bbdcd3b33d5 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 19 Sep 2013 18:16:39 +0200 Subject: [PATCH 5/5] [Form] Improved form debugger --- .../Resources/config/collectors.xml | 8 +- .../Resources/config/form_debug.xml | 10 +- .../Resources/views/Collector/form.html.twig | 339 ++++++++++++- .../views/Profiler/profiler.css.twig | 6 + .../DataCollector/Collector/FormCollector.php | 130 ----- .../DataCollector/DataCollectorExtension.php | 12 +- .../EventListener/DataCollectorListener.php | 85 ++++ .../DataCollector/FormDataCollector.php | 274 +++++++++++ .../FormDataCollectorInterface.php | 102 ++++ .../DataCollector/FormDataExtractor.php | 135 +++++ .../FormDataExtractorInterface.php | 60 +++ .../Proxy/ResolvedTypeDataCollectorProxy.php | 148 ++++++ .../ResolvedTypeFactoryDataCollectorProxy.php | 55 +++ .../Type/DataCollectorTypeExtension.php | 17 +- .../Collector/FormCollectorTest.php | 58 --- .../DataCollectorExtensionTest.php | 10 +- .../DataCollector/FormDataCollectorTest.php | 465 ++++++++++++++++++ .../DataCollector/FormDataExtractorTest.php | 338 +++++++++++++ .../Type/DataCollectorTypeExtensionTest.php | 13 +- 19 files changed, 2023 insertions(+), 242 deletions(-) delete mode 100644 src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php create mode 100644 src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php delete mode 100644 src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml index ee956f0fa0..7eb472fd97 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml @@ -13,7 +13,8 @@ Symfony\Component\HttpKernel\DataCollector\TimeDataCollector Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector - Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector + Symfony\Component\Form\Extension\DataCollector\FormDataCollector + Symfony\Component\Form\Extension\DataCollector\FormDataExtractor @@ -55,8 +56,11 @@ - + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml index b0c0e3ff65..5d4faac4ac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml @@ -5,11 +5,19 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + Symfony\Component\Form\Extension\DataCollector\Proxy\ResolvedTypeFactoryDataCollectorProxy Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension - + + + + + + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig index ab9db1f28e..ecc0a42633 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -1,10 +1,12 @@ {% extends '@WebProfiler/Profiler/layout.html.twig' %} +{% from _self import form_tree_entry, form_tree_details %} + {% block toolbar %} {% if collector.data|length %} {% set icon %} Forms - {% if collector.errorCount %}{{ collector.errorCount }}{% else %}{{ collector.data|length }}{% endif %} + {% if collector.data.nb_errors %}{{ collector.data.nb_errors }}{% else %}{{ collector.data.forms|length }}{% endif %} {% endset %} {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %} @@ -13,48 +15,331 @@ {% block menu %} - + Forms - {% if collector.data|length %} - {{ collector.data|length }} + {% if collector.data.forms|length %} + {{ collector.data.forms|length }} {% endif %} {% endblock %} {% block panel %} -

Form{% if collector.data|length > 1 %}s{% endif %}

+ + + {% if collector.data.forms|length %} +
+
+

Forms

+ +
    + {% for formName, formData in collector.data.forms %} + {{ form_tree_entry(formName, formData) }} + {% endfor %} +
+
+ + {% for formName, formData in collector.data.forms %} + {{ form_tree_details(formName, formData) }} + {% endfor %} +
+ {% else %} +

No forms were submitted for this request.

+ {% endif %} + + +{% endblock %} + +{% macro form_tree_entry(name, data) %} +
  • + {{ name }} + + {% if data.children|length > 1 %} +
      + {% for childName, childData in data.children %} + {{ _self.form_tree_entry(childName, childData) }} + {% endfor %} +
    + {% endif %} +
  • +{% endmacro %} + +{% macro form_tree_details(name, data) %} +
    +

    + {{ name }} + {% if data.type_class is defined %} + [{{ data.type }}] + {% endif %} +

    + + {% if data.errors is defined and data.errors|length > 0 %} +

    Errors

    - {% for formName, fields in collector.data %} -

    {{ formName }}

    - {% if fields %} - - - - + + - {% for fieldName, field in fields %} + {% for error in data.errors %} + + + + + {% endfor %} +
    FieldTypeValueMessagesMessageCause
    {{ error.message }}Unknown.
    + {% endif %} + + {% if data.default_data is defined %} +

    Default Data

    + + + + + + + + + + + + + + +
    Model Format + {% if data.default_data.model is defined %} +
    {{ data.default_data.model }}
    + {% else %} + same as normalized format + {% endif %} +
    Normalized Format
    {{ data.default_data.norm }}
    View Format + {% if data.default_data.view is defined %} +
    {{ data.default_data.view }}
    + {% else %} + same as normalized format + {% endif %} +
    + {% endif %} + + {% if data.submitted_data is defined %} +

    Submitted Data

    + + {% if data.submitted_data.norm is defined %} + - - - + + + + + + + + + +
    {{ fieldName }}{{ field.type }}{{ field.value }}View Format -
      - {% for errorMessage in field.errors %} -
    • - {{ errorMessage.message }} -
    • - {% endfor %} -
    + {% if data.submitted_data.view is defined %} +
    {{ data.submitted_data.view }}
    + {% else %} + same as normalized format + {% endif %}
    Normalized Format
    {{ data.submitted_data.norm }}
    Model Format + {% if data.submitted_data.model is defined %} +
    {{ data.submitted_data.model }}
    + {% else %} + same as normalized format + {% endif %} +
    + {% else %} +

    This form was not submitted.

    + {% endif %} + {% endif %} + + {% if data.passed_options is defined %} +

    Passed Options

    + + {% if data.passed_options|length %} + + + + + + + {% for option, value in data.passed_options %} + + + + + {% endfor %}
    OptionPassed ValueResolved Value
    {{ option }}
    {{ value }}
    + {% if data.resolved_options[option] is sameas(value) %} + same as passed value + {% else %} +
    {{ data.resolved_options[option] }}
    + {% endif %} +
    {% else %} - This form is valid. +

    No options where passed when constructing this form.

    {% endif %} - {% else %} - No forms were submitted for this request. + {% endif %} + + {% if data.resolved_options is defined %} +

    Resolved Options

    + + + + + + + {% for option, value in data.resolved_options %} + + + + + {% endfor %} +
    OptionValue
    {{ option }}
    {{ value }}
    + {% endif %} + +

    View Variables

    + + + + + + + {% for variable, value in data.view_vars %} + + + + + {% endfor %} +
    VariableValue
    {{ variable }}
    {{ value }}
    +
    + + {% for childName, childData in data.children %} + {{ _self.form_tree_details(childName, childData) }} {% endfor %} -{% endblock %} +{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 7f10150c72..3b45ca1116 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -63,6 +63,9 @@ table th, table td { font-size: 12px; padding: 8px 10px; } +table td em { + color: #aaa; +} fieldset { border: none; } @@ -70,6 +73,9 @@ abbr { border-bottom: 1px dotted #000; cursor: help; } +pre, code { + font-size: 0.9em; +} .clear { clear: both; height: 0; diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php b/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php deleted file mode 100644 index 82564e4068..0000000000 --- a/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php +++ /dev/null @@ -1,130 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Extension\DataCollector\Collector; - -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\DataCollector\DataCollector as BaseCollector; - -/** - * DataCollector for Form Validation. - * - * @author Robert Schönthal - */ -class FormCollector extends BaseCollector implements EventSubscriberInterface -{ - /** - * {@inheritDoc} - */ - public static function getSubscribedEvents() - { - return array(FormEvents::POST_SUBMIT => array('collectForm', -255)); - } - - /** - * {@inheritDoc} - */ - public function collect(Request $request, Response $response, \Exception $exception = null) - { - //nothing to do, everything is added with addError() - } - - /** - * Collects Form-Validation-Data and adds them to the Collector. - * - * @param FormEvent $event The event object - */ - public function collectForm(FormEvent $event) - { - $form = $event->getForm(); - - if ($form->isRoot()) { - $this->data[$form->getName()] = array(); - $this->addForm($form); - } - } - - /** - * Adds an Form-Element to the Collector. - * - * @param FormInterface $form - */ - private function addForm(FormInterface $form) - { - if ($form->getErrors()) { - $this->addError($form); - } - - // recursively add all child-errors - foreach ($form->all() as $field) { - $this->addForm($field); - } - } - - /** - * Adds a Form-Error to the Collector. - * - * @param FormInterface $form - */ - private function addError(FormInterface $form) - { - $storeData = array( - 'root' => $form->getRoot()->getName(), - 'name' => (string) $form->getPropertyPath(), - 'type' => $form->getConfig()->getType()->getName(), - 'errors' => $form->getErrors(), - 'value' => $this->varToString($form->getViewData()) - ); - - $this->data[$storeData['root']][$storeData['name']] = $storeData; - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return 'form'; - } - - /** - * Returns all collected Data. - * - * @return array - */ - public function getData() - { - return $this->data; - } - - /** - * Returns the number of Forms with Errors. - * - * @return integer - */ - public function getErrorCount() - { - $errorCount = 0; - - foreach ($this->data as $form) { - if (count($form)) { - $errorCount++; - } - } - - return $errorCount; - } -} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php index f9f8209188..941bd2102e 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php @@ -15,20 +15,22 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\AbstractExtension; /** - * DataCollectorExtension for collecting Form Validation Failures. + * Extension for collecting data of the forms on a page. * + * @since 2.4 * @author Robert Schönthal + * @author Bernhard Schussek */ class DataCollectorExtension extends AbstractExtension { /** * @var EventSubscriberInterface */ - private $eventSubscriber; + private $dataCollector; - public function __construct(EventSubscriberInterface $eventSubscriber) + public function __construct(FormDataCollectorInterface $dataCollector) { - $this->eventSubscriber = $eventSubscriber; + $this->dataCollector = $dataCollector; } /** @@ -37,7 +39,7 @@ class DataCollectorExtension extends AbstractExtension protected function loadTypeExtensions() { return array( - new Type\DataCollectorTypeExtension($this->eventSubscriber) + new Type\DataCollectorTypeExtension($this->dataCollector) ); } } diff --git a/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php new file mode 100644 index 0000000000..4822989b58 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * Listener that invokes a data collector for the {@link FormEvents::POST_SET_DATA} + * and {@link FormEvents::POST_SUBMIT} events. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class DataCollectorListener implements EventSubscriberInterface +{ + /** + * @var FormDataCollectorInterface + */ + private $dataCollector; + + public function __construct(FormDataCollectorInterface $dataCollector) + { + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritDoc} + */ + public static function getSubscribedEvents() + { + return array( + // High priority in order to be called as soon as possible + FormEvents::POST_SET_DATA => array('postSetData', 255), + // Low priority in order to be called as late as possible + FormEvents::POST_SUBMIT => array('postSubmit', -255), + ); + } + + /** + * Listener for the {@link FormEvents::POST_SET_DATA} event. + * + * @param FormEvent $event The event object + */ + public function postSetData(FormEvent $event) + { + if ($event->getForm()->isRoot()) { + // Collect basic information about each form + $this->dataCollector->collectConfiguration($event->getForm()); + + // Collect the default data + $this->dataCollector->collectDefaultData($event->getForm()); + } + } + + /** + * Listener for the {@link FormEvents::POST_SUBMIT} event. + * + * @param FormEvent $event The event object + */ + public function postSubmit(FormEvent $event) + { + if ($event->getForm()->isRoot()) { + // Collect the submitted data of each form + $this->dataCollector->collectSubmittedData($event->getForm()); + + // Assemble a form tree + // This is done again in collectViewVariables(), but that method + // is not guaranteed to be called (i.e. when no view is created) + $this->dataCollector->buildPreliminaryFormTree($event->getForm()); + } + } + +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php new file mode 100644 index 0000000000..543d6c5b04 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php @@ -0,0 +1,274 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; + +/** + * Data collector for {@link \Symfony\Component\Form\FormInterface} instances. + * + * @since 2.4 + * @author Robert Schönthal + * @author Bernhard Schussek + */ +class FormDataCollector extends DataCollector implements FormDataCollectorInterface +{ + /** + * @var FormDataExtractor + */ + private $dataExtractor; + + /** + * Stores the collected data per {@link FormInterface} instance. + * + * Uses the hashes of the forms as keys. This is preferrable over using + * {@link \SplObjectStorage}, because in this way no references are kept + * to the {@link FormInterface} instances. + * + * @var array + */ + private $dataByForm; + + /** + * Stores the collected data per {@link FormView} instance. + * + * Uses the hashes of the views as keys. This is preferrable over using + * {@link \SplObjectStorage}, because in this way no references are kept + * to the {@link FormView} instances. + * + * @var array + */ + private $dataByView; + + /** + * Connects {@link FormView} with {@link FormInterface} instances. + * + * Uses the hashes of the views as keys and the hashes of the forms as + * values. This is preferrable over storing the objects directly, because + * this way they can safely be discarded by the GC. + * + * @var array + */ + private $formsByView; + + public function __construct(FormDataExtractorInterface $dataExtractor) + { + $this->dataExtractor = $dataExtractor; + $this->data = array( + 'forms' => array(), + 'nb_errors' => 0, + ); + } + + /** + * Does nothing. The data is collected during the form event listeners. + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + } + + /** + * {@inheritdoc} + */ + public function associateFormWithView(FormInterface $form, FormView $view) + { + $this->formsByView[spl_object_hash($view)] = spl_object_hash($form); + } + + /** + * {@inheritdoc} + */ + public function collectConfiguration(FormInterface $form) + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + $this->dataByForm[$hash] = array(); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractConfiguration($form) + ); + + foreach ($form as $child) { + $this->collectConfiguration($child); + } + } + + /** + * {@inheritdoc} + */ + public function collectDefaultData(FormInterface $form) + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + $this->dataByForm[$hash] = array(); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractDefaultData($form) + ); + + foreach ($form as $child) { + $this->collectDefaultData($child); + } + } + + /** + * {@inheritdoc} + */ + public function collectSubmittedData(FormInterface $form) + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + $this->dataByForm[$hash] = array(); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractSubmittedData($form) + ); + + // Count errors + if (isset($this->dataByForm[$hash]['errors'])) { + $this->data['nb_errors'] += count($this->dataByForm[$hash]['errors']); + } + + foreach ($form as $child) { + $this->collectSubmittedData($child); + } + } + + /** + * {@inheritdoc} + */ + public function collectViewVariables(FormView $view) + { + $hash = spl_object_hash($view); + + if (!isset($this->dataByView[$hash])) { + $this->dataByView[$hash] = array(); + } + + $this->dataByView[$hash] = array_replace( + $this->dataByView[$hash], + $this->dataExtractor->extractViewVariables($view) + ); + + foreach ($view->children as $child) { + $this->collectViewVariables($child); + } + } + + /** + * {@inheritdoc} + */ + public function buildPreliminaryFormTree(FormInterface $form) + { + $this->data['forms'][$form->getName()] = array(); + + $this->recursiveBuildPreliminaryFormTree($form, $this->data['forms'][$form->getName()]); + } + + /** + * {@inheritdoc} + */ + public function buildFinalFormTree(FormInterface $form, FormView $view) + { + $this->data['forms'][$form->getName()] = array(); + + $this->recursiveBuildFinalFormTree($form, $view, $this->data['forms'][$form->getName()]); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'form'; + } + + /** + * {@inheritdoc} + */ + public function getData() + { + return $this->data; + } + + private function recursiveBuildPreliminaryFormTree(FormInterface $form, &$output = null) + { + $hash = spl_object_hash($form); + + $output = isset($this->dataByForm[$hash]) + ? $this->dataByForm[$hash] + : array(); + + $output['children'] = array(); + + foreach ($form as $name => $child) { + $output['children'][$name] = array(); + + $this->recursiveBuildPreliminaryFormTree($child, $output['children'][$name]); + } + } + + private function recursiveBuildFinalFormTree(FormInterface $form = null, FormView $view, &$output = null) + { + $viewHash = spl_object_hash($view); + $formHash = null; + + if (null !== $form) { + $formHash = spl_object_hash($form); + } elseif (isset($this->formsByView[$viewHash])) { + // The FormInterface instance of the CSRF token is never contained in + // the FormInterface tree of the form, so we need to get the + // corresponding FormInterface instance for its view in a different way + $formHash = $this->formsByView[$viewHash]; + } + + $output = isset($this->dataByView[$viewHash]) + ? $this->dataByView[$viewHash] + : array(); + + if (null !== $formHash) { + $output = array_replace( + $output, + isset($this->dataByForm[$formHash]) + ? $this->dataByForm[$formHash] + : array() + ); + } + + $output['children'] = array(); + + foreach ($view->children as $name => $childView) { + // The CSRF token, for example, is never added to the form tree. + // It is only present in the view. + $childForm = null !== $form && $form->has($name) + ? $form->get($name) + : null; + + $output['children'][$name] = array(); + + $this->recursiveBuildFinalFormTree($childForm, $childView, $output['children'][$name]); + } + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php new file mode 100644 index 0000000000..9a805029f8 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; + +/** + * Collects and structures information about forms. + * + * @since 2.4 + * @author Bernhard Schussek + */ +interface FormDataCollectorInterface extends DataCollectorInterface +{ + /** + * Stores configuration data of the given form and its children. + * + * @param FormInterface $form A root form + */ + public function collectConfiguration(FormInterface $form); + + /** + * Stores the default data of the given form and its children. + * + * @param FormInterface $form A root form + */ + public function collectDefaultData(FormInterface $form); + + /** + * Stores the submitted data of the given form and its children. + * + * @param FormInterface $form A root form + */ + public function collectSubmittedData(FormInterface $form); + + /** + * Stores the view variables of the given form view and its children. + * + * @param FormView $view A root form view + */ + public function collectViewVariables(FormView $view); + + /** + * Specifies that the given objects represent the same conceptual form. + * + * @param FormInterface $form A form object + * @param FormView $view A view object + */ + public function associateFormWithView(FormInterface $form, FormView $view); + + /** + * Assembles the data collected about the given form and its children as + * a tree-like data structure. + * + * The result can be queried using {@link getData()}. + * + * @param FormInterface $form A root form + */ + public function buildPreliminaryFormTree(FormInterface $form); + + /** + * Assembles the data collected about the given form and its children as + * a tree-like data structure. + * + * The result can be queried using {@link getData()}. + * + * Contrary to {@link buildPreliminaryFormTree()}, a {@link FormView} + * object has to be passed. The tree structure of this view object will be + * used for structuring the resulting data. That means, if a child is + * present in the view, but not in the form, it will be present in the final + * data array anyway. + * + * When {@link FormView} instances are present in the view tree, for which + * no corresponding {@link FormInterface} objects can be found in the form + * tree, only the view data will be included in the result. If a + * corresponding {@link FormInterface} exists otherwise, call + * {@link associateFormWithView()} before calling this method. + * + * @param FormInterface $form A root form + * @param FormView $view A root view + */ + public function buildFinalFormTree(FormInterface $form, FormView $view); + + /** + * Returns all collected data. + * + * @return array + */ + public function getData(); + +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php new file mode 100644 index 0000000000..ad401796bd --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter; + +/** + * Default implementation of {@link FormDataExtractorInterface}. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class FormDataExtractor implements FormDataExtractorInterface +{ + /** + * @var ValueExporter + */ + private $valueExporter; + + /** + * Constructs a new data extractor. + */ + public function __construct(ValueExporter $valueExporter = null) + { + $this->valueExporter = $valueExporter ?: new ValueExporter(); + } + + /** + * {@inheritdoc} + */ + public function extractConfiguration(FormInterface $form) + { + $data = array( + 'type' => $form->getConfig()->getType()->getName(), + 'type_class' => get_class($form->getConfig()->getType()->getInnerType()), + 'synchronized' => $this->valueExporter->exportValue($form->isSynchronized()), + 'passed_options' => array(), + 'resolved_options' => array(), + ); + + foreach ($form->getConfig()->getAttribute('data_collector/passed_options', array()) as $option => $value) { + $data['passed_options'][$option] = $this->valueExporter->exportValue($value); + } + + foreach ($form->getConfig()->getOptions() as $option => $value) { + $data['resolved_options'][$option] = $this->valueExporter->exportValue($value); + } + + ksort($data['passed_options']); + ksort($data['resolved_options']); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function extractDefaultData(FormInterface $form) + { + $data = array( + 'default_data' => array( + 'norm' => $this->valueExporter->exportValue($form->getNormData()), + ), + 'submitted_data' => array(), + ); + + if ($form->getData() !== $form->getNormData()) { + $data['default_data']['model'] = $this->valueExporter->exportValue($form->getData()); + } + + if ($form->getViewData() !== $form->getNormData()) { + $data['default_data']['view'] = $this->valueExporter->exportValue($form->getViewData()); + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function extractSubmittedData(FormInterface $form) + { + $data = array( + 'submitted_data' => array( + 'norm' => $this->valueExporter->exportValue($form->getNormData()), + ), + 'errors' => array(), + ); + + if ($form->getViewData() !== $form->getNormData()) { + $data['submitted_data']['view'] = $this->valueExporter->exportValue($form->getViewData()); + } + + if ($form->getData() !== $form->getNormData()) { + $data['submitted_data']['model'] = $this->valueExporter->exportValue($form->getData()); + } + + foreach ($form->getErrors() as $error) { + $data['errors'][] = array( + 'message' => $error->getMessage(), + ); + } + + $data['synchronized'] = $this->valueExporter->exportValue($form->isSynchronized()); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function extractViewVariables(FormView $view) + { + $data = array(); + + foreach ($view->vars as $varName => $value) { + $data['view_vars'][$varName] = $this->valueExporter->exportValue($value); + } + + ksort($data['view_vars']); + + return $data; + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php new file mode 100644 index 0000000000..d47496552d --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; + +/** + * Extracts arrays of information out of forms. + * + * @since 2.4 + * @author Bernhard Schussek + */ +interface FormDataExtractorInterface +{ + /** + * Extracts the configuration data of a form. + * + * @param FormInterface $form The form + * + * @return array Information about the form's configuration + */ + public function extractConfiguration(FormInterface $form); + + /** + * Extracts the default data of a form. + * + * @param FormInterface $form The form + * + * @return array Information about the form's default data + */ + public function extractDefaultData(FormInterface $form); + + /** + * Extracts the submitted data of a form. + * + * @param FormInterface $form The form + * + * @return array Information about the form's submitted data + */ + public function extractSubmittedData(FormInterface $form); + + /** + * Extracts the view variables of a form. + * + * @param FormView $view The form view + * + * @return array Information about the view's variables + */ + public function extractViewVariables(FormView $view); +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php new file mode 100644 index 0000000000..960048a0c2 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\Proxy; + +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\ResolvedFormTypeInterface; + +/** + * Proxy that invokes a data collector when creating a form and its view. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface +{ + /** + * @var ResolvedFormTypeInterface + */ + private $proxiedType; + + /** + * @var FormDataCollectorInterface + */ + private $dataCollector; + + public function __construct(ResolvedFormTypeInterface $proxiedType, FormDataCollectorInterface $dataCollector) + { + $this->proxiedType = $proxiedType; + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->proxiedType->getName(); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return $this->proxiedType->getParent(); + } + + /** + * {@inheritdoc} + */ + public function getInnerType() + { + return $this->proxiedType->getInnerType(); + } + + /** + * {@inheritdoc} + */ + public function getTypeExtensions() + { + return $this->proxiedType->getTypeExtensions(); + } + + /** + * {@inheritdoc} + */ + public function createBuilder(FormFactoryInterface $factory, $name, array $options = array()) + { + $builder = $this->proxiedType->createBuilder($factory, $name, $options); + + $builder->setAttribute('data_collector/passed_options', $options); + $builder->setType($this); + + return $builder; + } + + /** + * {@inheritdoc} + */ + public function createView(FormInterface $form, FormView $parent = null) + { + return $this->proxiedType->createView($form, $parent); + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $this->proxiedType->buildForm($builder, $options); + } + + /** + * {@inheritdoc} + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $this->proxiedType->buildView($view, $form, $options); + } + + /** + * {@inheritdoc} + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $this->proxiedType->finishView($view, $form, $options); + + // Remember which view belongs to which form instance, so that we can + // get the collected data for a view when its form instance is not + // available (e.g. CSRF token) + $this->dataCollector->associateFormWithView($form, $view); + + // Since the CSRF token is only present in the FormView tree, we also + // need to check the FormView tree instead of calling isRoot() on the + // FormInterface tree + if (null === $view->parent) { + $this->dataCollector->collectViewVariables($view); + + // Re-assemble data, in case FormView instances were added, for + // which no FormInterface instances were present (e.g. CSRF token). + // Since finishView() is called after finishing the views of all + // children, we can safely assume that information has been + // collected about the complete form tree. + $this->dataCollector->buildFinalFormTree($form, $view); + } + } + + /** + * {@inheritdoc} + */ + public function getOptionsResolver() + { + return $this->proxiedType->getOptionsResolver(); + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php new file mode 100644 index 0000000000..f15b720585 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\Proxy; + +use Symfony\Component\Form\Exception; +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\Form\ResolvedFormTypeFactoryInterface; +use Symfony\Component\Form\ResolvedFormTypeInterface; + +/** + * Proxy that wraps resolved types into {@link ResolvedTypeDataCollectorProxy} + * instances. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface +{ + /** + * @var ResolvedFormTypeFactoryInterface + */ + private $proxiedFactory; + + /** + * @var FormDataCollectorInterface + */ + private $dataCollector; + + public function __construct(ResolvedFormTypeFactoryInterface $proxiedFactory, FormDataCollectorInterface $dataCollector) + { + $this->proxiedFactory = $proxiedFactory; + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritdoc} + */ + public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ResolvedFormTypeInterface $parent = null) + { + return new ResolvedTypeDataCollectorProxy( + $this->proxiedFactory->createResolvedType($type, $typeExtensions, $parent), + $this->dataCollector + ); + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php index 8bc849b74b..1b5cfb695d 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php @@ -11,25 +11,28 @@ namespace Symfony\Component\Form\Extension\DataCollector\Type; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener; +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; use Symfony\Component\Form\FormBuilderInterface; /** - * DataCollector Type Extension for collecting invalid Forms. + * Type extension for collecting data of a form with this type. * + * @since 2.4 * @author Robert Schönthal + * @author Bernhard Schussek */ class DataCollectorTypeExtension extends AbstractTypeExtension { /** - * @var EventSubscriberInterface + * @var \Symfony\Component\EventDispatcher\EventSubscriberInterface */ - private $eventSubscriber; + private $listener; - public function __construct(EventSubscriberInterface $eventSubscriber) + public function __construct(FormDataCollectorInterface $dataCollector) { - $this->eventSubscriber = $eventSubscriber; + $this->listener = new DataCollectorListener($dataCollector); } /** @@ -37,7 +40,7 @@ class DataCollectorTypeExtension extends AbstractTypeExtension */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->addEventSubscriber($this->eventSubscriber); + $builder->addEventSubscriber($this->listener); } /** diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php deleted file mode 100644 index 20bf9878df..0000000000 --- a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Tests\Extension\DataCollector\Collector; - -use Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; - -class FormCollectorTest extends \PHPUnit_Framework_TestCase -{ - public function testSubscribedEvents() - { - $events = FormCollector::getSubscribedEvents(); - - $this->assertInternalType('array', $events); - $this->assertEquals(array(FormEvents::POST_SUBMIT => array('collectForm', -255)), $events); - } - - public function testCollect() - { - $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); - $subForm = $this->getMock('Symfony\Component\Form\Test\FormInterface'); - - $type = $this->getMock('Symfony\Component\Form\FormTypeInterface'); - $type->expects($this->atLeastOnce())->method('getName')->will($this->returnValue('fizz')); - - $config = $this->getMock('Symfony\Component\Form\FormConfigInterface'); - $config->expects($this->atLeastOnce())->method('getType')->will($this->returnValue($type)); - - $form->expects($this->atLeastOnce())->method('all')->will($this->returnValue(array($subForm))); - $form->expects($this->atLeastOnce())->method('isRoot')->will($this->returnValue(true)); - $form->expects($this->atLeastOnce())->method('getName')->will($this->returnValue('foo')); - - $subForm->expects($this->atLeastOnce())->method('all')->will($this->returnValue(array())); - $subForm->expects($this->atLeastOnce())->method('getErrors')->will($this->returnValue(array('foo'))); - $subForm->expects($this->atLeastOnce())->method('getRoot')->will($this->returnValue($form)); - $subForm->expects($this->atLeastOnce())->method('getConfig')->will($this->returnValue($config)); - $subForm->expects($this->atLeastOnce())->method('getPropertyPath')->will($this->returnValue('bar')); - $subForm->expects($this->atLeastOnce())->method('getViewData')->will($this->returnValue('bazz')); - - $event = new FormEvent($form, array()); - $c = new FormCollector(); - $c->collectForm($event); - - $this->assertInternalType('array', $c->getData()); - $this->assertEquals(1, $c->getErrorCount()); - $this->assertEquals(array('foo' => array('bar' => array('value' => 'bazz', 'root' => 'foo', 'type' => 'fizz', 'name' => 'bar', 'errors' => array('foo')))), $c->getData()); - } -} - \ No newline at end of file diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php index 14ab0eac1b..9f5991c105 100644 --- a/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Form\Tests\Extension\DataCollector; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Extension\DataCollector\DataCollectorExtension; /** @@ -25,14 +24,14 @@ class DataCollectorExtensionTest extends \PHPUnit_Framework_TestCase private $extension; /** - * @var EventSubscriberInterface + * @var \PHPUnit_Framework_MockObject_MockObject */ - private $eventSubscriber; + private $dataCollector; public function setUp() { - $this->eventSubscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface'); - $this->extension = new DataCollectorExtension($this->eventSubscriber); + $this->dataCollector = $this->getMock('Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface'); + $this->extension = new DataCollectorExtension($this->dataCollector); } public function testLoadTypeExtensions() @@ -44,4 +43,3 @@ class DataCollectorExtensionTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf('Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension', array_shift($typeExtensions)); } } - \ No newline at end of file diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php new file mode 100644 index 0000000000..c31f62c7e0 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php @@ -0,0 +1,465 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\DataCollector; + +use Symfony\Component\Form\Extension\DataCollector\FormDataCollector; +use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormView; + +class FormDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataExtractor; + + /** + * @var FormDataCollector + */ + private $dataCollector; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dispatcher; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataMapper; + + /** + * @var Form + */ + private $form; + + /** + * @var Form + */ + private $childForm; + + /** + * @var FormView + */ + private $view; + + /** + * @var FormView + */ + private $childView; + + + protected function setUp() + { + $this->dataExtractor = $this->getMock('Symfony\Component\Form\Extension\DataCollector\FormDataExtractorInterface'); + $this->dataCollector = new FormDataCollector($this->dataExtractor); + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + $this->dataMapper = $this->getMock('Symfony\Component\Form\DataMapperInterface'); + $this->form = $this->createForm('name'); + $this->childForm = $this->createForm('child'); + $this->view = new FormView(); + $this->childView = new FormView(); + } + + public function testBuildPreliminaryFormTree() + { + $this->form->add($this->childForm); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($this->childForm) + ->will($this->returnValue(array('default_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(4)) + ->method('extractSubmittedData') + ->with($this->form) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + $this->dataExtractor->expects($this->at(5)) + ->method('extractSubmittedData') + ->with($this->childForm) + ->will($this->returnValue(array('submitted_data' => 'bar'))); + + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectDefaultData($this->form); + $this->dataCollector->collectSubmittedData($this->form); + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + 'children' => array( + 'child' => array( + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar', + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testBuildMultiplePreliminaryFormTrees() + { + $form1 = $this->createForm('form1'); + $form2 = $this->createForm('form2'); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($form1) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($form2) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataCollector->collectConfiguration($form1); + $this->dataCollector->collectConfiguration($form2); + $this->dataCollector->buildPreliminaryFormTree($form1); + + $this->assertSame(array( + 'forms' => array( + 'form1' => array( + 'config' => 'foo', + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + + $this->dataCollector->buildPreliminaryFormTree($form2); + + $this->assertSame(array( + 'forms' => array( + 'form1' => array( + 'config' => 'foo', + 'children' => array(), + ), + 'form2' => array( + 'config' => 'bar', + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testBuildSamePreliminaryFormTreeMultipleTimes() + { + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + + $this->dataExtractor->expects($this->at(1)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + + $this->dataCollector->collectDefaultData($this->form); + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'default_data' => 'foo', + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testBuildPreliminaryFormTreeWithoutCollectingAnyData() + { + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testBuildFinalFormTree() + { + $this->form->add($this->childForm); + $this->view->children['child'] = $this->childView; + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($this->childForm) + ->will($this->returnValue(array('default_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(4)) + ->method('extractSubmittedData') + ->with($this->form) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + $this->dataExtractor->expects($this->at(5)) + ->method('extractSubmittedData') + ->with($this->childForm) + ->will($this->returnValue(array('submitted_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(6)) + ->method('extractViewVariables') + ->with($this->view) + ->will($this->returnValue(array('view_vars' => 'foo'))); + + $this->dataExtractor->expects($this->at(7)) + ->method('extractViewVariables') + ->with($this->childView) + ->will($this->returnValue(array('view_vars' => 'bar'))); + + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectDefaultData($this->form); + $this->dataCollector->collectSubmittedData($this->form); + $this->dataCollector->collectViewVariables($this->view); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'view_vars' => 'foo', + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + 'children' => array( + 'child' => array( + 'view_vars' => 'bar', + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar', + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testFinalFormReliesOnFormViewStructure() + { + $this->form->add($this->createForm('first')); + $this->form->add($this->createForm('second')); + + $this->view->children['second'] = $this->childView; + + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'children' => array( + 'first' => array( + 'children' => array(), + ), + 'second' => array( + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'children' => array( + // "first" not present in FormView + 'second' => array( + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testChildViewsCanBeWithoutCorrespondingChildForms() + { + // don't add $this->childForm to $this->form! + + $this->view->children['child'] = $this->childView; + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + // explicitly call collectConfiguration(), since $this->childForm is not + // contained in the form tree + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectConfiguration($this->childForm); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'children' => array( + 'child' => array( + // no "config" key + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testChildViewsWithoutCorrespondingChildFormsMayBeExplicitlyAssociated() + { + // don't add $this->childForm to $this->form! + + $this->view->children['child'] = $this->childView; + + // but associate the two + $this->dataCollector->associateFormWithView($this->childForm, $this->childView); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + // explicitly call collectConfiguration(), since $this->childForm is not + // contained in the form tree + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectConfiguration($this->childForm); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'children' => array( + 'child' => array( + 'config' => 'bar', + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testCollectSubmittedDataCountsErrors() + { + $form1 = $this->createForm('form1'); + $childForm1 = $this->createForm('child1'); + $form2 = $this->createForm('form2'); + + $form1->add($childForm1); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractSubmittedData') + ->with($form1) + ->will($this->returnValue(array('errors' => array('foo')))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractSubmittedData') + ->with($childForm1) + ->will($this->returnValue(array('errors' => array('bar', 'bam')))); + $this->dataExtractor->expects($this->at(2)) + ->method('extractSubmittedData') + ->with($form2) + ->will($this->returnValue(array('errors' => array('baz')))); + + $this->dataCollector->collectSubmittedData($form1); + + $data = $this->dataCollector->getData(); + $this->assertSame(3, $data['nb_errors']); + + $this->dataCollector->collectSubmittedData($form2); + + $data = $this->dataCollector->getData(); + $this->assertSame(4, $data['nb_errors']); + + } + + private function createForm($name) + { + $builder = new FormBuilder($name, null, $this->dispatcher, $this->factory); + $builder->setCompound(true); + $builder->setDataMapper($this->dataMapper); + + return $builder->getForm(); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php new file mode 100644 index 0000000000..d9c1bb3180 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php @@ -0,0 +1,338 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\DataCollector; + +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\DataCollector\FormDataExtractor; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer; +use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter; + +class FormDataExtractorTest_SimpleValueExporter extends ValueExporter +{ + /** + * {@inheritdoc} + */ + public function exportValue($value) + { + return var_export($value, true); + } +} + +/** + * @author Bernhard Schussek + */ +class FormDataExtractorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var FormDataExtractorTest_SimpleValueExporter + */ + private $valueExporter; + + /** + * @var FormDataExtractor + */ + private $dataExtractor; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dispatcher; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + protected function setUp() + { + $this->valueExporter = new FormDataExtractorTest_SimpleValueExporter(); + $this->dataExtractor = new FormDataExtractor($this->valueExporter); + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + } + + public function testExtractConfiguration() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $form = $this->createBuilder('name') + ->setType($type) + ->getForm(); + + $this->assertSame(array( + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array(), + 'resolved_options' => array(), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractConfigurationSortsPassedOptions() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $options = array( + 'b' => 'foo', + 'a' => 'bar', + 'c' => 'baz', + ); + + $form = $this->createBuilder('name') + ->setType($type) + // passed options are stored in an attribute by + // ResolvedTypeDataCollectorProxy + ->setAttribute('data_collector/passed_options', $options) + ->getForm(); + + $this->assertSame(array( + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array( + 'a' => "'bar'", + 'b' => "'foo'", + 'c' => "'baz'", + ), + 'resolved_options' => array(), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractConfigurationSortsResolvedOptions() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $options = array( + 'b' => 'foo', + 'a' => 'bar', + 'c' => 'baz', + ); + + $form = $this->createBuilder('name', $options) + ->setType($type) + ->getForm(); + + $this->assertSame(array( + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array(), + 'resolved_options' => array( + 'a' => "'bar'", + 'b' => "'foo'", + 'c' => "'baz'", + ), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractDefaultData() + { + $form = $this->createBuilder('name')->getForm(); + + $form->setData('Foobar'); + + $this->assertSame(array( + 'default_data' => array( + 'norm' => "'Foobar'", + ), + 'submitted_data' => array(), + ), $this->dataExtractor->extractDefaultData($form)); + } + + public function testExtractDefaultDataStoresModelDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addModelTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar' + ))) + ->getForm(); + + $form->setData('Foo'); + + $this->assertSame(array( + 'default_data' => array( + 'norm' => "'Bar'", + 'model' => "'Foo'", + ), + 'submitted_data' => array(), + ), $this->dataExtractor->extractDefaultData($form)); + } + + public function testExtractDefaultDataStoresViewDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addViewTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar' + ))) + ->getForm(); + + $form->setData('Foo'); + + $this->assertSame(array( + 'default_data' => array( + 'norm' => "'Foo'", + 'view' => "'Bar'", + ), + 'submitted_data' => array(), + ), $this->dataExtractor->extractDefaultData($form)); + } + + public function testExtractSubmittedData() + { + $form = $this->createBuilder('name')->getForm(); + + $form->submit('Foobar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + ), + 'errors' => array(), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresModelDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addModelTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar', + '' => '', + ))) + ->getForm(); + + $form->submit('Bar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Bar'", + 'model' => "'Foo'", + ), + 'errors' => array(), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresViewDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addViewTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar', + '' => '', + ))) + ->getForm(); + + $form->submit('Bar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foo'", + 'view' => "'Bar'", + ), + 'errors' => array(), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresErrors() + { + $form = $this->createBuilder('name')->getForm(); + + $form->submit('Foobar'); + $form->addError(new FormError('Invalid!')); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + ), + 'errors' => array( + array('message' => 'Invalid!'), + ), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataRemembersIfNonSynchronized() + { + $form = $this->createBuilder('name') + ->addModelTransformer(new CallbackTransformer( + function () {}, + function () { + throw new TransformationFailedException('Fail!'); + } + )) + ->getForm(); + + $form->submit('Foobar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + 'model' => 'NULL', + ), + 'errors' => array(), + 'synchronized' => 'false', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractViewVariables() + { + $view = new FormView(); + + $view->vars = array( + 'b' => 'foo', + 'a' => 'bar', + 'c' => 'baz', + ); + + $this->assertSame(array( + 'view_vars' => array( + 'a' => "'bar'", + 'b' => "'foo'", + 'c' => "'baz'", + ), + ), $this->dataExtractor->extractViewVariables($view)); + } + + /** + * @param string $name + * @param array $options + * + * @return FormBuilder + */ + private function createBuilder($name, array $options = array()) + { + return new FormBuilder($name, null, $this->dispatcher, $this->factory, $options); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php index 4039bf6ee6..cb77456984 100644 --- a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php @@ -10,7 +10,6 @@ namespace Symfony\Component\Form\Tests\Extension\DataCollector\Type; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension; class DataCollectorTypeExtensionTest extends \PHPUnit_Framework_TestCase @@ -21,14 +20,14 @@ class DataCollectorTypeExtensionTest extends \PHPUnit_Framework_TestCase private $extension; /** - * @var EventSubscriberInterface + * @var \PHPUnit_Framework_MockObject_MockObject */ - private $eventSubscriber; + private $dataCollector; public function setUp() { - $this->eventSubscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface'); - $this->extension = new DataCollectorTypeExtension($this->eventSubscriber); + $this->dataCollector = $this->getMock('Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface'); + $this->extension = new DataCollectorTypeExtension($this->dataCollector); } public function testGetExtendedType() @@ -39,7 +38,9 @@ class DataCollectorTypeExtensionTest extends \PHPUnit_Framework_TestCase public function testBuildForm() { $builder = $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); - $builder->expects($this->atLeastOnce())->method('addEventSubscriber')->with($this->eventSubscriber); + $builder->expects($this->atLeastOnce()) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener')); $this->extension->buildForm($builder, array()); }