bug #10946 [2.5][PropertyAccess] Fixed getValue() when accessing non-existing indices of ArrayAccess implementations (webmozart)

This PR was merged into the 2.5-dev branch.

Discussion
----------

[2.5][PropertyAccess] Fixed getValue() when accessing non-existing indices of ArrayAccess implementations

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

Previously, when the following code was executed:

```php
$object = new ImplOfArrayAccess();

$propertyAccessor->setValue($object, '[index]', 'Value');
```

and that index did not exist, a fatal error would be generated because `array_keys()` was executed on `$object`. This error is fixed now.

Commits
-------

fef698e [PropertyAccess] Fixed getValue() when accessing non-existing indices of ArrayAccess implementations
This commit is contained in:
Fabien Potencier 2014-05-21 17:54:53 +02:00
commit 8954a1a88d
9 changed files with 212 additions and 83 deletions

View File

@ -231,7 +231,22 @@ class PropertyAccessor implements PropertyAccessorInterface
// Create missing nested arrays on demand
if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) {
if (!$ignoreInvalidIndices) {
throw new NoSuchIndexException(sprintf('Cannot read property "%s". Available properties are "%s"', $property, print_r(array_keys($objectOrArray), true)));
if (!is_array($objectOrArray)) {
if (!$objectOrArray instanceof \Traversable) {
throw new NoSuchIndexException(sprintf(
'Cannot read property "%s".',
$property
));
}
$objectOrArray = iterator_to_array($objectOrArray);
}
throw new NoSuchIndexException(sprintf(
'Cannot read property "%s". Available properties are "%s"',
$property,
print_r(array_keys($objectOrArray), true)
));
}
$objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null;

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
/**
* 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 NonTraversableArrayObject implements \ArrayAccess, \Countable, \Serializable
{
private $array;
public function __construct(array $array = null)
{
$this->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)
{
unset($this->array[$offset]);
}
public function count()
{
return count($this->array);
}
public function serialize()
{
return serialize($this->array);
}
public function unserialize($serialized)
{
$this->array = (array) unserialize((string) $serialized);
}
}

View File

@ -15,7 +15,7 @@ namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
* 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 CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable
class TraversableArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable
{
private $array;

View File

@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
abstract class PropertyAccessorArrayAccessTest extends \PHPUnit_Framework_TestCase
{
/**
* @var PropertyAccessor
*/
protected $propertyAccessor;
protected function setUp()
{
$this->propertyAccessor = new PropertyAccessor();
}
abstract protected function getContainer(array $array);
public function getValidPropertyPaths()
{
return array(
array($this->getContainer(array('firstName' => 'Bernhard')), '[firstName]', 'Bernhard'),
array($this->getContainer(array('person' => $this->getContainer(array('firstName' => 'Bernhard')))), '[person][firstName]', 'Bernhard'),
);
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testGetValue($collection, $path, $value)
{
$this->assertSame($value, $this->propertyAccessor->getValue($collection, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testGetValueFailsIfNoSuchIndex()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
->enableExceptionOnInvalidIndex()
->getPropertyAccessor();
$object = $this->getContainer(array('firstName' => 'Bernhard'));
$this->propertyAccessor->getValue($object, '[lastName]');
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testSetValue($collection, $path)
{
$this->propertyAccessor->setValue($collection, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($collection, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsReadable($collection, $path)
{
$this->assertTrue($this->propertyAccessor->isReadable($collection, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsWritable($collection, $path)
{
$this->assertTrue($this->propertyAccessor->isWritable($collection, $path, 'Updated'));
}
}

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\PropertyAccess\Tests;
class PropertyAccessorArrayObjectTest extends PropertyAccessorCollectionTest
{
protected function getCollection(array $array)
protected function getContainer(array $array)
{
return new \ArrayObject($array);
}

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\PropertyAccess\Tests;
class PropertyAccessorArrayTest extends PropertyAccessorCollectionTest
{
protected function getCollection(array $array)
protected function getContainer(array $array)
{
return $array;
}

View File

@ -11,8 +11,6 @@
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\PropertyAccessor;
class PropertyAccessorCollectionTest_Car
{
private $axes;
@ -80,55 +78,13 @@ class PropertyAccessorCollectionTest_CarStructure
public function getAxes() {}
}
abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCase
abstract class PropertyAccessorCollectionTest extends PropertyAccessorArrayAccessTest
{
/**
* @var PropertyAccessor
*/
private $propertyAccessor;
protected function setUp()
{
$this->propertyAccessor = new PropertyAccessor();
}
abstract protected function getCollection(array $array);
public function getValidPropertyPaths()
{
return array(
array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'),
array(array('person' => array('firstName' => 'Bernhard')), '[person][firstName]', 'Bernhard'),
);
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testGetValue(array $array, $path, $value)
{
$collection = $this->getCollection($array);
$this->assertSame($value, $this->propertyAccessor->getValue($collection, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testSetValue(array $array, $path)
{
$collection = $this->getCollection($array);
$this->propertyAccessor->setValue($collection, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($collection, $path));
}
public function testSetValueCallsAdderAndRemoverForCollections()
{
$axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth', 4 => 'fifth'));
$axesMerged = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
$axesAfter = $this->getCollection(array(1 => 'second', 5 => 'first', 6 => 'third'));
$axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth', 4 => 'fifth'));
$axesMerged = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third'));
$axesAfter = $this->getContainer(array(1 => 'second', 5 => 'first', 6 => 'third'));
$axesMergedCopy = is_object($axesMerged) ? clone $axesMerged : $axesMerged;
// Don't use a mock in order to test whether the collections are
@ -147,8 +103,8 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
{
$car = $this->getMock(__CLASS__.'_CompositeCar');
$structure = $this->getMock(__CLASS__.'_CarStructure');
$axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
$axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third'));
$car->expects($this->any())
->method('getStructure')
@ -177,35 +133,20 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
public function testSetValueFailsIfNoAdderNorRemoverFound()
{
$car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover');
$axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
$axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third'));
$this->propertyAccessor->setValue($car, 'axes', $axes);
}
$car->expects($this->any())
->method('getAxes')
->will($this->returnValue($axesBefore));
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsReadable(array $array, $path)
{
$collection = $this->getCollection($array);
$this->assertTrue($this->propertyAccessor->isReadable($collection, $path));
}
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsWritable(array $array, $path)
{
$collection = $this->getCollection($array);
$this->assertTrue($this->propertyAccessor->isWritable($collection, $path, 'Updated'));
$this->propertyAccessor->setValue($car, 'axes', $axesAfter);
}
public function testIsWritableReturnsTrueIfAdderAndRemoverExists()
{
$car = $this->getMock(__CLASS__.'_Car');
$axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
$axes = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third'));
$this->assertTrue($this->propertyAccessor->isWritable($car, 'axes', $axes));
}
@ -213,7 +154,7 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
public function testIsWritableReturnsFalseIfOnlyAdderExists()
{
$car = $this->getMock(__CLASS__.'_CarOnlyAdder');
$axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
$axes = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third'));
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes));
}
@ -221,7 +162,7 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
public function testIsWritableReturnsFalseIfOnlyRemoverExists()
{
$car = $this->getMock(__CLASS__.'_CarOnlyRemover');
$axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
$axes = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third'));
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes));
}
@ -229,7 +170,7 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists()
{
$car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover');
$axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
$axes = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third'));
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes));
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\Tests\Fixtures\NonTraversableArrayObject;
class PropertyAccessorNonTraversableArrayObjectTest extends PropertyAccessorArrayAccessTest
{
protected function getContainer(array $array)
{
return new NonTraversableArrayObject($array);
}
}

View File

@ -11,12 +11,12 @@
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\Tests\Fixtures\CustomArrayObject;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TraversableArrayObject;
class PropertyAccessorCustomArrayObjectTest extends PropertyAccessorCollectionTest
class PropertyAccessorTraversableArrayObjectTest extends PropertyAccessorCollectionTest
{
protected function getCollection(array $array)
protected function getContainer(array $array)
{
return new CustomArrayObject($array);
return new TraversableArrayObject($array);
}
}