diff --git a/src/Symfony/Component/Form/Button.php b/src/Symfony/Component/Form/Button.php index 6e12ba163c..aecbabaebe 100644 --- a/src/Symfony/Component/Form/Button.php +++ b/src/Symfony/Component/Form/Button.php @@ -200,7 +200,8 @@ class Button implements \IteratorAggregate, FormInterface */ public function setData($modelData) { - throw new BadMethodCallException('Buttons cannot have data.'); + // called during initialization of the form tree + // noop } /** diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php index ae4796e5e7..d5942f2a55 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -35,7 +35,7 @@ class PropertyPathMapper implements DataMapperInterface */ public function __construct(PropertyAccessorInterface $propertyAccessor = null) { - $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor(); + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } /** @@ -43,11 +43,9 @@ class PropertyPathMapper implements DataMapperInterface */ public function mapDataToForms($data, $forms) { - if (null === $data || array() === $data) { - return; - } + $empty = null === $data || array() === $data; - if (!is_array($data) && !is_object($data)) { + if (!$empty && !is_array($data) && !is_object($data)) { throw new UnexpectedTypeException($data, 'object, array or empty'); } @@ -55,8 +53,10 @@ class PropertyPathMapper implements DataMapperInterface $propertyPath = $form->getPropertyPath(); $config = $form->getConfig(); - if (null !== $propertyPath && $config->getMapped()) { + if (!$empty && null !== $propertyPath && $config->getMapped()) { $form->setData($this->propertyAccessor->getValue($data, $propertyPath)); + } else { + $form->setData($form->getConfig()->getData()); } } } diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index bcab8afdc2..556c190d8e 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -536,7 +536,10 @@ class Form implements \IteratorAggregate, FormInterface $submittedData = array(); } - foreach ($this->children as $name => $child) { + for (reset($this->children); false !== current($this->children); next($this->children)) { + $child = current($this->children); + $name = key($this->children); + if (array_key_exists($name, $submittedData) || $clearMissing) { $child->submit(isset($submittedData[$name]) ? $submittedData[$name] : null, $clearMissing); unset($submittedData[$name]); @@ -762,7 +765,7 @@ class Form implements \IteratorAggregate, FormInterface /** * {@inheritdoc} */ - public function all() + public function &all() { return $this->children; } @@ -833,7 +836,8 @@ class Form implements \IteratorAggregate, FormInterface $child->setParent($this); if (!$this->lockSetData && $this->defaultDataSet && !$this->config->getInheritData()) { - $childrenIterator = new InheritDataAwareIterator(array($child)); + $children = array($child); + $childrenIterator = new InheritDataAwareIterator($children); $childrenIterator = new \RecursiveIteratorIterator($childrenIterator); $this->config->getDataMapper()->mapDataToForms($viewData, $childrenIterator); } diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index 4c5cfb9a48..bc0c57d33f 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Form\Tests; +use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Forms; @@ -372,6 +373,41 @@ class CompoundFormTest extends AbstractFormTest $form->add($child); } + public function testSetDataSupportsDynamicAdditionAndRemovalOfChildren() + { + $form = $this->getBuilder() + ->setCompound(true) + // We test using PropertyPathMapper on purpose. The traversal logic + // is currently contained in InheritDataAwareIterator, but even + // if that changes, this test should still function. + ->setDataMapper(new PropertyPathMapper()) + ->getForm(); + + $child = $this->getMockForm('child'); + $childToBeRemoved = $this->getMockForm('removed'); + $childToBeAdded = $this->getMockForm('added'); + + $form->add($child); + $form->add($childToBeRemoved); + + $child->expects($this->once()) + ->method('setData') + ->will($this->returnCallback(function () use ($form, $childToBeAdded) { + $form->remove('removed'); + $form->add($childToBeAdded); + })); + + $childToBeRemoved->expects($this->never()) + ->method('setData'); + + // once when it it is created, once when it is added + $childToBeAdded->expects($this->exactly(2)) + ->method('setData'); + + // pass NULL to all children + $form->setData(array()); + } + public function testSetDataMapsViewDataToChildren() { $test = $this; @@ -399,6 +435,34 @@ class CompoundFormTest extends AbstractFormTest $form->setData('foo'); } + public function testSubmitSupportsDynamicAdditionAndRemovalOfChildren() + { + $child = $this->getMockForm('child'); + $childToBeRemoved = $this->getMockForm('removed'); + $childToBeAdded = $this->getMockForm('added'); + + $this->form->add($child); + $this->form->add($childToBeRemoved); + + $form = $this->form; + + $child->expects($this->once()) + ->method('submit') + ->will($this->returnCallback(function () use ($form, $childToBeAdded) { + $form->remove('removed'); + $form->add($childToBeAdded); + })); + + $childToBeRemoved->expects($this->never()) + ->method('submit'); + + $childToBeAdded->expects($this->once()) + ->method('submit'); + + // pass NULL to all children + $this->form->submit(array()); + } + public function testSubmitMapsSubmittedChildrenOntoExistingViewData() { $test = $this; diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php index ed6d8cb01e..faa10e2a7a 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php @@ -169,8 +169,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $this->assertNull($form->getData()); } - public function testMapDataToFormsIgnoresEmptyData() + public function testMapDataToFormsSetsDefaultDataIfPassedDataIsNull() { + $default = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); $this->propertyAccessor->expects($this->never()) @@ -179,11 +180,43 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); $config->setByReference(true); $config->setPropertyPath($propertyPath); - $form = $this->getForm($config); + $config->setData($default); + + $form = $this->getMockBuilder('Symfony\Component\Form\Form') + ->setConstructorArgs(array($config)) + ->setMethods(array('setData')) + ->getMock(); + + $form->expects($this->once()) + ->method('setData') + ->with($default); $this->mapper->mapDataToForms(null, array($form)); + } - $this->assertNull($form->getData()); + public function testMapDataToFormsSetsDefaultDataIfPassedDataIsEmptyArray() + { + $default = new \stdClass(); + $propertyPath = $this->getPropertyPath('engine'); + + $this->propertyAccessor->expects($this->never()) + ->method('getValue'); + + $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); + $config->setByReference(true); + $config->setPropertyPath($propertyPath); + $config->setData($default); + + $form = $this->getMockBuilder('Symfony\Component\Form\Form') + ->setConstructorArgs(array($config)) + ->setMethods(array('setData')) + ->getMock(); + + $form->expects($this->once()) + ->method('setData') + ->with($default); + + $this->mapper->mapDataToForms(array(), array($form)); } public function testMapFormsToDataWritesBackIfNotByReference() diff --git a/src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php b/src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php new file mode 100644 index 0000000000..19a0940bc0 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Util; + +use Symfony\Component\Form\Util\InheritDataAwareIterator; + +/** + * @author Bernhard Schussek + */ +class InheritDataAwareIteratorTest extends \PHPUnit_Framework_TestCase +{ + public function testSupportDynamicModification() + { + $form = $this->getMockForm('form'); + $formToBeAdded = $this->getMockForm('added'); + $formToBeRemoved = $this->getMockForm('removed'); + + $forms = array('form' => $form, 'removed' => $formToBeRemoved); + $iterator = new InheritDataAwareIterator($forms); + + $iterator->rewind(); + $this->assertTrue($iterator->valid()); + $this->assertSame('form', $iterator->key()); + $this->assertSame($form, $iterator->current()); + + // dynamic modification + unset($forms['removed']); + $forms['added'] = $formToBeAdded; + + // continue iteration + $iterator->next(); + $this->assertTrue($iterator->valid()); + $this->assertSame('added', $iterator->key()); + $this->assertSame($formToBeAdded, $iterator->current()); + + // end of array + $iterator->next(); + $this->assertFalse($iterator->valid()); + } + + public function testSupportDynamicModificationInRecursiveCall() + { + $inheritingForm = $this->getMockForm('inheriting'); + $form = $this->getMockForm('form'); + $formToBeAdded = $this->getMockForm('added'); + $formToBeRemoved = $this->getMockForm('removed'); + + $inheritingForm->getConfig()->expects($this->any()) + ->method('getInheritData') + ->will($this->returnValue(true)); + + $inheritingForm->add($form); + $inheritingForm->add($formToBeRemoved); + + $forms = array('inheriting' => $inheritingForm); + $iterator = new InheritDataAwareIterator($forms); + + $iterator->rewind(); + $this->assertTrue($iterator->valid()); + $this->assertSame('inheriting', $iterator->key()); + $this->assertSame($inheritingForm, $iterator->current()); + $this->assertTrue($iterator->hasChildren()); + + // enter nested iterator + $nestedIterator = $iterator->getChildren(); + $this->assertSame('form', $nestedIterator->key()); + $this->assertSame($form, $nestedIterator->current()); + $this->assertFalse($nestedIterator->hasChildren()); + + // dynamic modification + $inheritingForm->remove('removed'); + $inheritingForm->add($formToBeAdded); + + // continue iteration - nested iterator discovers change in the form + $nestedIterator->next(); + $this->assertTrue($nestedIterator->valid()); + $this->assertSame('added', $nestedIterator->key()); + $this->assertSame($formToBeAdded, $nestedIterator->current()); + + // end of array + $nestedIterator->next(); + $this->assertFalse($nestedIterator->valid()); + } + + /** + * @param string $name + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getMockForm($name = 'name') + { + $config = $this->getMock('Symfony\Component\Form\FormConfigInterface'); + + $config->expects($this->any()) + ->method('getName') + ->will($this->returnValue($name)); + $config->expects($this->any()) + ->method('getCompound') + ->will($this->returnValue(true)); + $config->expects($this->any()) + ->method('getDataMapper') + ->will($this->returnValue($this->getMock('Symfony\Component\Form\DataMapperInterface'))); + $config->expects($this->any()) + ->method('getEventDispatcher') + ->will($this->returnValue($this->getMock('Symfony\Component\EventDispatcher\EventDispatcher'))); + + return $this->getMockBuilder('Symfony\Component\Form\Form') + ->setConstructorArgs(array($config)) + ->disableArgumentCloning() + ->setMethods(array('getViewData')) + ->getMock(); + } +} diff --git a/src/Symfony/Component/Form/Util/InheritDataAwareIterator.php b/src/Symfony/Component/Form/Util/InheritDataAwareIterator.php index 5c2c5fadcc..ba157b7d18 100644 --- a/src/Symfony/Component/Form/Util/InheritDataAwareIterator.php +++ b/src/Symfony/Component/Form/Util/InheritDataAwareIterator.php @@ -12,24 +12,17 @@ namespace Symfony\Component\Form\Util; /** - * Iterator that returns only forms from a form tree that do not inherit their - * parent data. + * Iterator that traverses an array of forms. * - * If the iterator encounters a form that inherits its parent data, it enters - * the form and traverses its children as well. + * Contrary to \ArrayIterator, this iterator recognizes changes in the original + * array during iteration. + * + * You can wrap the iterator into a {@link \RecursiveIterator} in order to + * enter any child form that inherits its parent's data and iterate the children + * of that form as well. * * @author Bernhard Schussek */ class InheritDataAwareIterator extends VirtualFormAwareIterator { - /** - * Creates a new iterator. - * - * @param \Symfony\Component\Form\FormInterface[] $forms An array - */ - public function __construct(array $forms) - { - // Skip the deprecation error - \ArrayIterator::__construct($forms); - } } diff --git a/src/Symfony/Component/Form/Util/ReferencingArrayIterator.php b/src/Symfony/Component/Form/Util/ReferencingArrayIterator.php new file mode 100644 index 0000000000..9bb64d79d2 --- /dev/null +++ b/src/Symfony/Component/Form/Util/ReferencingArrayIterator.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +/** + * Iterator that traverses an array. + * + * Contrary to {@link \ArrayIterator}, this iterator recognizes changes in the + * original array during iteration. + * + * @author Bernhard Schussek + */ +class ReferencingArrayIterator implements \Iterator +{ + /** + * @var array + */ + private $array; + + /** + * Creates a new iterator. + * + * @param array $array An array + */ + public function __construct(array &$array) + { + $this->array = &$array; + } + + /** + *{@inheritdoc} + */ + public function current() + { + return current($this->array); + } + + /** + *{@inheritdoc} + */ + public function next() + { + next($this->array); + } + + /** + *{@inheritdoc} + */ + public function key() + { + return key($this->array); + } + + /** + *{@inheritdoc} + */ + public function valid() + { + return null !== key($this->array); + } + + /** + *{@inheritdoc} + */ + public function rewind() + { + reset($this->array); + } +} diff --git a/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php b/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php index 24fdc8bb9f..708726dedf 100644 --- a/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php +++ b/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php @@ -12,39 +12,35 @@ namespace Symfony\Component\Form\Util; /** - * Iterator that returns only forms from a form tree that do not inherit their - * parent data. + * Iterator that traverses an array of forms. * - * If the iterator encounters a form that inherits its parent data, it enters - * the form and traverses its children as well. + * Contrary to {@link \ArrayIterator}, this iterator recognizes changes in the + * original array during iteration. + * + * You can wrap the iterator into a {@link \RecursiveIterator} in order to + * enter any child form that inherits its parent's data and iterate the children + * of that form as well. * * @author Bernhard Schussek * * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use * {@link InheritDataAwareIterator} instead. */ -class VirtualFormAwareIterator extends \ArrayIterator implements \RecursiveIterator +class VirtualFormAwareIterator extends ReferencingArrayIterator implements \RecursiveIterator { /** - * Creates a new iterator. - * - * @param \Symfony\Component\Form\FormInterface[] $forms An array + * {@inheritdoc} */ - public function __construct(array $forms) - { - // Uncomment this as soon as the deprecation note should be shown - // trigger_error('VirtualFormAwareIterator is deprecated since version 2.3 and will be removed in 3.0. Use InheritDataAwareIterator instead.', E_USER_DEPRECATED); - - parent::__construct($forms); - } - public function getChildren() { return new static($this->current()->all()); } + /** + *{@inheritdoc} + */ public function hasChildren() { - return $this->current()->getConfig()->getInheritData(); + return (bool) $this->current()->getConfig()->getInheritData(); } } diff --git a/src/Symfony/Component/Routing/Route.php b/src/Symfony/Component/Routing/Route.php index 060e978116..5bc535c683 100644 --- a/src/Symfony/Component/Routing/Route.php +++ b/src/Symfony/Component/Routing/Route.php @@ -57,7 +57,7 @@ class Route implements \Serializable private $options = array(); /** - * @var null|RouteCompiler + * @var null|CompiledRoute */ private $compiled; diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 4905e71ac9..e86b7e5b9e 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -111,7 +111,7 @@ class Parser $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport); } } - } elseif (preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P.+?))?\s*$#u', $this->currentLine, $values)) { + } elseif (preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P.+?))?\s*$#u', $this->currentLine, $values) && false === strpos($values['key'],' #')) { if ($context && 'sequence' == $context) { throw new ParseException('You cannot define a mapping item when in a sequence'); } diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/sfComments.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/sfComments.yml index 34225e11e0..46addfcd3d 100644 --- a/src/Symfony/Component/Yaml/Tests/Fixtures/sfComments.yml +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/sfComments.yml @@ -49,3 +49,17 @@ yaml: | foo: bar # a comment php: | array('foo' => 'bar') +--- +test: Comment containing a colon on a hash line +brief: > + Comment containing a colon on a scalar line +yaml: 'foo # comment: this is also part of the comment' +php: | + 'foo' +--- +test: 'Hash key containing a #' +brief: > + 'Hash key containing a #' +yaml: 'foo#bar: baz' +php: | + array('foo#bar' => 'baz')