diff --git a/src/Symfony/Component/Form/CollectionField.php b/src/Symfony/Component/Form/CollectionField.php index 515340bdf4..53bfc56bc0 100644 --- a/src/Symfony/Component/Form/CollectionField.php +++ b/src/Symfony/Component/Form/CollectionField.php @@ -74,79 +74,4 @@ class CollectionField extends Form parent::configure(); } - - public function setData($collection) - { - if (null === $collection) { - $collection = array(); - } - - if (!is_array($collection) && !$collection instanceof \Traversable) { - throw new UnexpectedTypeException($collection, 'array or \Traversable'); - } - - foreach ($this as $name => $field) { - if (!$this->modifiable || '$$key$$' != $name) { - $this->remove($name); - } - } - - foreach ($collection as $name => $value) { - $this->add($this->newField($name, $name)); - } - - parent::setData($collection); - } - - public function submit($data) - { - $this->removedFields = array(); - - if (null === $data) { - $data = array(); - } - - foreach ($this as $name => $field) { - if (!isset($data[$name]) && $this->modifiable && '$$key$$' != $name) { - $this->remove($name); - $this->removedFields[] = $name; - } - } - - foreach ($data as $name => $value) { - if (!isset($this[$name]) && $this->modifiable) { - $this->add($this->newField($name, $name)); - } - } - - parent::submit($data); - } - - protected function writeObject(&$objectOrArray) - { - parent::writeObject($objectOrArray); - - foreach ($this->removedFields as $name) { - unset($objectOrArray[$name]); - } - } - - protected function newField($key, $propertyPath) - { - if (null !== $propertyPath) { - $propertyPath = '['.$propertyPath.']'; - } - - if ($this->prototype) { - $field = clone $this->prototype; - $field->setKey($key); - $field->setPropertyPath($propertyPath); - } else { - $field = new TextField($key, array( - 'property_path' => $propertyPath, - )); - } - - return $field; - } } \ No newline at end of file diff --git a/src/Symfony/Component/Form/EventListener/EventListenerInterface.php b/src/Symfony/Component/Form/EventListener/EventListenerInterface.php new file mode 100644 index 0000000000..53cd03e917 --- /dev/null +++ b/src/Symfony/Component/Form/EventListener/EventListenerInterface.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\EventListener; + +interface EventListenerInterface +{ + function getSupportedEvents(); +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/EventListener/EventManager.php b/src/Symfony/Component/Form/EventListener/EventManager.php new file mode 100644 index 0000000000..715902fafd --- /dev/null +++ b/src/Symfony/Component/Form/EventListener/EventManager.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\EventListener; + +class EventManager +{ + private $listeners = array(); + + private $supportedEvents = array(); + + public function __construct(array $supportedEvents) + { + $this->supportedEvents = $supportedEvents; + } + + public function addEventListener(EventListenerInterface $listener) + { + foreach ((array)$listener->getSupportedEvents() as $event) { + // TODO check whether the listener has the $event method + + if (!isset($this->listeners[$event])) { + $this->listeners[$event] = array(); + } + + $this->listeners[$event][] = $listener; + } + } + + public function triggerEvent($event, $data = null) + { + if (isset($this->listeners[$event])) { + foreach ($this->listeners[$event] as $listener) { + $listener->$event($data); + } + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/EventListener/ResizeFormListener.php b/src/Symfony/Component/Form/EventListener/ResizeFormListener.php new file mode 100644 index 0000000000..a31bcfa8c0 --- /dev/null +++ b/src/Symfony/Component/Form/EventListener/ResizeFormListener.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\EventListener; + +use Symfony\Component\Form\Events; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FieldInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +class ResizeFormListener implements EventListenerInterface +{ + private $form; + + private $prototype; + + private $resizeOnBind; + + public function __construct(FormInterface $form, FieldInterface $prototype, $resizeOnBind = false) + { + $this->form = $form; + $this->prototype = $prototype; + $this->resizeOnBind = $resizeOnBind; + } + + public function getSupportedEvents() + { + return array( + Events::preSetData, + Events::preBind, + ); + } + + public function preSetData($collection) + { + if (null === $collection) { + $collection = array(); + } + + if (!is_array($collection) && !$collection instanceof \Traversable) { + throw new UnexpectedTypeException($collection, 'array or \Traversable'); + } + + foreach ($this->form as $name => $field) { + if (!$this->resizeOnBind || '$$key$$' != $name) { + $this->form->remove($name); + } + } + + foreach ($collection as $name => $value) { + $this->form->add($this->newField($name)); + } + } + + public function preBind($data) + { + $this->removedFields = array(); + + if (null === $data) { + $data = array(); + } + + foreach ($this->form as $name => $field) { + if (!isset($data[$name]) && $this->resizeOnBind && '$$key$$' != $name) { + $this->form->remove($name); + $this->removedFields[] = $name; + } + } + + foreach ($data as $name => $value) { + if (!$this->form->has($name) && $this->resizeOnBind) { + $this->form->add($this->newField($name)); + } + } + } + + protected function newField($key) + { + $field = clone $this->prototype; + $field->setKey($key); + $field->setPropertyPath('['.$key.']'); + + return $field; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/Events.php b/src/Symfony/Component/Form/Events.php new file mode 100644 index 0000000000..bf1853f6b0 --- /dev/null +++ b/src/Symfony/Component/Form/Events.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * @author Bernhard Schussek + */ +final class Events +{ + const preBind = 'preBind'; + + const postBind = 'postBind'; + + const preSetData = 'preSetData'; + + const postSetData = 'postSetData'; + + public static $all = array( + self::preBind, + self::postBind, + self::preSetData, + self::postSetData, + ); +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/Field.php b/src/Symfony/Component/Form/Field.php index 9b24df74b8..10745bde09 100644 --- a/src/Symfony/Component/Form/Field.php +++ b/src/Symfony/Component/Form/Field.php @@ -15,9 +15,10 @@ use Symfony\Component\Form\ValueTransformer\ValueTransformerInterface; use Symfony\Component\Form\ValueTransformer\TransformationFailedException; use Symfony\Component\Form\Renderer\RendererInterface; use Symfony\Component\Form\Renderer\Plugin\PluginInterface; -use Symfony\Component\Form\Filter\FilterChainInterface; use Symfony\Component\Form\Filter\FilterChain; use Symfony\Component\Form\Filter\FilterInterface; +use Symfony\Component\Form\EventListener\EventManager; +use Symfony\Component\Form\EventListener\EventListenerInterface; /** * Base class for form fields @@ -70,12 +71,14 @@ class Field implements FieldInterface private $trim = true; private $disabled = false; private $filterChain; + private $eventManager; public function __construct($key = null) { $this->key = (string)$key; // TODO should be injected instead $this->filterChain = new FilterChain(Filters::$all); + $this->eventManager = new EventManager(Events::$all); } /** @@ -98,6 +101,16 @@ class Field implements FieldInterface return $this; } + /** + * @deprecated + */ + public function addEventListener(EventListenerInterface $listener) + { + $this->eventManager->addEventListener($listener); + + return $this; + } + /** * Clones this field. */ @@ -268,6 +281,8 @@ class Field implements FieldInterface */ public function setData($appData) { + $this->eventManager->triggerEvent(Events::preSetData, $appData); + // Hook to change content of the data $appData = $this->filterChain->filter(Filters::filterSetData, $appData); @@ -284,6 +299,8 @@ class Field implements FieldInterface $this->normalizedData = $normData; $this->transformedData = $clientData; + $this->eventManager->triggerEvent(Events::postSetData); + return $this; } @@ -294,13 +311,12 @@ class Field implements FieldInterface */ public function submit($clientData) { - $this->submitted = true; - $this->errors = array(); - if (is_scalar($clientData) || null === $clientData) { $clientData = (string)$clientData; } + $this->eventManager->triggerEvent(Events::preBind, $clientData); + if (is_string($clientData) && $this->trim) { $clientData = trim($clientData); } @@ -329,9 +345,13 @@ class Field implements FieldInterface $clientData = $this->transform($normData); } + $this->submitted = true; + $this->errors = array(); $this->data = $appData; $this->normalizedData = $normData; $this->transformedData = $clientData; + + $this->eventManager->triggerEvent(Events::postBind); } /** diff --git a/src/Symfony/Component/Form/FormFactory.php b/src/Symfony/Component/Form/FormFactory.php index a01de2fc65..5c4a9259ee 100644 --- a/src/Symfony/Component/Form/FormFactory.php +++ b/src/Symfony/Component/Form/FormFactory.php @@ -18,6 +18,7 @@ use Symfony\Component\Form\ChoiceList\MonthChoiceList; use Symfony\Component\Form\ChoiceList\TimeZoneChoiceList; use Symfony\Component\Form\ChoiceList\EntityChoiceList; use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Form\EventListener\ResizeFormListener; use Symfony\Component\Form\Filter\RadioInputFilter; use Symfony\Component\Form\Filter\FixUrlProtocolFilter; use Symfony\Component\Form\Filter\FileUploadFilter; @@ -908,4 +909,33 @@ class FormFactory ->add($this->getHiddenField('token')) ->add($this->getHiddenField('name')); } + + public function getCollectionField($key, array $options = array()) + { + $options = array_merge(array( + 'template' => 'collection', + 'prototype' => null, + 'modifiable' => false, + ), $options); + + $field = $this->getForm($key, $options); + + if (!isset($options['prototype'])) { + $options['prototype'] = $this->getTextField('prototype'); + } + + if ($options['modifiable']) { + $child = clone $options['prototype']; + $child->setKey('$$key$$'); + $child->setPropertyPath(null); + // TESTME + $child->setRequired(false); + $field->add($child); + } + + $field->addEventListener(new ResizeFormListener($field, + $options['prototype'], $options['modifiable'])); + + return $field; + } } \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Form/CollectionFieldTest.php b/tests/Symfony/Tests/Component/Form/CollectionFieldTest.php index 63ce736505..15af52450e 100644 --- a/tests/Symfony/Tests/Component/Form/CollectionFieldTest.php +++ b/tests/Symfony/Tests/Component/Form/CollectionFieldTest.php @@ -11,15 +11,17 @@ namespace Symfony\Tests\Component\Form; +require_once __DIR__.'/TestCase.php'; + use Symfony\Component\Form\CollectionField; use Symfony\Component\Form\Form; use Symfony\Component\Form\Field; -class CollectionFieldTest extends \PHPUnit_Framework_TestCase +class CollectionFieldTest extends TestCase { public function testContainsNoFieldsByDefault() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), )); $this->assertEquals(0, count($field)); @@ -27,7 +29,7 @@ class CollectionFieldTest extends \PHPUnit_Framework_TestCase public function testSetDataAdjustsSize() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), )); $field->setData(array('foo@foo.com', 'foo@bar.com')); @@ -47,7 +49,7 @@ class CollectionFieldTest extends \PHPUnit_Framework_TestCase public function testSetDataAdjustsSizeIfModifiable() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), 'modifiable' => true, )); @@ -67,7 +69,7 @@ class CollectionFieldTest extends \PHPUnit_Framework_TestCase public function testThrowsExceptionIfObjectIsNotTraversable() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), )); $this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException'); @@ -76,7 +78,7 @@ class CollectionFieldTest extends \PHPUnit_Framework_TestCase public function testModifiableCollectionsContainExtraField() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), 'modifiable' => true, )); @@ -89,7 +91,7 @@ class CollectionFieldTest extends \PHPUnit_Framework_TestCase public function testNotResizedIfSubmittedWithMissingData() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), )); $field->setData(array('foo@foo.com', 'bar@bar.com')); @@ -103,7 +105,7 @@ class CollectionFieldTest extends \PHPUnit_Framework_TestCase public function testResizedIfSubmittedWithMissingDataAndModifiable() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), 'modifiable' => true, )); @@ -117,7 +119,7 @@ class CollectionFieldTest extends \PHPUnit_Framework_TestCase public function testNotResizedIfSubmittedWithExtraData() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), )); $field->setData(array('foo@bar.com')); @@ -130,7 +132,7 @@ class CollectionFieldTest extends \PHPUnit_Framework_TestCase public function testResizedUpIfSubmittedWithExtraDataAndModifiable() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), 'modifiable' => true, )); @@ -146,7 +148,7 @@ class CollectionFieldTest extends \PHPUnit_Framework_TestCase public function testResizedDownIfSubmittedWithLessDataAndModifiable() { - $field = new CollectionField('emails', array( + $field = $this->factory->getCollectionField('emails', array( 'prototype' => new Field(), 'modifiable' => true, ));