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

This commit is contained in:
Bernhard Schussek 2014-05-19 17:05:32 +02:00
parent 91ee82107b
commit 5b5a6b6bcf
9 changed files with 198 additions and 66 deletions

View File

@ -33,7 +33,7 @@ class PropertyAccessor implements PropertyAccessorInterface
/**
* @var bool
*/
private $throwExceptionOnInvalidIndex;
private $ignoreInvalidIndices;
/**
* Should not be used by application code. Use
@ -42,7 +42,7 @@ class PropertyAccessor implements PropertyAccessorInterface
public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false)
{
$this->magicCall = $magicCall;
$this->throwExceptionOnInvalidIndex = $throwExceptionOnInvalidIndex;
$this->ignoreInvalidIndices = $throwExceptionOnInvalidIndex;
}
/**
@ -56,7 +56,7 @@ class PropertyAccessor implements PropertyAccessorInterface
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface');
}
$propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->throwExceptionOnInvalidIndex);
$propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return $propertyValues[count($propertyValues) - 1][self::VALUE];
}
@ -116,7 +116,7 @@ class PropertyAccessor implements PropertyAccessorInterface
*
* @throws UnexpectedTypeException If a value within the path is neither object nor array.
*/
private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $throwExceptionOnInvalidIndex = false)
private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = false)
{
$propertyValues = array();
@ -131,8 +131,23 @@ class PropertyAccessor implements PropertyAccessorInterface
// Create missing nested arrays on demand
if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) {
if ($throwExceptionOnInvalidIndex) {
throw new NoSuchIndexException(sprintf('Cannot read property "%s". Available properties are "%s"', $property, print_r(array_keys($objectOrArray), true)));
if ($ignoreInvalidIndices) {
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;
@ -429,6 +444,8 @@ class PropertyAccessor implements PropertyAccessorInterface
));
}
}
return 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,70 @@
<?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));
}
}

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

@ -82,55 +82,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
@ -149,8 +107,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')
@ -179,8 +137,8 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
public function testSetValueFailsIfOnlyAdderFound()
{
$car = $this->getMock(__CLASS__.'_CarOnlyAdder');
$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('getAxes')
@ -196,8 +154,8 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
public function testSetValueFailsIfOnlyRemoverFound()
{
$car = $this->getMock(__CLASS__.'_CarOnlyRemover');
$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('getAxes')
@ -213,7 +171,7 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
public function testSetValueFailsIfNoAdderAndNoRemoverFound()
{
$car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover');
$axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
$axes = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third'));
$this->propertyAccessor->setValue($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);
}
}