From 7e5104e09b8a600013330283daa6e82757022fcd Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 9 Feb 2012 16:10:10 +0100 Subject: [PATCH 1/3] [Form] Fixed MergeCollectionListener for the case that the form's data is updated by the data mapper (as happening in CollectionType) --- .../EventListener/MergeCollectionListener.php | 199 ++++++--- .../Form/Extension/Core/Type/ChoiceType.php | 40 +- .../Extension/Core/Type/CollectionType.php | 24 +- .../MergeCollectionListenerTest.php | 388 +++++++++++++----- 4 files changed, 481 insertions(+), 170 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php index 8249a81d2f..c7b5829b5f 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\Event\DataEvent; use Symfony\Component\Form\Event\FilterDataEvent; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Exception\FormException; @@ -23,6 +24,21 @@ use Symfony\Component\Form\Util\FormUtil; */ class MergeCollectionListener implements EventSubscriberInterface { + /** + * Strategy for merging the new collection into the old collection + * + * @var integer + */ + const MERGE_NORMAL = 1; + + /** + * Strategy for calling add/remove methods on the parent data for all + * new/removed elements in the new collection + * + * @var integer + */ + const MERGE_INTO_PARENT = 2; + /** * Whether elements may be added to the collection * @var Boolean @@ -39,7 +55,7 @@ class MergeCollectionListener implements EventSubscriberInterface * Whether to search for and use adder and remover methods * @var Boolean */ - private $useAccessors; + private $mergeStrategy; /** * The name of the adder method to look for @@ -53,35 +69,91 @@ class MergeCollectionListener implements EventSubscriberInterface */ private $removeMethod; - public function __construct($allowAdd = false, $allowDelete = false, $useAccessors = true, $addMethod = null, $removeMethod = null) + /** + * A copy of the data before starting binding for this form + * @var mixed + */ + private $dataSnapshot; + + /** + * Creates a new listener. + * + * @param Boolean $allowAdd Whether values might be added to the + * collection. + * @param Boolean $allowDelete Whether values might be removed from the + * collection. + * @param integer $mergeStrategy Which strategy to use for merging the + * bound collection with the original + * collection. Might be any combination of + * MERGE_NORMAL and MERGE_INTO_PARENT. + * MERGE_INTO_PARENT has precedence over + * MERGE_NORMAL if an adder/remover method + * is found. The default strategy is to use + * both strategies. + * @param string $addMethod The name of the adder method to use. If + * not given, the listener tries to discover + * the method automatically. + * @param string $removeMethod The name of the remover method to use. If + * not given, the listener tries to discover + * the method automatically. + * + * @throws FormException If the given strategy is invalid. + */ + public function __construct($allowAdd = false, $allowDelete = false, $mergeStrategy = null, $addMethod = null, $removeMethod = null) { + if ($mergeStrategy && !($mergeStrategy & (self::MERGE_NORMAL | self::MERGE_INTO_PARENT))) { + throw new FormException('The merge strategy needs to be at least MERGE_NORMAL or MERGE_INTO_PARENT'); + } + $this->allowAdd = $allowAdd; $this->allowDelete = $allowDelete; - $this->useAccessors = $useAccessors; + $this->mergeStrategy = $mergeStrategy ?: self::MERGE_NORMAL | self::MERGE_INTO_PARENT; $this->addMethod = $addMethod; $this->removeMethod = $removeMethod; } static public function getSubscribedEvents() { - return array(FormEvents::BIND_NORM_DATA => 'onBindNormData'); + return array( + FormEvents::PRE_BIND => 'preBind', + FormEvents::BIND_NORM_DATA => 'onBindNormData', + ); + } + + public function preBind(DataEvent $event) + { + // Get a snapshot of the current state of the normalized data + // to compare against later + $this->dataSnapshot = $event->getForm()->getNormData(); + + if (is_object($this->dataSnapshot)) { + // Make sure the snapshot remains stable and doesn't change + $this->dataSnapshot = clone $this->dataSnapshot; + } + + if (null !== $this->dataSnapshot && !is_array($this->dataSnapshot) && !($this->dataSnapshot instanceof \Traversable && $this->dataSnapshot instanceof \ArrayAccess)) { + throw new UnexpectedTypeException($this->dataSnapshot, 'array or (\Traversable and \ArrayAccess)'); + } } public function onBindNormData(FilterDataEvent $event) { - $originalData = $event->getForm()->getData(); + $originalData = $event->getForm()->getNormData(); // If we are not allowed to change anything, return immediately if (!$this->allowAdd && !$this->allowDelete) { + // Don't set to the snapshot as then we are switching from the + // original object to its copy, which might break things $event->setData($originalData); return; } $form = $event->getForm(); $data = $event->getData(); - $parentData = $form->hasParent() ? $form->getParent()->getData() : null; + $parentData = $form->hasParent() ? $form->getParent()->getClientData() : null; $addMethod = null; $removeMethod = null; + $getMethod = null; if (null === $data) { $data = array(); @@ -96,24 +168,35 @@ class MergeCollectionListener implements EventSubscriberInterface } // Check if the parent has matching methods to add/remove items - if ($this->useAccessors && is_object($parentData)) { + if (($this->mergeStrategy & self::MERGE_INTO_PARENT) && is_object($parentData)) { + $plural = ucfirst($form->getName()); $reflClass = new \ReflectionClass($parentData); $addMethodNeeded = $this->allowAdd && !$this->addMethod; $removeMethodNeeded = $this->allowDelete && !$this->removeMethod; // Any of the two methods is required, but not yet known if ($addMethodNeeded || $removeMethodNeeded) { - $singulars = (array) FormUtil::singularify(ucfirst($form->getName())); + $singulars = (array) FormUtil::singularify($plural); foreach ($singulars as $singular) { // Try to find adder, but don't override preconfigured one if ($addMethodNeeded) { - $addMethod = $this->checkMethod($reflClass, 'add' . $singular); + $addMethod = 'add' . $singular; + + // False alert + if (!$this->isAccessible($reflClass, $addMethod, 1)) { + $addMethod = null; + } } // Try to find remover, but don't override preconfigured one if ($removeMethodNeeded) { - $removeMethod = $this->checkMethod($reflClass, 'remove' . $singular); + $removeMethod = 'remove' . $singular; + + // False alert + if (!$this->isAccessible($reflClass, $removeMethod, 1)) { + $removeMethod = null; + } } // Found all that we need. Abort search. @@ -129,12 +212,12 @@ class MergeCollectionListener implements EventSubscriberInterface // Set preconfigured adder if ($this->allowAdd && $this->addMethod) { - $addMethod = $this->checkMethod($reflClass, $this->addMethod); + $addMethod = $this->addMethod; - if (!$addMethod) { + if (!$this->isAccessible($reflClass, $addMethod, 1)) { throw new FormException(sprintf( - 'The method "%s" could not be found on class %s', - $this->addMethod, + 'The public method "%s" could not be found on class %s', + $addMethod, $reflClass->getName() )); } @@ -142,25 +225,36 @@ class MergeCollectionListener implements EventSubscriberInterface // Set preconfigured remover if ($this->allowDelete && $this->removeMethod) { - $removeMethod = $this->checkMethod($reflClass, $this->removeMethod); + $removeMethod = $this->removeMethod; - if (!$removeMethod) { + if (!$this->isAccessible($reflClass, $removeMethod, 1)) { throw new FormException(sprintf( - 'The method "%s" could not be found on class %s', - $this->removeMethod, + 'The public method "%s" could not be found on class %s', + $removeMethod, + $reflClass->getName() + )); + } + } + + if ($addMethod || $removeMethod) { + $getMethod = 'get' . $plural; + + if (!$this->isAccessible($reflClass, $getMethod, 0)) { + throw new FormException(sprintf( + 'The public method "%s" could not be found on class %s', + $getMethod, $reflClass->getName() )); } } } - // Check which items are in $data that are not in $originalData and - // vice versa + // Calculate delta between $data and the snapshot created in PRE_BIND $itemsToDelete = array(); $itemsToAdd = is_object($data) ? clone $data : $data; - if ($originalData) { - foreach ($originalData as $originalKey => $originalItem) { + if ($this->dataSnapshot) { + foreach ($this->dataSnapshot as $originalItem) { foreach ($data as $key => $item) { if ($item === $originalItem) { // Item found, next original item @@ -170,7 +264,12 @@ class MergeCollectionListener implements EventSubscriberInterface } // Item not found, remember for deletion - $itemsToDelete[$originalKey] = $originalItem; + foreach ($originalData as $key => $item) { + if ($item === $originalItem) { + $itemsToDelete[$key] = $item; + continue 2; + } + } } } @@ -187,43 +286,47 @@ class MergeCollectionListener implements EventSubscriberInterface $parentData->$addMethod($item); } } - } elseif (!$originalData) { - // No original data was set. Set it if allowed - if ($this->allowAdd) { - $originalData = $data; - } - } else { - // Original data is an array-like structure - // Add and remove items in the original variable - if ($this->allowDelete) { - foreach ($itemsToDelete as $key => $item) { - unset($originalData[$key]); - } - } - if ($this->allowAdd) { - foreach ($itemsToAdd as $key => $item) { - if (!isset($originalData[$key])) { - $originalData[$key] = $item; - } else { - $originalData[] = $item; + $event->setData($parentData->$getMethod()); + } elseif ($this->mergeStrategy & self::MERGE_NORMAL) { + if (!$originalData) { + // No original data was set. Set it if allowed + if ($this->allowAdd) { + $originalData = $data; + } + } else { + // Original data is an array-like structure + // Add and remove items in the original variable + if ($this->allowDelete) { + foreach ($itemsToDelete as $key => $item) { + unset($originalData[$key]); + } + } + + if ($this->allowAdd) { + foreach ($itemsToAdd as $key => $item) { + if (!isset($originalData[$key])) { + $originalData[$key] = $item; + } else { + $originalData[] = $item; + } } } } - } - $event->setData($originalData); + $event->setData($originalData); + } } - private function checkMethod(\ReflectionClass $reflClass, $methodName) { + private function isAccessible(\ReflectionClass $reflClass, $methodName, $numberOfRequiredParameters) { if ($reflClass->hasMethod($methodName)) { $method = $reflClass->getMethod($methodName); - if ($method->isPublic() && $method->getNumberOfRequiredParameters() === 1) { - return $methodName; + if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $numberOfRequiredParameters) { + return true; } } - return null; + return false; } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index ff4a4b5ee9..37c4c672cd 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -81,10 +81,7 @@ class ChoiceType extends AbstractType if ($options['expanded']) { if ($options['multiple']) { - $builder - ->appendClientTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list'])) - ->addEventSubscriber(new MergeCollectionListener(true, true)) - ; + $builder->appendClientTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list'])); } else { $builder ->appendClientTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'])) @@ -93,24 +90,31 @@ class ChoiceType extends AbstractType } } else { if ($options['multiple']) { - $builder - ->appendClientTransformer(new ChoicesToValuesTransformer($options['choice_list'])) - ->addEventSubscriber(new MergeCollectionListener( - true, - true, - // If "by_reference" is disabled (explicit calling of - // the setter is desired), disable support for - // adders/removers - // Same as in CollectionType - $options['by_reference'], - $options['add_method'], - $options['remove_method'] - )) - ; + $builder->appendClientTransformer(new ChoicesToValuesTransformer($options['choice_list'])); } else { $builder->appendClientTransformer(new ChoiceToValueTransformer($options['choice_list'])); } } + + if ($options['multiple']) { + // Make sure the collection created during the client->norm + // transformation is merged back into the original collection + $mergeStrategy = MergeCollectionListener::MERGE_NORMAL; + + // Enable support for adders/removers unless "by_reference" is disabled + // (explicit calling of the setter is desired) + if ($options['by_reference']) { + $mergeStrategy = $mergeStrategy | MergeCollectionListener::MERGE_INTO_PARENT; + } + + $builder->addEventSubscriber(new MergeCollectionListener( + true, + true, + $mergeStrategy, + $options['add_method'], + $options['remove_method'] + )); + } } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php index fbdb35383a..c094c2126c 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php @@ -38,23 +38,23 @@ class CollectionType extends AbstractType $options['allow_delete'] ); - $mergeListener = new MergeCollectionListener( - $options['allow_add'], - $options['allow_delete'], - // If "by_reference" is disabled (explicit calling of the setter - // is desired), disable support for adders/removers - // Same as in ChoiceType - $options['by_reference'], - $options['add_method'], - $options['remove_method'] - ); - $builder ->addEventSubscriber($resizeListener) - ->addEventSubscriber($mergeListener) ->setAttribute('allow_add', $options['allow_add']) ->setAttribute('allow_delete', $options['allow_delete']) ; + + // Enable support for adders/removers unless "by_reference" is disabled + // (explicit calling of the setter is desired) + if ($options['by_reference']) { + $builder->addEventSubscriber(new MergeCollectionListener( + $options['allow_add'], + $options['allow_delete'], + MergeCollectionListener::MERGE_INTO_PARENT, + $options['add_method'], + $options['remove_method'] + )); + } } /** diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php index 02fd3e7300..e29b40dff1 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php @@ -22,6 +22,8 @@ class MergeCollectionListenerTest_Car public function addAxis($axis) {} public function removeAxis($axis) {} + + public function getAxes() {} } class MergeCollectionListenerTest_CarCustomNames @@ -29,16 +31,22 @@ class MergeCollectionListenerTest_CarCustomNames public function foo($axis) {} public function bar($axis) {} + + public function getAxes() {} } class MergeCollectionListenerTest_CarOnlyAdder { public function addAxis($axis) {} + + public function getAxes() {} } class MergeCollectionListenerTest_CarOnlyRemover { public function removeAxis($axis) {} + + public function getAxes() {} } abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase @@ -76,17 +84,55 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase return $this->getMock('Symfony\Tests\Component\Form\FormInterface'); } + public function getModesWithNormal() + { + return array( + array(MergeCollectionListener::MERGE_NORMAL), + array(MergeCollectionListener::MERGE_NORMAL | MergeCollectionListener::MERGE_INTO_PARENT), + ); + } + + public function getModesWithMergeIntoParent() + { + return array( + array(MergeCollectionListener::MERGE_INTO_PARENT), + array(MergeCollectionListener::MERGE_INTO_PARENT | MergeCollectionListener::MERGE_NORMAL), + ); + } + + public function getModesWithoutMergeIntoParent() + { + return array( + array(MergeCollectionListener::MERGE_NORMAL), + ); + } + + public function getInvalidModes() + { + return array( + // 0 is a valid mode, because it is treated as "default" (=3) + array(4), + array(8), + ); + } + abstract protected function getData(array $data); - public function testAddExtraEntriesIfAllowAdd() + /** + * @dataProvider getModesWithNormal + */ + public function testAddExtraEntriesIfAllowAdd($mode) { $originalData = $this->getData(array(1 => 'second')); $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $listener = new MergeCollectionListener(true, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, false); $listener->onBindNormData($event); // The original object was modified @@ -98,15 +144,21 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($newData, $event->getData()); } - public function testAddExtraEntriesIfAllowAddDontOverwriteExistingIndices() + /** + * @dataProvider getModesWithNormal + */ + public function testAddExtraEntriesIfAllowAddDontOverwriteExistingIndices($mode) { $originalData = $this->getData(array(1 => 'first')); $newData = $this->getData(array(0 => 'first', 1 => 'second')); + $listener = new MergeCollectionListener(true, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, false); $listener->onBindNormData($event); // The original object was modified @@ -118,16 +170,22 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($this->getData(array(1 => 'first', 2 => 'second')), $event->getData()); } - public function testDoNothingIfNotAllowAdd() + /** + * @dataProvider getModesWithNormal + */ + public function testDoNothingIfNotAllowAdd($mode) { $originalDataArray = array(1 => 'second'); $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $listener = new MergeCollectionListener(false, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, false); $listener->onBindNormData($event); // We still have the original object @@ -139,15 +197,21 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($this->getData($originalDataArray), $event->getData()); } - public function testRemoveMissingEntriesIfAllowDelete() + /** + * @dataProvider getModesWithNormal + */ + public function testRemoveMissingEntriesIfAllowDelete($mode) { $originalData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); $newData = $this->getData(array(1 => 'second')); + $listener = new MergeCollectionListener(false, true, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, true); $listener->onBindNormData($event); // The original object was modified @@ -159,16 +223,22 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($newData, $event->getData()); } - public function testDoNothingIfNotAllowDelete() + /** + * @dataProvider getModesWithNormal + */ + public function testDoNothingIfNotAllowDelete($mode) { $originalDataArray = array(0 => 'first', 1 => 'second', 2 => 'third'); $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(1 => 'second')); + $listener = new MergeCollectionListener(false, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, false); $listener->onBindNormData($event); // We still have the original object @@ -181,59 +251,81 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase } /** + * @dataProvider getModesWithNormal * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException */ - public function testRequireArrayOrTraversable() + public function testRequireArrayOrTraversable($mode) { $newData = 'no array or traversable'; $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, false); + $listener = new MergeCollectionListener(true, false, $mode); $listener->onBindNormData($event); } - public function testDealWithNullData() + /** + * @dataProvider getModesWithNormal + */ + public function testDealWithNullData($mode) { $originalData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); $newData = null; + $listener = new MergeCollectionListener(false, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, false); $listener->onBindNormData($event); $this->assertSame($originalData, $event->getData()); } - public function testDealWithNullOriginalDataIfAllowAdd() + /** + * @dataProvider getModesWithNormal + */ + public function testDealWithNullOriginalDataIfAllowAdd($mode) { $originalData = null; $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $listener = new MergeCollectionListener(true, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, false); $listener->onBindNormData($event); $this->assertSame($newData, $event->getData()); } - public function testDontDealWithNullOriginalDataIfNotAllowAdd() + /** + * @dataProvider getModesWithNormal + */ + public function testDontDealWithNullOriginalDataIfNotAllowAdd($mode) { $originalData = null; $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $listener = new MergeCollectionListener(false, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, false); $listener->onBindNormData($event); $this->assertNull($event->getData()); } - public function testCallAdderIfAllowAdd() + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testCallAdderIfAllowAdd($mode) { $parentData = $this->getMock(__CLASS__ . '_CarOnlyAdder'); $parentForm = $this->getForm('car'); @@ -244,29 +336,77 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $listener = new MergeCollectionListener(true, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + $parentData->expects($this->at(0)) ->method('addAxis') ->with('first'); $parentData->expects($this->at(1)) ->method('addAxis') ->with('third'); + $parentData->expects($this->at(2)) + ->method('getAxes') + ->will($this->returnValue('RESULT')); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, false); $listener->onBindNormData($event); - if (is_object($originalData)) { - $this->assertSame($originalData, $event->getData()); - } - - // The data was not modified directly - // Thus it should not be written back into the parent data! - $this->assertEquals($this->getData($originalDataArray), $event->getData()); + $this->assertEquals('RESULT', $event->getData()); } - public function testDontCallAdderIfNotAllowAdd() + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testCallAdderIfOriginalDataAlreadyModified($mode) + { + $parentData = $this->getMock(__CLASS__ . '_CarOnlyAdder'); + $parentForm = $this->getForm('car'); + $parentForm->setData($parentData); + $parentForm->add($this->form); + + $originalDataArray = array(1 => 'second'); + $originalData = $this->getData($originalDataArray); + $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $listener = new MergeCollectionListener(true, false, $mode); + + $this->form->setData($originalData); + + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + + // The form already contains the new data + // This happens if the data mapper maps the data of the child forms + // back into the original collection. + // The original collection is then both already modified and passed + // as event argument. + $this->form->setData($newData); + + $parentData->expects($this->at(0)) + ->method('addAxis') + ->with('first'); + $parentData->expects($this->at(1)) + ->method('addAxis') + ->with('third'); + $parentData->expects($this->at(2)) + ->method('getAxes') + ->will($this->returnValue('RESULT')); + + $event = new FilterDataEvent($this->form, $newData); + $listener->onBindNormData($event); + + $this->assertEquals('RESULT', $event->getData()); + } + + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testDontCallAdderIfNotAllowAdd($mode) { $parentData = $this->getMock(__CLASS__ . '_Car'); $parentForm = $this->getForm('car'); @@ -277,13 +417,17 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $listener = new MergeCollectionListener(false, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + $parentData->expects($this->never()) ->method('addAxis'); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, false); $listener->onBindNormData($event); if (is_object($originalData)) { @@ -294,7 +438,10 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($this->getData($originalDataArray), $event->getData()); } - public function testDontCallAdderIfNotUseAccessors() + /** + * @dataProvider getModesWithoutMergeIntoParent + */ + public function testDontCallAdderIfNotMergeIntoParent($mode) { $parentData = $this->getMock(__CLASS__ . '_Car'); $parentForm = $this->getForm('car'); @@ -304,13 +451,17 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData(array(1 => 'second')); $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $listener = new MergeCollectionListener(true, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + $parentData->expects($this->never()) ->method('addAxis'); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, false, false); $listener->onBindNormData($event); if (is_object($originalData)) { @@ -321,7 +472,10 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($newData, $event->getData()); } - public function testCallRemoverIfAllowDelete() + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testCallRemoverIfAllowDelete($mode) { $parentData = $this->getMock(__CLASS__ . '_CarOnlyRemover'); $parentForm = $this->getForm('car'); @@ -332,29 +486,33 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(1 => 'second')); + $listener = new MergeCollectionListener(false, true, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + $parentData->expects($this->at(0)) ->method('removeAxis') ->with('first'); $parentData->expects($this->at(1)) ->method('removeAxis') ->with('third'); + $parentData->expects($this->at(2)) + ->method('getAxes') + ->will($this->returnValue('RESULT')); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, true); $listener->onBindNormData($event); - if (is_object($originalData)) { - $this->assertSame($originalData, $event->getData()); - } - - // The data was not modified directly - // Thus it should not be written back into the parent data! - $this->assertEquals($this->getData($originalDataArray), $event->getData()); + $this->assertEquals('RESULT', $event->getData()); } - public function testDontCallRemoverIfNotAllowDelete() + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testDontCallRemoverIfNotAllowDelete($mode) { $parentData = $this->getMock(__CLASS__ . '_Car'); $parentForm = $this->getForm('car'); @@ -365,13 +523,17 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(1 => 'second')); + $listener = new MergeCollectionListener(false, false, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + $parentData->expects($this->never()) ->method('removeAxis'); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, false); $listener->onBindNormData($event); if (is_object($originalData)) { @@ -382,7 +544,10 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($this->getData($originalDataArray), $event->getData()); } - public function testDontCallRemoverIfNotUseAccessors() + /** + * @dataProvider getModesWithoutMergeIntoParent + */ + public function testDontCallRemoverIfNotMergeIntoParent($mode) { $parentData = $this->getMock(__CLASS__ . '_Car'); $parentForm = $this->getForm('car'); @@ -392,13 +557,17 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); $newData = $this->getData(array(1 => 'second')); + $listener = new MergeCollectionListener(false, true, $mode); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + $parentData->expects($this->never()) ->method('removeAxis'); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, true, false); $listener->onBindNormData($event); if (is_object($originalData)) { @@ -409,7 +578,10 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($newData, $event->getData()); } - public function testCallAdderAndDeleterIfAllowAll() + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testCallAdderAndDeleterIfAllowAll($mode) { $parentData = $this->getMock(__CLASS__ . '_Car'); $parentForm = $this->getForm('car'); @@ -420,29 +592,33 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(0 => 'first')); + $listener = new MergeCollectionListener(true, true, $mode); + $this->form->setData($originalData); - $parentData->expects($this->once()) - ->method('addAxis') - ->with('first'); - $parentData->expects($this->once()) + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + + $parentData->expects($this->at(0)) ->method('removeAxis') ->with('second'); + $parentData->expects($this->at(1)) + ->method('addAxis') + ->with('first'); + $parentData->expects($this->at(2)) + ->method('getAxes') + ->will($this->returnValue('RESULT')); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, true, true); $listener->onBindNormData($event); - if (is_object($originalData)) { - $this->assertSame($originalData, $event->getData()); - } - - // The data was not modified directly - // Thus it should not be written back into the parent data! - $this->assertEquals($this->getData($originalDataArray), $event->getData()); + $this->assertEquals('RESULT', $event->getData()); } - public function testCallAccessorsWithCustomNames() + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testCallAccessorsWithCustomNames($mode) { $parentData = $this->getMock(__CLASS__ . '_CarCustomNames'); $parentForm = $this->getForm('car'); @@ -453,29 +629,33 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(0 => 'first')); + $listener = new MergeCollectionListener(true, true, $mode, 'foo', 'bar'); + $this->form->setData($originalData); - $parentData->expects($this->once()) - ->method('foo') - ->with('first'); - $parentData->expects($this->once()) + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + + $parentData->expects($this->at(0)) ->method('bar') ->with('second'); + $parentData->expects($this->at(1)) + ->method('foo') + ->with('first'); + $parentData->expects($this->at(2)) + ->method('getAxes') + ->will($this->returnValue('RESULT')); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, true, true, 'foo', 'bar'); $listener->onBindNormData($event); - if (is_object($originalData)) { - $this->assertSame($originalData, $event->getData()); - } - - // The data was not modified directly - // Thus it should not be written back into the parent data! - $this->assertEquals($this->getData($originalDataArray), $event->getData()); + $this->assertEquals('RESULT', $event->getData()); } - public function testDontCallAdderWithCustomNameIfDisallowed() + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testDontCallAdderWithCustomNameIfDisallowed($mode) { $parentData = $this->getMock(__CLASS__ . '_CarCustomNames'); $parentForm = $this->getForm('car'); @@ -486,27 +666,32 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(0 => 'first')); + $listener = new MergeCollectionListener(false, true, $mode, 'foo', 'bar'); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + $parentData->expects($this->never()) ->method('foo'); - $parentData->expects($this->once()) + $parentData->expects($this->at(0)) ->method('bar') ->with('second'); + $parentData->expects($this->at(1)) + ->method('getAxes') + ->will($this->returnValue('RESULT')); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, true, true, 'foo', 'bar'); $listener->onBindNormData($event); - if (is_object($originalData)) { - $this->assertSame($originalData, $event->getData()); - } - - // The data was not modified - $this->assertEquals($this->getData($originalDataArray), $event->getData()); + $this->assertEquals('RESULT', $event->getData()); } - public function testDontCallRemoverWithCustomNameIfDisallowed() + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testDontCallRemoverWithCustomNameIfDisallowed($mode) { $parentData = $this->getMock(__CLASS__ . '_CarCustomNames'); $parentForm = $this->getForm('car'); @@ -517,30 +702,33 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData($originalDataArray); $newData = $this->getData(array(0 => 'first')); + $listener = new MergeCollectionListener(true, false, $mode, 'foo', 'bar'); + $this->form->setData($originalData); - $parentData->expects($this->once()) + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + + $parentData->expects($this->at(0)) ->method('foo') ->with('first'); $parentData->expects($this->never()) ->method('bar'); + $parentData->expects($this->at(1)) + ->method('getAxes') + ->will($this->returnValue('RESULT')); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, false, true, 'foo', 'bar'); $listener->onBindNormData($event); - if (is_object($originalData)) { - $this->assertSame($originalData, $event->getData()); - } - - // The data was not modified - $this->assertEquals($this->getData($originalDataArray), $event->getData()); + $this->assertEquals('RESULT', $event->getData()); } /** + * @dataProvider getModesWithMergeIntoParent * @expectedException Symfony\Component\Form\Exception\FormException */ - public function testThrowExceptionIfInvalidAdder() + public function testThrowExceptionIfInvalidAdder($mode) { $parentData = $this->getMock(__CLASS__ . '_CarCustomNames'); $parentForm = $this->getForm('car'); @@ -550,17 +738,21 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData(array(1 => 'second')); $newData = $this->getData(array(0 => 'first')); + $listener = new MergeCollectionListener(true, false, $mode, 'doesnotexist'); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(true, false, true, 'doesnotexist'); $listener->onBindNormData($event); } /** + * @dataProvider getModesWithMergeIntoParent * @expectedException Symfony\Component\Form\Exception\FormException */ - public function testThrowExceptionIfInvalidRemover() + public function testThrowExceptionIfInvalidRemover($mode) { $parentData = $this->getMock(__CLASS__ . '_CarCustomNames'); $parentForm = $this->getForm('car'); @@ -570,10 +762,22 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $originalData = $this->getData(array(1 => 'second')); $newData = $this->getData(array(0 => 'first')); + $listener = new MergeCollectionListener(false, true, $mode, null, 'doesnotexist'); + $this->form->setData($originalData); + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); $event = new FilterDataEvent($this->form, $newData); - $listener = new MergeCollectionListener(false, true, true, null, 'doesnotexist'); $listener->onBindNormData($event); } + + /** + * @dataProvider getInvalidModes + * @expectedException Symfony\Component\Form\Exception\FormException + */ + public function testThrowExceptionIfInvalidMode($mode) + { + new MergeCollectionListener(true, true, $mode); + } } From b56502f023ad6c551801ed76da9ba510e1030950 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 9 Feb 2012 16:41:15 +0100 Subject: [PATCH 2/3] [Form] Added getParent() to PropertyPath --- .../Component/Form/Util/PropertyPath.php | 43 +++++++++++++++++-- .../Tests/Component/Form/PropertyPathTest.php | 21 +++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Form/Util/PropertyPath.php b/src/Symfony/Component/Form/Util/PropertyPath.php index 96fa57f202..f29aa3d515 100644 --- a/src/Symfony/Component/Form/Util/PropertyPath.php +++ b/src/Symfony/Component/Form/Util/PropertyPath.php @@ -27,26 +27,32 @@ class PropertyPath implements \IteratorAggregate * The elements of the property path * @var array */ - protected $elements = array(); + private $elements = array(); /** * The number of elements in the property path * @var integer */ - protected $length; + private $length; /** * Contains a Boolean for each property in $elements denoting whether this * element is an index. It is a property otherwise. * @var array */ - protected $isIndex = array(); + private $isIndex = array(); /** * String representation of the path * @var string */ - protected $string; + private $string; + + /** + * Positions where the individual elements start in the string representation + * @var array + */ + private $positions; /** * Parses the given property path @@ -67,6 +73,8 @@ class PropertyPath implements \IteratorAggregate $pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/'; while (preg_match($pattern, $remaining, $matches)) { + $this->positions[] = $position; + if ('' !== $matches[2]) { $this->elements[] = $matches[2]; $this->isIndex[] = false; @@ -102,6 +110,33 @@ class PropertyPath implements \IteratorAggregate return $this->string; } + /** + * Returns the parent property path. + * + * The parent property path is the one that contains the same items as + * this one except for the last one. + * + * If this property path only contains one item, null is returned. + * + * @return PropertyPath The parent path or null. + */ + public function getParent() + { + if ($this->length <= 1) { + return null; + } + + $parent = clone $this; + + --$parent->length; + $parent->string = substr($parent->string, 0, $parent->positions[$parent->length]); + array_pop($parent->elements); + array_pop($parent->isIndex); + array_pop($parent->positions); + + return $parent; + } + /** * Returns a new iterator for this path * diff --git a/tests/Symfony/Tests/Component/Form/PropertyPathTest.php b/tests/Symfony/Tests/Component/Form/PropertyPathTest.php index 8ddc83eea1..76b7f69b41 100644 --- a/tests/Symfony/Tests/Component/Form/PropertyPathTest.php +++ b/tests/Symfony/Tests/Component/Form/PropertyPathTest.php @@ -391,4 +391,25 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase new PropertyPath(null); } + + public function testGetParent_dot() + { + $propertyPath = new PropertyPath('grandpa.parent.child'); + + $this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent()); + } + + public function testGetParent_index() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent()); + } + + public function testGetParent_noParent() + { + $propertyPath = new PropertyPath('path'); + + $this->assertNull($propertyPath->getParent()); + } } From da2447e1184716a4728d9d965e8d723dd80e45e5 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 9 Feb 2012 17:03:48 +0100 Subject: [PATCH 3/3] [Form] Fixed MergeCollectionListener when used with a custom property path --- .../EventListener/MergeCollectionListener.php | 41 +++++++----- .../Component/Form/Util/PropertyPath.php | 10 +++ .../MergeCollectionListenerTest.php | 65 ++++++++++++++++++- 3 files changed, 98 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php index c7b5829b5f..b80c05c0a1 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php @@ -18,6 +18,7 @@ use Symfony\Component\Form\Event\FilterDataEvent; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Exception\FormException; use Symfony\Component\Form\Util\FormUtil; +use Symfony\Component\Form\Util\PropertyPath; /** * @author Bernhard Schussek @@ -150,10 +151,31 @@ class MergeCollectionListener implements EventSubscriberInterface $form = $event->getForm(); $data = $event->getData(); - $parentData = $form->hasParent() ? $form->getParent()->getClientData() : null; + $childPropertyPath = null; + $parentData = null; $addMethod = null; $removeMethod = null; - $getMethod = null; + $propertyPath = null; + $plural = null; + + if ($form->hasParent() && $form->getAttribute('property_path')) { + $propertyPath = new PropertyPath($form->getAttribute('property_path')); + $childPropertyPath = $propertyPath; + $parentData = $form->getParent()->getClientData(); + $lastElement = $propertyPath->getElement($propertyPath->getLength() - 1); + + // If the property path contains more than one element, the parent + // data is the object at the parent property path + if ($propertyPath->getLength() > 1) { + $parentData = $propertyPath->getParent()->getValue($parentData); + + // Property path relative to $parentData + $childPropertyPath = new PropertyPath($lastElement); + } + + // The plural form is the last element of the property path + $plural = ucfirst($lastElement); + } if (null === $data) { $data = array(); @@ -169,7 +191,6 @@ class MergeCollectionListener implements EventSubscriberInterface // Check if the parent has matching methods to add/remove items if (($this->mergeStrategy & self::MERGE_INTO_PARENT) && is_object($parentData)) { - $plural = ucfirst($form->getName()); $reflClass = new \ReflectionClass($parentData); $addMethodNeeded = $this->allowAdd && !$this->addMethod; $removeMethodNeeded = $this->allowDelete && !$this->removeMethod; @@ -235,18 +256,6 @@ class MergeCollectionListener implements EventSubscriberInterface )); } } - - if ($addMethod || $removeMethod) { - $getMethod = 'get' . $plural; - - if (!$this->isAccessible($reflClass, $getMethod, 0)) { - throw new FormException(sprintf( - 'The public method "%s" could not be found on class %s', - $getMethod, - $reflClass->getName() - )); - } - } } // Calculate delta between $data and the snapshot created in PRE_BIND @@ -287,7 +296,7 @@ class MergeCollectionListener implements EventSubscriberInterface } } - $event->setData($parentData->$getMethod()); + $event->setData($childPropertyPath->getValue($parentData)); } elseif ($this->mergeStrategy & self::MERGE_NORMAL) { if (!$originalData) { // No original data was set. Set it if allowed diff --git a/src/Symfony/Component/Form/Util/PropertyPath.php b/src/Symfony/Component/Form/Util/PropertyPath.php index f29aa3d515..44087d933b 100644 --- a/src/Symfony/Component/Form/Util/PropertyPath.php +++ b/src/Symfony/Component/Form/Util/PropertyPath.php @@ -110,6 +110,16 @@ class PropertyPath implements \IteratorAggregate return $this->string; } + /** + * Returns the length of the property path. + * + * @return integer + */ + public function getLength() + { + return $this->length; + } + /** * Returns the parent property path. * diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php index e29b40dff1..11256d77ee 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php @@ -49,6 +49,20 @@ class MergeCollectionListenerTest_CarOnlyRemover public function getAxes() {} } +class MergeCollectionListenerTest_CompositeCar +{ + public function getStructure() {} +} + +class MergeCollectionListenerTest_CarStructure +{ + public function addAxis($axis) {} + + public function removeAxis($axis) {} + + public function getAxes() {} +} + abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase { private $dispatcher; @@ -74,9 +88,11 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase return new FormBuilder($name, $this->factory, $this->dispatcher); } - protected function getForm($name = 'name') + protected function getForm($name = 'name', $propertyPath = null) { - return $this->getBuilder($name)->getForm(); + $propertyPath = $propertyPath ?: $name; + + return $this->getBuilder($name)->setAttribute('property_path', $propertyPath)->getForm(); } protected function getMockForm() @@ -359,6 +375,51 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals('RESULT', $event->getData()); } + /** + * @dataProvider getModesWithMergeIntoParent + */ + public function testCallAdderIfCustomPropertyPath($mode) + { + $this->form = $this->getForm('structure_axes', 'structure.axes'); + + $parentData = $this->getMock(__CLASS__ . '_CompositeCar'); + $parentForm = $this->getForm('car'); + $parentForm->setData($parentData); + $parentForm->add($this->form); + + $modifData = $this->getMock(__CLASS__ . '_CarStructure'); + + $originalDataArray = array(1 => 'second'); + $originalData = $this->getData($originalDataArray); + $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $listener = new MergeCollectionListener(true, false, $mode); + + $this->form->setData($originalData); + + $event = new DataEvent($this->form, $newData); + $listener->preBind($event); + + $parentData->expects($this->once()) + ->method('getStructure') + ->will($this->returnValue($modifData)); + + $modifData->expects($this->at(0)) + ->method('addAxis') + ->with('first'); + $modifData->expects($this->at(1)) + ->method('addAxis') + ->with('third'); + $modifData->expects($this->at(2)) + ->method('getAxes') + ->will($this->returnValue('RESULT')); + + $event = new FilterDataEvent($this->form, $newData); + $listener->onBindNormData($event); + + $this->assertEquals('RESULT', $event->getData()); + } + /** * @dataProvider getModesWithMergeIntoParent */