From fd09484a61dee5a028f62274fdb7f3c4762c92fe Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 24 Aug 2013 17:43:39 +0200 Subject: [PATCH] [Form] Fixed Form::all() signature for PHP 5.3.3 --- .../ViolationMapper/ViolationMapper.php | 2 +- src/Symfony/Component/Form/Form.php | 38 +- .../Util/InheritDataAwareIteratorTest.php | 122 ----- .../Form/Tests/Util/OrderedHashMapTest.php | 487 ++++++++++++++++++ .../Component/Form/Util/OrderedHashMap.php | 190 +++++++ .../Form/Util/OrderedHashMapIterator.php | 163 ++++++ .../Form/Util/ReferencingArrayIterator.php | 78 --- .../Form/Util/VirtualFormAwareIterator.php | 7 +- 8 files changed, 861 insertions(+), 226 deletions(-) delete mode 100644 src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Util/OrderedHashMapTest.php create mode 100644 src/Symfony/Component/Form/Util/OrderedHashMap.php create mode 100644 src/Symfony/Component/Form/Util/OrderedHashMapIterator.php delete mode 100644 src/Symfony/Component/Form/Util/ReferencingArrayIterator.php diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php index 8a7636c7e8..3be55e64c1 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -167,7 +167,7 @@ class ViolationMapper implements ViolationMapperInterface // Skip forms inheriting their parent data when iterating the children $childIterator = new \RecursiveIteratorIterator( - new InheritDataAwareIterator($form->all()) + new InheritDataAwareIterator($form) ); // Make the path longer until we find a matching child diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 556c190d8e..f7fa834b35 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -19,6 +19,7 @@ use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Exception\OutOfBoundsException; use Symfony\Component\Form\Util\FormUtil; use Symfony\Component\Form\Util\InheritDataAwareIterator; +use Symfony\Component\Form\Util\OrderedHashMap; use Symfony\Component\PropertyAccess\PropertyPath; /** @@ -73,9 +74,9 @@ class Form implements \IteratorAggregate, FormInterface /** * The children of this form - * @var FormInterface[] An array of FormInterface instances + * @var FormInterface[] A map of FormInterface instances */ - private $children = array(); + private $children; /** * The errors of this form @@ -164,6 +165,7 @@ class Form implements \IteratorAggregate, FormInterface } $this->config = $config; + $this->children = new OrderedHashMap(); } public function __clone() @@ -370,9 +372,9 @@ class Form implements \IteratorAggregate, FormInterface // even if the form is compound. if (count($this->children) > 0) { // Update child forms from the data - $childrenIterator = new InheritDataAwareIterator($this->children); - $childrenIterator = new \RecursiveIteratorIterator($childrenIterator); - $this->config->getDataMapper()->mapDataToForms($viewData, $childrenIterator); + $iterator = new InheritDataAwareIterator($this->children); + $iterator = new \RecursiveIteratorIterator($iterator); + $this->config->getDataMapper()->mapDataToForms($viewData, $iterator); } if ($dispatcher->hasListeners(FormEvents::POST_SET_DATA)) { @@ -536,10 +538,7 @@ class Form implements \IteratorAggregate, FormInterface $submittedData = array(); } - for (reset($this->children); false !== current($this->children); next($this->children)) { - $child = current($this->children); - $name = key($this->children); - + foreach ($this->children as $name => $child) { if (array_key_exists($name, $submittedData) || $clearMissing) { $child->submit(isset($submittedData[$name]) ? $submittedData[$name] : null, $clearMissing); unset($submittedData[$name]); @@ -587,9 +586,9 @@ class Form implements \IteratorAggregate, FormInterface // descendants that inherit this form's data. // These descendants will not be submitted normally (see the check // for $this->config->getInheritData() above) - $childrenIterator = new InheritDataAwareIterator($this->children); - $childrenIterator = new \RecursiveIteratorIterator($childrenIterator); - $this->config->getDataMapper()->mapFormsToData($childrenIterator, $viewData); + $iterator = new InheritDataAwareIterator($this->children); + $iterator = new \RecursiveIteratorIterator($iterator); + $this->config->getDataMapper()->mapFormsToData($iterator, $viewData); } $modelData = null; @@ -765,9 +764,9 @@ class Form implements \IteratorAggregate, FormInterface /** * {@inheritdoc} */ - public function &all() + public function all() { - return $this->children; + return iterator_to_array($this->children); } /** @@ -836,10 +835,9 @@ class Form implements \IteratorAggregate, FormInterface $child->setParent($this); if (!$this->lockSetData && $this->defaultDataSet && !$this->config->getInheritData()) { - $children = array($child); - $childrenIterator = new InheritDataAwareIterator($children); - $childrenIterator = new \RecursiveIteratorIterator($childrenIterator); - $this->config->getDataMapper()->mapDataToForms($viewData, $childrenIterator); + $iterator = new InheritDataAwareIterator(new \ArrayIterator(array($child))); + $iterator = new \RecursiveIteratorIterator($iterator); + $this->config->getDataMapper()->mapDataToForms($viewData, $iterator); } return $this; @@ -940,11 +938,11 @@ class Form implements \IteratorAggregate, FormInterface /** * Returns the iterator for this group. * - * @return \ArrayIterator + * @return \Iterator */ public function getIterator() { - return new \ArrayIterator($this->children); + return $this->children; } /** diff --git a/src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php b/src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php deleted file mode 100644 index 19a0940bc0..0000000000 --- a/src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Tests\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/Tests/Util/OrderedHashMapTest.php b/src/Symfony/Component/Form/Tests/Util/OrderedHashMapTest.php new file mode 100644 index 0000000000..60703c5b3b --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Util/OrderedHashMapTest.php @@ -0,0 +1,487 @@ + + * + * 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\OrderedHashMap; + +/** + * @author Bernhard Schussek + */ +class OrderedHashMapTest extends \PHPUnit_Framework_TestCase +{ + public function testGet() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + + $this->assertSame(1, $map['first']); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testGetNonExistingFails() + { + $map = new OrderedHashMap(); + + $map['first']; + } + + public function testInsertStringKeys() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + $map['second'] = 2; + + $this->assertSame(array('first' => 1, 'second' => 2), iterator_to_array($map)); + } + + public function testInsertNullKeys() + { + $map = new OrderedHashMap(); + $map[] = 1; + $map['foo'] = 2; + $map[] = 3; + + $this->assertSame(array(0 => 1, 'foo' => 2, 1 => 3), iterator_to_array($map)); + } + + /** + * Updates should not change the position of an element, otherwise we could + * turn foreach loops into endless loops if they change the current + * element: + * + * foreach ($map as $index => $value) { + * $map[$index] = $value + 1; + * } + * + * And we don't want this, right? :) + */ + public function testUpdateDoesNotChangeElementPosition() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + $map['second'] = 2; + $map['first'] = 1; + + $this->assertSame(array('first' => 1, 'second' => 2), iterator_to_array($map)); + } + + public function testIsset() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + + $this->assertTrue(isset($map['first'])); + } + + public function testIssetReturnsFalseForNonExisting() + { + $map = new OrderedHashMap(); + + $this->assertFalse(isset($map['first'])); + } + + public function testIssetReturnsFalseForNull() + { + $map = new OrderedHashMap(); + $map['first'] = null; + + $this->assertFalse(isset($map['first'])); + } + + public function testUnset() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + $map['second'] = 2; + + unset($map['first']); + + $this->assertSame(array('second' => 2), iterator_to_array($map)); + } + + public function testUnsetNonExistingSucceeds() + { + $map = new OrderedHashMap(); + + unset($map['first']); + } + + public function testEmptyIteration() + { + $map = new OrderedHashMap(); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsInsertion() + { + $map = new OrderedHashMap(array('first' => 1)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('first', $it->key()); + $this->assertSame(1, $it->current()); + + // dynamic modification + $map['added'] = 2; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('first', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('added', $it->key()); + $this->assertSame(2, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionAndInsertion() + { + $map = new OrderedHashMap(array('first' => 1, 'removed' => 2)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('first', $it->key()); + $this->assertSame(1, $it->current()); + + // dynamic modification + unset($map['removed']); + $map['added'] = 3; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('first', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('added', $it->key()); + $this->assertSame(3, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionOfCurrentElement() + { + $map = new OrderedHashMap(array('removed' => 1, 'next' => 2)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + unset($map['removed']); + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationIgnoresReplacementOfCurrentElement() + { + $map = new OrderedHashMap(array('replaced' => 1, 'next' => 2)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + $map['replaced'] = 3; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionOfCurrentAndLastElement() + { + $map = new OrderedHashMap(array('removed' => 1)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + unset($map['removed']); + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationIgnoresReplacementOfCurrentAndLastElement() + { + $map = new OrderedHashMap(array('replaced' => 1)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + $map['replaced'] = 2; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionOfPreviousElement() + { + $map = new OrderedHashMap(array('removed' => 1, 'next' => 2, 'onemore' => 3)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + unset($map['removed']); + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('onemore', $it->key()); + $this->assertSame(3, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationIgnoresReplacementOfPreviousElement() + { + $map = new OrderedHashMap(array('replaced' => 1, 'next' => 2, 'onemore' => 3)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + $map['replaced'] = 4; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('onemore', $it->key()); + $this->assertSame(3, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionOfMultiplePreviousElements() + { + $map = new OrderedHashMap(array('removed' => 1, 'alsoremoved' => 2, 'next' => 3, 'onemore' => 4)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('alsoremoved', $it->key()); + $this->assertSame(2, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(3, $it->current()); + + unset($map['removed'], $map['alsoremoved']); + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(3, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('onemore', $it->key()); + $this->assertSame(4, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testParallelIteration() + { + $map = new OrderedHashMap(array('first' => 1, 'second' => 2)); + $it1 = $map->getIterator(); + $it2 = $map->getIterator(); + + $it1->rewind(); + $this->assertTrue($it1->valid()); + $this->assertSame('first', $it1->key()); + $this->assertSame(1, $it1->current()); + + $it2->rewind(); + $this->assertTrue($it2->valid()); + $this->assertSame('first', $it2->key()); + $this->assertSame(1, $it2->current()); + + // 1: continue iteration + $it1->next(); + $this->assertTrue($it1->valid()); + $this->assertSame('second', $it1->key()); + $this->assertSame(2, $it1->current()); + + // 2: remains unchanged + $this->assertTrue($it2->valid()); + $this->assertSame('first', $it2->key()); + $this->assertSame(1, $it2->current()); + + // 1: advance to end of map + $it1->next(); + $this->assertFalse($it1->valid()); + $this->assertNull($it1->key()); + $this->assertNull($it1->current()); + + // 2: remains unchanged + $this->assertTrue($it2->valid()); + $this->assertSame('first', $it2->key()); + $this->assertSame(1, $it2->current()); + + // 2: continue iteration + $it2->next(); + $this->assertTrue($it2->valid()); + $this->assertSame('second', $it2->key()); + $this->assertSame(2, $it2->current()); + + // 1: remains unchanged + $this->assertFalse($it1->valid()); + $this->assertNull($it1->key()); + $this->assertNull($it1->current()); + + // 2: advance to end of map + $it2->next(); + $this->assertFalse($it2->valid()); + $this->assertNull($it2->key()); + $this->assertNull($it2->current()); + + // 1: remains unchanged + $this->assertFalse($it1->valid()); + $this->assertNull($it1->key()); + $this->assertNull($it1->current()); + } + + public function testCount() + { + $map = new OrderedHashMap(); + $map[] = 1; + $map['foo'] = 2; + unset($map[0]); + $map[] = 3; + + $this->assertSame(2, count($map)); + } +} diff --git a/src/Symfony/Component/Form/Util/OrderedHashMap.php b/src/Symfony/Component/Form/Util/OrderedHashMap.php new file mode 100644 index 0000000000..bf5b08cda6 --- /dev/null +++ b/src/Symfony/Component/Form/Util/OrderedHashMap.php @@ -0,0 +1,190 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +/** + * A hash map which keeps track of deletions and additions. + * + * Like in associative arrays, elements can be mapped to integer or string keys. + * Unlike associative arrays, the map keeps track of the order in which keys + * were added and removed. This order is reflected during iteration. + * + * The map supports concurrent modification during iteration. That means that + * you can insert and remove elements from within a foreach loop and the + * iterator will reflect those changes accordingly. + * + * While elements that are added during the loop are recognized by the iterator, + * changed elements are not. Otherwise the loop could be infinite if each loop + * changes the current element: + * + * $map = new OrderedHashMap(); + * $map[1] = 1; + * $map[2] = 2; + * $map[3] = 3; + * + * foreach ($map as $index => $value) { + * echo "$index: $value\n" + * if (1 === $index) { + * $map[1] = 4; + * $map[] = 5; + * } + * } + * + * print_r(iterator_to_array($map)); + * + * // => 1: 1 + * // 2: 2 + * // 3: 3 + * // 4: 5 + * // Array + * // ( + * // [1] => 4 + * // [2] => 2 + * // [3] => 3 + * // [4] => 5 + * // ) + * + * The map also supports multiple parallel iterators. That means that you can + * nest foreach loops without affecting each other's iteration: + * + * foreach ($map as $index => $value) { + * foreach ($map as $index2 => $value2) { + * // ... + * } + * } + * + * @author Bernhard Schussek + * + * @since 2.2.6 + */ +class OrderedHashMap implements \ArrayAccess, \IteratorAggregate, \Countable +{ + /** + * The elements of the map, indexed by their keys. + * + * @var array + */ + private $elements = array(); + + /** + * The keys of the map in the order in which they were inserted or changed. + * + * @var array + */ + private $orderedKeys = array(); + + /** + * References to the cursors of all open iterators. + * + * @var array + */ + private $managedCursors = array(); + + /** + * Creates a new map. + * + * @param array $elements The elements to insert initially. + * + * @since 2.2.6 + */ + public function __construct(array $elements = array()) + { + $this->elements = $elements; + $this->orderedKeys = array_keys($elements); + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function offsetExists($key) + { + return isset($this->elements[$key]); + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function offsetGet($key) + { + if (!isset($this->elements[$key])) { + throw new \OutOfBoundsException('The offset "' . $key . '" does not exist.'); + } + + return $this->elements[$key]; + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function offsetSet($key, $value) + { + if (null === $key || !isset($this->elements[$key])) { + if (null === $key) { + $key = array() === $this->orderedKeys + // If the array is empty, use 0 as key + ? 0 + // Imitate PHP's behavior of generating a key that equals + // the highest existing integer key + 1 + : max($this->orderedKeys) + 1; + } + + $this->orderedKeys[] = $key; + } + + $this->elements[$key] = $value; + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function offsetUnset($key) + { + if (false !== ($position = array_search($key, $this->orderedKeys))) { + array_splice($this->orderedKeys, $position, 1); + unset($this->elements[$key]); + + foreach ($this->managedCursors as $i => $cursor) { + if ($cursor >= $position) { + --$this->managedCursors[$i]; + } + } + } + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function getIterator() + { + return new OrderedHashMapIterator($this->elements, $this->orderedKeys, $this->managedCursors); + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function count() + { + return count($this->elements); + } +} diff --git a/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php b/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php new file mode 100644 index 0000000000..87cf6a9d39 --- /dev/null +++ b/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php @@ -0,0 +1,163 @@ + + * + * 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 for {@link OrderedHashMap} objects. + * + * This class is internal and should not be used. + * + * @author Bernhard Schussek + * + * @since 2.2.6 + */ +class OrderedHashMapIterator implements \Iterator +{ + /** + * @var array + */ + private $elements; + + /** + * @var array + */ + private $orderedKeys; + + /** + * @var integer + */ + private $cursor; + + /** + * @var integer + */ + private $cursorId; + + /** + * @var array + */ + private $managedCursors; + + /** + * @var string|integer|null + */ + private $key; + + /** + * @var mixed + */ + private $current; + + /** + * Creates a new iterator. + * + * @param array $elements The elements of the map, indexed by their + * keys. + * @param array $orderedKeys The keys of the map in the order in which + * they should be iterated. + * @param array $managedCursors An array from which to reference the + * iterator's cursor as long as it is alive. + * This array is managed by the corresponding + * {@link OrderedHashMap} instance to support + * recognizing the deletion of elements. + * + * @since 2.2.6 + */ + public function __construct(array &$elements, array &$orderedKeys, array &$managedCursors) + { + $this->elements = &$elements; + $this->orderedKeys = &$orderedKeys; + $this->managedCursors = &$managedCursors; + $this->cursorId = count($managedCursors); + + $this->managedCursors[$this->cursorId] = &$this->cursor; + } + + /** + * Removes the iterator's cursors from the managed cursors of the + * corresponding {@link OrderedHashMap} instance. + * + * @since 2.2.6 + */ + public function __destruct() + { + // Use array_splice() instead of isset() to prevent holes in the + // array indices, which would break the initialization of $cursorId + array_splice($this->managedCursors, $this->cursorId, 1); + } + + /** + *{@inheritdoc} + * + * @since 2.2.6 + */ + public function current() + { + return $this->current; + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function next() + { + ++$this->cursor; + + if (isset($this->orderedKeys[$this->cursor])) { + $this->key = $this->orderedKeys[$this->cursor]; + $this->current = $this->elements[$this->key]; + } else { + $this->key = null; + $this->current = null; + } + } + + /** + *{@inheritdoc} + * + * @since 2.2.6 + */ + public function key() + { + return $this->key; + } + + /** + *{@inheritdoc} + * + * @since 2.2.6 + */ + public function valid() + { + return null !== $this->key; + } + + /** + *{@inheritdoc} + * + * @since 2.2.6 + */ + public function rewind() + { + $this->cursor = 0; + + if (isset($this->orderedKeys[0])) { + $this->key = $this->orderedKeys[0]; + $this->current = $this->elements[$this->key]; + } else { + $this->key = null; + $this->current = null; + } + } +} diff --git a/src/Symfony/Component/Form/Util/ReferencingArrayIterator.php b/src/Symfony/Component/Form/Util/ReferencingArrayIterator.php deleted file mode 100644 index 9bb64d79d2..0000000000 --- a/src/Symfony/Component/Form/Util/ReferencingArrayIterator.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\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 708726dedf..581e3540e4 100644 --- a/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php +++ b/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php @@ -14,9 +14,6 @@ namespace Symfony\Component\Form\Util; /** * Iterator that traverses an array of forms. * - * 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. @@ -26,14 +23,14 @@ namespace Symfony\Component\Form\Util; * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use * {@link InheritDataAwareIterator} instead. */ -class VirtualFormAwareIterator extends ReferencingArrayIterator implements \RecursiveIterator +class VirtualFormAwareIterator extends \IteratorIterator implements \RecursiveIterator { /** * {@inheritdoc} */ public function getChildren() { - return new static($this->current()->all()); + return new static($this->current()); } /**