From 49d1464b43ba23312b72586e57289dbaef126484 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 1 Feb 2012 18:52:43 +0100 Subject: [PATCH] [Form] Implemented MergeCollectionListener which calls addXxx() and removeXxx() in your model if found The listener is used by the Collection type as well as the Choice and Entity type (with multiple selection). The effect is that you can have for example this model: class Article { public function addTag($tag) { ... } public function removeTag($tag) { ... } public function getTags($tag) { ... } } You can create a form for the article with a field "tags" of either type "collection" or "choice" (or "entity"). The field will correctly use the three methods of the model for displaying and editing tags. --- CHANGELOG-2.1.md | 3 + ...hp => MergeDoctrineCollectionListener.php} | 27 +- .../Doctrine/Form/Type/DoctrineType.php | 4 +- .../EventListener/MergeCollectionListener.php | 158 +++++++++ .../Core/EventListener/ResizeFormListener.php | 3 +- .../Form/Extension/Core/Type/ChoiceType.php | 12 +- .../Extension/Core/Type/CollectionType.php | 11 +- src/Symfony/Component/Form/Util/FormUtil.php | 4 +- .../Doctrine/Form/Type/EntityTypeTest.php | 10 +- ...MergeCollectionListenerArrayObjectTest.php | 20 ++ .../MergeCollectionListenerArrayTest.php | 20 ++ ...ollectionListenerCustomArrayObjectTest.php | 82 +++++ .../MergeCollectionListenerTest.php | 326 ++++++++++++++++++ .../Core/Type/CollectionTypeTest.php | 14 +- .../Component/Form/Util/FormUtilTest.php | 3 +- 15 files changed, 653 insertions(+), 44 deletions(-) rename src/Symfony/Bridge/Doctrine/Form/EventListener/{MergeCollectionListener.php => MergeDoctrineCollectionListener.php} (59%) create mode 100644 src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php create mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerArrayObjectTest.php create mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerArrayTest.php create mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerCustomArrayObjectTest.php create mode 100644 tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php diff --git a/CHANGELOG-2.1.md b/CHANGELOG-2.1.md index 237c87e881..98f8eed04c 100644 --- a/CHANGELOG-2.1.md +++ b/CHANGELOG-2.1.md @@ -195,6 +195,9 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c * choice fields now throw a FormException if neither the "choices" nor the "choice_list" option is set * the radio type is now a child of the checkbox type + * the Collection, Choice (with multiple selection) and Entity (with multiple + selection) types now make use of addXxx() and removeXxx() methods in your + model ### HttpFoundation diff --git a/src/Symfony/Bridge/Doctrine/Form/EventListener/MergeCollectionListener.php b/src/Symfony/Bridge/Doctrine/Form/EventListener/MergeDoctrineCollectionListener.php similarity index 59% rename from src/Symfony/Bridge/Doctrine/Form/EventListener/MergeCollectionListener.php rename to src/Symfony/Bridge/Doctrine/Form/EventListener/MergeDoctrineCollectionListener.php index 39c3b11f21..d3e567d69a 100644 --- a/src/Symfony/Bridge/Doctrine/Form/EventListener/MergeCollectionListener.php +++ b/src/Symfony/Bridge/Doctrine/Form/EventListener/MergeDoctrineCollectionListener.php @@ -24,11 +24,13 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; * * @see Doctrine\Common\Collections\Collection */ -class MergeCollectionListener implements EventSubscriberInterface +class MergeDoctrineCollectionListener implements EventSubscriberInterface { static public function getSubscribedEvents() { - return array(FormEvents::BIND_NORM_DATA => 'onBindNormData'); + // Higher priority than core MergeCollectionListener so that this one + // is called before + return array(FormEvents::BIND_NORM_DATA => array('onBindNormData', 10)); } public function onBindNormData(FilterDataEvent $event) @@ -36,25 +38,10 @@ class MergeCollectionListener implements EventSubscriberInterface $collection = $event->getForm()->getData(); $data = $event->getData(); - if (!$collection) { - $collection = $data; - } elseif (count($data) === 0) { + // If all items were removed, call clear which has a higher + // performance on persistent collections + if ($collection && count($data) === 0) { $collection->clear(); - } else { - // merge $data into $collection - foreach ($collection as $entity) { - if (!$data->contains($entity)) { - $collection->removeElement($entity); - } else { - $data->removeElement($entity); - } - } - - foreach ($data as $entity) { - $collection->add($entity); - } } - - $event->setData($collection); } } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 0937c1c001..1c634fb5b0 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -16,7 +16,7 @@ use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\FormBuilder; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; -use Symfony\Bridge\Doctrine\Form\EventListener\MergeCollectionListener; +use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Component\Form\AbstractType; @@ -36,7 +36,7 @@ abstract class DoctrineType extends AbstractType { if ($options['multiple']) { $builder - ->addEventSubscriber(new MergeCollectionListener()) + ->addEventSubscriber(new MergeDoctrineCollectionListener()) ->prependClientTransformer(new CollectionToArrayTransformer()) ; } diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php new file mode 100644 index 0000000000..7bfab6888f --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/MergeCollectionListener.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\EventListener; + +use Symfony\Component\Form\Util\FormUtil; + +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\Event\FilterDataEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * @author Bernhard Schussek + */ +class MergeCollectionListener implements EventSubscriberInterface +{ + /** + * Whether elements may be added to the collection + * @var Boolean + */ + private $allowAdd; + + /** + * Whether elements may be removed from the collection + * @var Boolean + */ + private $allowDelete; + + public function __construct($allowAdd = false, $allowDelete = false) + { + $this->allowAdd = $allowAdd; + $this->allowDelete = $allowDelete; + } + + static public function getSubscribedEvents() + { + return array(FormEvents::BIND_NORM_DATA => 'onBindNormData'); + } + + public function onBindNormData(FilterDataEvent $event) + { + $originalData = $event->getForm()->getData(); + $form = $event->getForm(); + $data = $event->getData(); + $parentData = $form->hasParent() ? $form->getParent()->getData() : null; + $adder = null; + $remover = null; + + if (null === $data) { + $data = array(); + } + + if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { + throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); + } + + if (null !== $originalData && !is_array($originalData) && !($originalData instanceof \Traversable && $originalData instanceof \ArrayAccess)) { + throw new UnexpectedTypeException($originalData, 'array or (\Traversable and \ArrayAccess)'); + } + + // Check if the parent has matching methods to add/remove items + if (is_object($parentData)) { + $plural = ucfirst($form->getName()); + $singulars = (array) FormUtil::singularify($plural); + $reflClass = new \ReflectionClass($parentData); + + foreach ($singulars as $singular) { + $adderName = 'add' . $singular; + $removerName = 'remove' . $singular; + + if ($reflClass->hasMethod($adderName) && $reflClass->hasMethod($removerName)) { + $adder = $reflClass->getMethod($adderName); + $remover = $reflClass->getMethod($removerName); + + if ($adder->isPublic() && $adder->getNumberOfRequiredParameters() === 1 + && $remover->isPublic() && $remover->getNumberOfRequiredParameters() === 1) { + + // We found a public, one-parameter add and remove method + break; + } + + // False alert + $adder = null; + $remover = null; + } + } + } + + // Check which items are in $data that are not in $originalData and + // vice versa + $itemsToDelete = array(); + $itemsToAdd = is_object($data) ? clone $data : $data; + + if ($originalData) { + foreach ($originalData as $originalKey => $originalItem) { + foreach ($data as $key => $item) { + if ($item === $originalItem) { + // Item found, next original item + unset($itemsToAdd[$key]); + continue 2; + } + } + + // Item not found, remember for deletion + $itemsToDelete[$originalKey] = $originalItem; + } + } + + if ($adder && $remover) { + // If methods to add and to remove exist, call them now, if allowed + if ($this->allowDelete) { + foreach ($itemsToDelete as $item) { + $remover->invoke($parentData, $item); + } + } + + if ($this->allowAdd) { + foreach ($itemsToAdd as $item) { + $adder->invoke($parentData, $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($originalData); + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php index 30159bf637..5ac3994ff9 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php @@ -66,7 +66,8 @@ class ResizeFormListener implements EventSubscriberInterface return array( FormEvents::PRE_SET_DATA => 'preSetData', FormEvents::PRE_BIND => 'preBind', - FormEvents::BIND_NORM_DATA => 'onBindNormData', + // (MergeCollectionListener, MergeDoctrineCollectionListener) + FormEvents::BIND_NORM_DATA => array('onBindNormData', 50), ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 088043eacb..e629d37747 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -19,6 +19,7 @@ use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener; +use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer; @@ -80,7 +81,10 @@ class ChoiceType extends AbstractType if ($options['expanded']) { if ($options['multiple']) { - $builder->appendClientTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list'])); + $builder + ->appendClientTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list'])) + ->addEventSubscriber(new MergeCollectionListener(true, true)) + ; } else { $builder ->appendClientTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'])) @@ -89,12 +93,14 @@ class ChoiceType extends AbstractType } } else { if ($options['multiple']) { - $builder->appendClientTransformer(new ChoicesToValuesTransformer($options['choice_list'])); + $builder + ->appendClientTransformer(new ChoicesToValuesTransformer($options['choice_list'])) + ->addEventSubscriber(new MergeCollectionListener(true, true)) + ; } else { $builder->appendClientTransformer(new ChoiceToValueTransformer($options['choice_list'])); } } - } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php index 20ac8345f1..748370a007 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php @@ -16,6 +16,7 @@ use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener; +use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener; class CollectionType extends AbstractType { @@ -29,7 +30,7 @@ class CollectionType extends AbstractType $builder->setAttribute('prototype', $prototype->getForm()); } - $listener = new ResizeFormListener( + $resizeListener = new ResizeFormListener( $builder->getFormFactory(), $options['type'], $options['options'], @@ -37,8 +38,14 @@ class CollectionType extends AbstractType $options['allow_delete'] ); + $mergeListener = new MergeCollectionListener( + $options['allow_add'], + $options['allow_delete'] + ); + $builder - ->addEventSubscriber($listener) + ->addEventSubscriber($resizeListener) + ->addEventSubscriber($mergeListener) ->setAttribute('allow_add', $options['allow_add']) ->setAttribute('allow_delete', $options['allow_delete']) ; diff --git a/src/Symfony/Component/Form/Util/FormUtil.php b/src/Symfony/Component/Form/Util/FormUtil.php index 284f02e1e7..9596796add 100644 --- a/src/Symfony/Component/Form/Util/FormUtil.php +++ b/src/Symfony/Component/Form/Util/FormUtil.php @@ -83,8 +83,8 @@ abstract class FormUtil // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) array('sev', 3, true, true, 'f'), - // axes (axis) - array('sexa', 4, false, false, 'axis'), + // axes (axis), axes (ax), axes (axe) + array('sexa', 4, false, false, array('ax', 'axe', 'axis')), // indexes (index), matrixes (matrix) array('sex', 3, true, false, 'x'), diff --git a/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php b/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php index 8c4f65abb1..20d759d579 100644 --- a/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php +++ b/tests/Symfony/Tests/Bridge/Doctrine/Form/Type/EntityTypeTest.php @@ -344,13 +344,13 @@ class EntityTypeTest extends TypeTestCase 'property' => 'name', )); - $existing = new ArrayCollection(array($entity2)); + $existing = new ArrayCollection(array(0 => $entity2)); $field->setData($existing); $field->bind(array('1', '3')); - // entry with index 0 was removed - $expected = new ArrayCollection(array(1 => $entity1, 2 => $entity3)); + // entry with index 0 ($entity2) was replaced + $expected = new ArrayCollection(array(0 => $entity1, 1 => $entity3)); $this->assertTrue($field->isSynchronized()); $this->assertEquals($expected, $field->getData()); @@ -406,8 +406,8 @@ class EntityTypeTest extends TypeTestCase $field->setData($existing); $field->bind(array('0', '2')); - // entry with index 0 was removed - $expected = new ArrayCollection(array(1 => $entity1, 2 => $entity3)); + // entry with index 0 ($entity2) was replaced + $expected = new ArrayCollection(array(0 => $entity1, 1 => $entity3)); $this->assertTrue($field->isSynchronized()); $this->assertEquals($expected, $field->getData()); diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerArrayObjectTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerArrayObjectTest.php new file mode 100644 index 0000000000..96cece7532 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerArrayObjectTest.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\Form\Extension\Core\EventListener; + +class MergeCollectionListenerArrayObjectTest extends MergeCollectionListenerTest +{ + protected function getData(array $data) + { + return new \ArrayObject($data); + } +} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerArrayTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerArrayTest.php new file mode 100644 index 0000000000..c150658bb4 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerArrayTest.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\Form\Extension\Core\EventListener; + +class MergeCollectionListenerArrayTest extends MergeCollectionListenerTest +{ + protected function getData(array $data) + { + return $data; + } +} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerCustomArrayObjectTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerCustomArrayObjectTest.php new file mode 100644 index 0000000000..59dec2aa0b --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerCustomArrayObjectTest.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\Form\Extension\Core\EventListener; + +/** +* This class is a hand written simplified version of PHP native `ArrayObject` +* class, to show that it behaves differently than the PHP native implementation. +*/ +class MergeCollectionListenerCustomArrayObjectTest_CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable +{ + private $array; + + public function __construct(array $array = null) + { + $this->array = (array) ($array ?: array()); + } + + public function offsetExists($offset) + { + return array_key_exists($offset, $this->array); + } + + public function offsetGet($offset) + { + return $this->array[$offset]; + } + + public function offsetSet($offset, $value) + { + if (null === $offset) { + $this->array[] = $value; + } else { + $this->array[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + if (array_key_exists($offset, $this->array)) { + unset($this->array[$offset]); + } + } + + public function getIterator() + { + return new \ArrayIterator($this->array); + } + + public function count() + { + return count($this->array); + } + + public function serialize() + { + return serialize($this->array); + } + + public function unserialize($serialized) + { + $this->array = (array) unserialize((string) $serialized); + } +} + +class MergeCollectionListenerCustomArrayObjectTest extends MergeCollectionListenerTest +{ + protected function getData(array $data) + { + $class = __CLASS__ . '_CustomArrayObject'; + + return new $class($data); + } +} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php new file mode 100644 index 0000000000..4f6463ca84 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/EventListener/MergeCollectionListenerTest.php @@ -0,0 +1,326 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\Form\Extension\Core\EventListener; + +use Symfony\Component\Form\Event\DataEvent; +use Symfony\Component\Form\Event\FilterDataEvent; +use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener; +use Symfony\Component\Form\FormBuilder; + +class MergeCollectionListenerTest_Car +{ + // In the test, use a name that FormUtil can't uniquely singularify + public function addAxis($axis) {} + + public function removeAxis($axis) {} +} + +abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase +{ + private $dispatcher; + private $factory; + private $form; + + public function setUp() + { + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + $this->form = $this->getForm('axes'); + } + + protected function tearDown() + { + $this->dispatcher = null; + $this->factory = null; + $this->form = null; + } + + protected function getBuilder($name = 'name') + { + return new FormBuilder($name, $this->factory, $this->dispatcher); + } + + protected function getForm($name = 'name') + { + return $this->getBuilder($name)->getForm(); + } + + protected function getMockForm() + { + return $this->getMock('Symfony\Tests\Component\Form\FormInterface'); + } + + abstract protected function getData(array $data); + + public function testAddExtraEntriesIfAllowAdd() + { + $originalData = $this->getData(array(1 => 'second')); + $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $this->form->setData($originalData); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(true, false); + $listener->onBindNormData($event); + + // The original object was modified + if (is_object($originalData)) { + $this->assertSame($originalData, $event->getData()); + } + + // The original object matches the new object + $this->assertEquals($newData, $event->getData()); + } + + public function testAddExtraEntriesIfAllowAddDontOverwriteExistingIndices() + { + $originalData = $this->getData(array(1 => 'first')); + $newData = $this->getData(array(0 => 'first', 1 => 'second')); + + $this->form->setData($originalData); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(true, false); + $listener->onBindNormData($event); + + // The original object was modified + if (is_object($originalData)) { + $this->assertSame($originalData, $event->getData()); + } + + // The original object matches the new object + $this->assertEquals($this->getData(array(1 => 'first', 2 => 'second')), $event->getData()); + } + + public function testDoNothingIfNotAllowAdd() + { + $originalDataArray = array(1 => 'second'); + $originalData = $this->getData($originalDataArray); + $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $this->form->setData($originalData); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(false, false); + $listener->onBindNormData($event); + + // We still have the original object + if (is_object($originalData)) { + $this->assertSame($originalData, $event->getData()); + } + + // Nothing was removed + $this->assertEquals($this->getData($originalDataArray), $event->getData()); + } + + public function testRemoveMissingEntriesIfAllowDelete() + { + $originalData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $newData = $this->getData(array(1 => 'second')); + + $this->form->setData($originalData); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(false, true); + $listener->onBindNormData($event); + + // The original object was modified + if (is_object($originalData)) { + $this->assertSame($originalData, $event->getData()); + } + + // The original object matches the new object + $this->assertEquals($newData, $event->getData()); + } + + public function testDoNothingIfNotAllowDelete() + { + $originalDataArray = array(0 => 'first', 1 => 'second', 2 => 'third'); + $originalData = $this->getData($originalDataArray); + $newData = $this->getData(array(1 => 'second')); + + $this->form->setData($originalData); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(false, false); + $listener->onBindNormData($event); + + // We still have the original object + if (is_object($originalData)) { + $this->assertSame($originalData, $event->getData()); + } + + // Nothing was removed + $this->assertEquals($this->getData($originalDataArray), $event->getData()); + } + + /** + * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testRequireArrayOrTraversable() + { + $newData = 'no array or traversable'; + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(false, false); + $listener->onBindNormData($event); + } + + public function testDealWithNullData() + { + $originalData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $newData = null; + + $this->form->setData($originalData); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(false, false); + $listener->onBindNormData($event); + + $this->assertSame($originalData, $event->getData()); + } + + public function testDealWithNullOriginalDataIfAllowAdd() + { + $originalData = null; + $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $this->form->setData($originalData); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(true, false); + $listener->onBindNormData($event); + + $this->assertSame($newData, $event->getData()); + } + + public function testDontDealWithNullOriginalDataIfNotAllowAdd() + { + $originalData = null; + $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $this->form->setData($originalData); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(false, false); + $listener->onBindNormData($event); + + $this->assertNull($event->getData()); + } + + public function testCallAdderIfAllowAdd() + { + $parentData = $this->getMock(__CLASS__ . '_Car'); + $parentForm = $this->getForm('article'); + $parentForm->setData($parentData); + $parentForm->add($this->form); + + $originalData = $this->getData(array(1 => 'second')); + $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $this->form->setData($originalData); + + $parentData->expects($this->at(0)) + ->method('addAxis') + ->with('first'); + $parentData->expects($this->at(1)) + ->method('addAxis') + ->with('third'); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(true, false); + $listener->onBindNormData($event); + + // The original object was modified + if (is_object($originalData)) { + $this->assertSame($originalData, $event->getData()); + } + } + + public function testDontCallAdderIfNotAllowAdd() + { + $parentData = $this->getMock(__CLASS__ . '_Car'); + $parentForm = $this->getForm('article'); + $parentForm->setData($parentData); + $parentForm->add($this->form); + + $originalData = $this->getData(array(1 => 'second')); + $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + + $this->form->setData($originalData); + + $parentData->expects($this->never()) + ->method('addAxis'); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(false, false); + $listener->onBindNormData($event); + + // The original object was modified + if (is_object($originalData)) { + $this->assertSame($originalData, $event->getData()); + } + } + + public function testCallRemoverIfAllowDelete() + { + $parentData = $this->getMock(__CLASS__ . '_Car'); + $parentForm = $this->getForm('article'); + $parentForm->setData($parentData); + $parentForm->add($this->form); + + $originalData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $newData = $this->getData(array(1 => 'second')); + + $this->form->setData($originalData); + + $parentData->expects($this->at(0)) + ->method('removeAxis') + ->with('first'); + $parentData->expects($this->at(1)) + ->method('removeAxis') + ->with('third'); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(false, true); + $listener->onBindNormData($event); + + // The original object was modified + if (is_object($originalData)) { + $this->assertSame($originalData, $event->getData()); + } + } + + public function testDontCallRemoverIfNotAllowDelete() + { + $parentData = $this->getMock(__CLASS__ . '_Car'); + $parentForm = $this->getForm('article'); + $parentForm->setData($parentData); + $parentForm->add($this->form); + + $originalData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third')); + $newData = $this->getData(array(1 => 'second')); + + $this->form->setData($originalData); + + $parentData->expects($this->never()) + ->method('removeAxis'); + + $event = new FilterDataEvent($this->form, $newData); + $listener = new MergeCollectionListener(false, false); + $listener->onBindNormData($event); + + // The original object was modified + if (is_object($originalData)) { + $this->assertSame($originalData, $event->getData()); + } + } +} diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php index 615a1d4fdc..5a082f4701 100644 --- a/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/Type/CollectionTypeTest.php @@ -13,7 +13,7 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type; use Symfony\Component\Form\Form; -class CollectionFormTest extends TypeTestCase +class CollectionTypeTest extends TypeTestCase { public function testContainsNoFieldByDefault() { @@ -80,12 +80,12 @@ class CollectionFormTest extends TypeTestCase 'allow_delete' => true, )); $form->setData(array('foo@foo.com', 'bar@bar.com')); - $form->bind(array('foo@bar.com')); + $form->bind(array('foo@foo.com')); $this->assertTrue($form->has('0')); $this->assertFalse($form->has('1')); - $this->assertEquals('foo@bar.com', $form[0]->getData()); - $this->assertEquals(array('foo@bar.com'), $form->getData()); + $this->assertEquals('foo@foo.com', $form[0]->getData()); + $this->assertEquals(array('foo@foo.com'), $form->getData()); } public function testNotResizedIfBoundWithExtraData() @@ -108,13 +108,13 @@ class CollectionFormTest extends TypeTestCase 'allow_add' => true, )); $form->setData(array('foo@bar.com')); - $form->bind(array('foo@foo.com', 'bar@bar.com')); + $form->bind(array('foo@bar.com', 'bar@bar.com')); $this->assertTrue($form->has('0')); $this->assertTrue($form->has('1')); - $this->assertEquals('foo@foo.com', $form[0]->getData()); + $this->assertEquals('foo@bar.com', $form[0]->getData()); $this->assertEquals('bar@bar.com', $form[1]->getData()); - $this->assertEquals(array('foo@foo.com', 'bar@bar.com'), $form->getData()); + $this->assertEquals(array('foo@bar.com', 'bar@bar.com'), $form->getData()); } public function testAllowAddButNoPrototype() diff --git a/tests/Symfony/Tests/Component/Form/Util/FormUtilTest.php b/tests/Symfony/Tests/Component/Form/Util/FormUtilTest.php index 5fc1d0736c..bacba206be 100644 --- a/tests/Symfony/Tests/Component/Form/Util/FormUtilTest.php +++ b/tests/Symfony/Tests/Component/Form/Util/FormUtilTest.php @@ -87,7 +87,7 @@ class FormUtilTest extends \PHPUnit_Framework_TestCase array('alumni', 'alumnus'), array('funguses', array('fungus', 'funguse', 'fungusis')), array('fungi', 'fungus'), - array('axes', 'axis'), + array('axes', array('ax', 'axe', 'axis')), array('appendices', array('appendex', 'appendix')), array('indices', array('index', 'indix')), array('indexes', 'index'), @@ -123,7 +123,6 @@ class FormUtilTest extends \PHPUnit_Framework_TestCase array('houses', array('hous', 'house', 'housis')), array('arches', array('arch', 'arche')), array('atlases', array('atlas', 'atlase', 'atlasis')), -// array('axes', 'axe'), array('batches', array('batch', 'batche')), array('bushes', array('bush', 'bushe')), array('buses', array('bus', 'buse', 'busis')),