[Form] Made PropertyPath deterministic: "[prop]" always refers to indices (array or ArrayAccess), "prop" always refers to properties

This commit is contained in:
Bernhard Schussek 2012-05-17 10:00:32 +02:00
parent 29963400e8
commit c2a243f926
2 changed files with 210 additions and 136 deletions

View File

@ -21,25 +21,28 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
{ {
$array = array('firstName' => 'Bernhard'); $array = array('firstName' => 'Bernhard');
$path = new PropertyPath('firstName'); $path = new PropertyPath('[firstName]');
$this->assertEquals('Bernhard', $path->getValue($array)); $this->assertEquals('Bernhard', $path->getValue($array));
} }
public function testGetValueIgnoresSingular() /**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testGetValueThrowsExceptionIfIndexNotationExpected()
{ {
$array = array('children' => 'Many'); $array = array('firstName' => 'Bernhard');
$path = new PropertyPath('children|child'); $path = new PropertyPath('firstName');
$this->assertEquals('Many', $path->getValue($array)); $path->getValue($array);
} }
public function testGetValueReadsZeroIndex() public function testGetValueReadsZeroIndex()
{ {
$array = array('Bernhard'); $array = array('Bernhard');
$path = new PropertyPath('0'); $path = new PropertyPath('[0]');
$this->assertEquals('Bernhard', $path->getValue($array)); $this->assertEquals('Bernhard', $path->getValue($array));
} }
@ -53,20 +56,11 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Bernhard', $path->getValue($array)); $this->assertEquals('Bernhard', $path->getValue($array));
} }
public function testGetValueReadsElementWithSpecialCharsExceptDot()
{
$array = array('%!@$§' => 'Bernhard');
$path = new PropertyPath('%!@$§');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsNestedIndexWithSpecialChars() public function testGetValueReadsNestedIndexWithSpecialChars()
{ {
$array = array('root' => array('%!@$§.' => 'Bernhard')); $array = array('root' => array('%!@$§.' => 'Bernhard'));
$path = new PropertyPath('root[%!@$§.]'); $path = new PropertyPath('[root][%!@$§.]');
$this->assertEquals('Bernhard', $path->getValue($array)); $this->assertEquals('Bernhard', $path->getValue($array));
} }
@ -75,7 +69,7 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
{ {
$array = array('child' => array('index' => array('firstName' => 'Bernhard'))); $array = array('child' => array('index' => array('firstName' => 'Bernhard')));
$path = new PropertyPath('child[index].firstName'); $path = new PropertyPath('[child][index][firstName]');
$this->assertEquals('Bernhard', $path->getValue($array)); $this->assertEquals('Bernhard', $path->getValue($array));
} }
@ -84,7 +78,7 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
{ {
$array = array('child' => array('index' => array())); $array = array('child' => array('index' => array()));
$path = new PropertyPath('child[index].firstName'); $path = new PropertyPath('[child][index][firstName]');
$this->assertNull($path->getValue($array)); $this->assertNull($path->getValue($array));
} }
@ -99,6 +93,24 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Bernhard', $path->getValue($object)); $this->assertEquals('Bernhard', $path->getValue($object));
} }
public function testGetValueIgnoresSingular()
{
$object = (object) array('children' => 'Many');
$path = new PropertyPath('children|child');
$this->assertEquals('Many', $path->getValue($object));
}
public function testGetValueReadsPropertyWithSpecialCharsExceptDot()
{
$array = (object) array('%!@$§' => 'Bernhard');
$path = new PropertyPath('%!@$§');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsPropertyWithCustomPropertyPath() public function testGetValueReadsPropertyWithCustomPropertyPath()
{ {
$object = new Author(); $object = new Author();
@ -121,21 +133,23 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Bernhard', $path->getValue($object)); $this->assertEquals('Bernhard', $path->getValue($object));
} }
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testGetValueThrowsExceptionIfArrayAccessExpected() public function testGetValueThrowsExceptionIfArrayAccessExpected()
{ {
$path = new PropertyPath('[firstName]'); $path = new PropertyPath('[firstName]');
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException');
$path->getValue(new Author()); $path->getValue(new Author());
} }
/**
* @expectedException Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfPropertyIsNotPublic() public function testGetValueThrowsExceptionIfPropertyIsNotPublic()
{ {
$path = new PropertyPath('privateProperty'); $path = new PropertyPath('privateProperty');
$this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException');
$path->getValue(new Author()); $path->getValue(new Author());
} }
@ -159,12 +173,13 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Schussek', $path->getValue($object)); $this->assertEquals('Schussek', $path->getValue($object));
} }
/**
* @expectedException Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfGetterIsNotPublic() public function testGetValueThrowsExceptionIfGetterIsNotPublic()
{ {
$path = new PropertyPath('privateGetter'); $path = new PropertyPath('privateGetter');
$this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException');
$path->getValue(new Author()); $path->getValue(new Author());
} }
@ -198,48 +213,53 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertSame('foobar', $path->getValue($object)); $this->assertSame('foobar', $path->getValue($object));
} }
/**
* @expectedException Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfIsserIsNotPublic() public function testGetValueThrowsExceptionIfIsserIsNotPublic()
{ {
$path = new PropertyPath('privateIsser'); $path = new PropertyPath('privateIsser');
$this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException');
$path->getValue(new Author()); $path->getValue(new Author());
} }
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testGetValueThrowsExceptionIfPropertyDoesNotExist() public function testGetValueThrowsExceptionIfPropertyDoesNotExist()
{ {
$path = new PropertyPath('foobar'); $path = new PropertyPath('foobar');
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException');
$path->getValue(new Author()); $path->getValue(new Author());
} }
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfNotObjectOrArray() public function testGetValueThrowsExceptionIfNotObjectOrArray()
{ {
$path = new PropertyPath('foobar'); $path = new PropertyPath('foobar');
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->getValue('baz'); $path->getValue('baz');
} }
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfNull() public function testGetValueThrowsExceptionIfNull()
{ {
$path = new PropertyPath('foobar'); $path = new PropertyPath('foobar');
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->getValue(null); $path->getValue(null);
} }
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfEmpty() public function testGetValueThrowsExceptionIfEmpty()
{ {
$path = new PropertyPath('foobar'); $path = new PropertyPath('foobar');
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->getValue(''); $path->getValue('');
} }
@ -247,17 +267,28 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
{ {
$array = array(); $array = array();
$path = new PropertyPath('firstName'); $path = new PropertyPath('[firstName]');
$path->setValue($array, 'Bernhard'); $path->setValue($array, 'Bernhard');
$this->assertEquals(array('firstName' => 'Bernhard'), $array); $this->assertEquals(array('firstName' => 'Bernhard'), $array);
} }
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testSetValueThrowsExceptionIfIndexNotationExpected()
{
$array = array();
$path = new PropertyPath('firstName');
$path->setValue($array, 'Bernhard');
}
public function testSetValueUpdatesArraysWithCustomPropertyPath() public function testSetValueUpdatesArraysWithCustomPropertyPath()
{ {
$array = array(); $array = array();
$path = new PropertyPath('child[index].firstName'); $path = new PropertyPath('[child][index][firstName]');
$path->setValue($array, 'Bernhard'); $path->setValue($array, 'Bernhard');
$this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array); $this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array);
@ -305,12 +336,13 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foobar', $object->__get('magicProperty')); $this->assertEquals('foobar', $object->__get('magicProperty'));
} }
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testSetValueThrowsExceptionIfArrayAccessExpected() public function testSetValueThrowsExceptionIfArrayAccessExpected()
{ {
$path = new PropertyPath('[firstName]'); $path = new PropertyPath('[firstName]');
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException');
$path->setValue(new Author(), 'Bernhard'); $path->setValue(new Author(), 'Bernhard');
} }
@ -334,42 +366,46 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Schussek', $object->getLastName()); $this->assertEquals('Schussek', $object->getLastName());
} }
/**
* @expectedException Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testSetValueThrowsExceptionIfGetterIsNotPublic() public function testSetValueThrowsExceptionIfGetterIsNotPublic()
{ {
$path = new PropertyPath('privateSetter'); $path = new PropertyPath('privateSetter');
$this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException');
$path->setValue(new Author(), 'foobar'); $path->setValue(new Author(), 'foobar');
} }
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfNotObjectOrArray() public function testSetValueThrowsExceptionIfNotObjectOrArray()
{ {
$path = new PropertyPath('foobar'); $path = new PropertyPath('foobar');
$value = 'baz'; $value = 'baz';
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->setValue($value, 'bam'); $path->setValue($value, 'bam');
} }
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfNull() public function testSetValueThrowsExceptionIfNull()
{ {
$path = new PropertyPath('foobar'); $path = new PropertyPath('foobar');
$value = null; $value = null;
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->setValue($value, 'bam'); $path->setValue($value, 'bam');
} }
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfEmpty() public function testSetValueThrowsExceptionIfEmpty()
{ {
$path = new PropertyPath('foobar'); $path = new PropertyPath('foobar');
$value = ''; $value = '';
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->setValue($value, 'bam'); $path->setValue($value, 'bam');
} }
@ -380,31 +416,35 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('reference.traversable[index].property', $path->__toString()); $this->assertEquals('reference.traversable[index].property', $path->__toString());
} }
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_noDotBeforeProperty() public function testInvalidPropertyPath_noDotBeforeProperty()
{ {
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyPathException');
new PropertyPath('[index]property'); new PropertyPath('[index]property');
} }
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_dotAtTheBeginning() public function testInvalidPropertyPath_dotAtTheBeginning()
{ {
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyPathException');
new PropertyPath('.property'); new PropertyPath('.property');
} }
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_unexpectedCharacters() public function testInvalidPropertyPath_unexpectedCharacters()
{ {
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyPathException');
new PropertyPath('property.$form'); new PropertyPath('property.$form');
} }
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_null() public function testInvalidPropertyPath_null()
{ {
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyPathException');
new PropertyPath(null); new PropertyPath(null);
} }

View File

@ -69,9 +69,11 @@ class PropertyPath implements \IteratorAggregate
private $positions; private $positions;
/** /**
* Parses the given property path * Constructs a property path from a string.
* *
* @param string $propertyPath * @param string $propertyPath The property path as string.
*
* @throws InvalidPropertyPathException If the syntax of the property path is not valid.
*/ */
public function __construct($propertyPath) public function __construct($propertyPath)
{ {
@ -258,25 +260,7 @@ class PropertyPath implements \IteratorAggregate
*/ */
public function getValue($objectOrArray) public function getValue($objectOrArray)
{ {
for ($i = 0; $i < $this->length; ++$i) { return $this->readPropertyAt($objectOrArray, $this->length - 1);
if (is_object($objectOrArray)) {
$value = $this->readProperty($objectOrArray, $i);
// arrays need to be treated separately (due to PHP bug?)
// http://bugs.php.net/bug.php?id=52133
} elseif (is_array($objectOrArray)) {
$property = $this->elements[$i];
if (!array_key_exists($property, $objectOrArray)) {
$objectOrArray[$property] = $i + 1 < $this->length ? array() : null;
}
$value =& $objectOrArray[$property];
} else {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
$objectOrArray =& $value;
}
return $value;
} }
/** /**
@ -299,63 +283,89 @@ class PropertyPath implements \IteratorAggregate
* *
* If neither is found, an exception is thrown. * If neither is found, an exception is thrown.
* *
* @param object|array $objectOrArray The object or array to traverse * @param object|array $objectOrArray The object or array to modify.
* @param mixed $value The value at the end of the property path * @param mixed $value The value to set at the end of the property path.
* *
* @throws InvalidPropertyException If the property/setter does not exist * @throws InvalidPropertyException If a property does not exist.
* @throws PropertyAccessDeniedException If the property/setter exists but is not public * @throws PropertyAccessDeniedException If a property cannot be accessed due to
* access restrictions (private or protected).
* @throws UnexpectedTypeException If a value within the path is neither object
* nor array.
*/ */
public function setValue(&$objectOrArray, $value) public function setValue(&$objectOrArray, $value)
{ {
for ($i = 0, $l = $this->length - 1; $i < $l; ++$i) { $objectOrArray =& $this->readPropertyAt($objectOrArray, $this->length - 2);
if (is_object($objectOrArray)) {
$nestedObject = $this->readProperty($objectOrArray, $i);
// arrays need to be treated separately (due to PHP bug?)
// http://bugs.php.net/bug.php?id=52133
} elseif (is_array($objectOrArray)) {
$property = $this->elements[$i];
if (!array_key_exists($property, $objectOrArray)) {
$objectOrArray[$property] = array();
}
$nestedObject =& $objectOrArray[$property];
} else {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
$objectOrArray =& $nestedObject;
}
if (!is_object($objectOrArray) && !is_array($objectOrArray)) { if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, 'object or array'); throw new UnexpectedTypeException($objectOrArray, 'object or array');
} }
$this->writeProperty($objectOrArray, $i, $value); $property = $this->elements[$this->length - 1];
$singular = $this->singulars[$this->length - 1];
$isIndex = $this->isIndex[$this->length - 1];
$this->writeProperty($objectOrArray, $property, $singular, $isIndex, $value);
} }
/** /**
* Reads the value of the property at the given index in the path * Reads the path from an object up to a given path index.
* *
* @param object $object The object to read from * @param object|array $objectOrArray The object or array to read from.
* @param integer $currentIndex The index of the read property in the path * @param integer $index The integer up to which should be read.
* *
* @return mixed The value of the property * @return mixed The value read at the end of the path.
*
* @throws UnexpectedTypeException If a value within the path is neither object nor array.
*/ */
protected function readProperty($object, $currentIndex) protected function &readPropertyAt(&$objectOrArray, $index)
{ {
$property = $this->elements[$currentIndex]; for ($i = 0; $i <= $index; ++$i) {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
if ($this->isIndex[$currentIndex]) { throw new UnexpectedTypeException($objectOrArray, 'object or array');
if (!$object instanceof \ArrayAccess) {
throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($object)));
} }
if (isset($object[$property])) { // Create missing nested arrays on demand
return $object[$property]; if (is_array($objectOrArray) && !array_key_exists($this->elements[$i], $objectOrArray)) {
$objectOrArray[$this->elements[$i]] = $i + 1 < $this->length ? array() : null;
} }
} else {
$property = $this->elements[$i];
$isIndex = $this->isIndex[$i];
$objectOrArray =& $this->readProperty($objectOrArray, $property, $isIndex);
}
return $objectOrArray;
}
/**
* Reads the a property from an object or array.
*
* @param object|array $objectOrArray The object or array to read from.
* @param string $property The property to read.
* @param integer $isIndex Whether to interpret the property as index.
*
* @return mixed The value of the read property
*
* @throws InvalidPropertyException If the property does not exist.
* @throws PropertyAccessDeniedException If the property cannot be accessed due to
* access restrictions (private or protected).
*/
protected function &readProperty(&$objectOrArray, $property, $isIndex)
{
$result = null;
if ($isIndex) {
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) {
throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
}
if (isset($objectOrArray[$property])) {
$result =& $objectOrArray[$property];
}
} elseif (is_object($objectOrArray)) {
$camelProp = $this->camelize($property); $camelProp = $this->camelize($property);
$reflClass = new ReflectionClass($object); $reflClass = new ReflectionClass($objectOrArray);
$getter = 'get'.$camelProp; $getter = 'get'.$camelProp;
$isser = 'is'.$camelProp; $isser = 'is'.$camelProp;
$hasser = 'has'.$camelProp; $hasser = 'has'.$camelProp;
@ -365,50 +375,58 @@ class PropertyPath implements \IteratorAggregate
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->getName())); throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->getName()));
} }
return $object->$getter(); $result = $objectOrArray->$getter();
} elseif ($reflClass->hasMethod($isser)) { } elseif ($reflClass->hasMethod($isser)) {
if (!$reflClass->getMethod($isser)->isPublic()) { if (!$reflClass->getMethod($isser)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->getName())); throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->getName()));
} }
return $object->$isser(); $result = $objectOrArray->$isser();
} elseif ($reflClass->hasMethod($hasser)) { } elseif ($reflClass->hasMethod($hasser)) {
if (!$reflClass->getMethod($hasser)->isPublic()) { if (!$reflClass->getMethod($hasser)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $hasser, $reflClass->getName())); throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $hasser, $reflClass->getName()));
} }
return $object->$hasser(); $result = $objectOrArray->$hasser();
} elseif ($reflClass->hasMethod('__get')) { } elseif ($reflClass->hasMethod('__get')) {
// needed to support magic method __get // needed to support magic method __get
return $object->$property; $result =& $objectOrArray->$property;
} elseif ($reflClass->hasProperty($property)) { } elseif ($reflClass->hasProperty($property)) {
if (!$reflClass->getProperty($property)->isPublic()) { if (!$reflClass->getProperty($property)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()"?', $property, $reflClass->getName(), $getter, $isser)); throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()"?', $property, $reflClass->getName(), $getter, $isser));
} }
return $object->$property; $result =& $objectOrArray->$property;
} elseif (property_exists($object, $property)) { } elseif (property_exists($objectOrArray, $property)) {
// needed to support \stdClass instances // needed to support \stdClass instances
return $object->$property; $result =& $objectOrArray->$property;
} else { } else {
throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->getName())); throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->getName()));
} }
} else {
throw new InvalidPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
} }
return $result;
} }
/** /**
* Sets the value of the property at the given index in the path * Sets the value of the property at the given index in the path
* *
* @param object $objectOrArray The object or array to traverse * @param object|array $objectOrArray The object or array to write to.
* @param integer $currentIndex The index of the modified property in the path * @param string $property The property to write.
* @param mixed $value The value to set * @param string $singular The singular form of the property name or null.
* @param integer $isIndex Whether to interpret the property as index.
* @param mixed $value The value to write.
*
* @throws InvalidPropertyException If the property does not exist.
* @throws PropertyAccessDeniedException If the property cannot be accessed due to
* access restrictions (private or protected).
*/ */
protected function writeProperty(&$objectOrArray, $currentIndex, $value) protected function writeProperty(&$objectOrArray, $property, $singular, $isIndex, $value)
{ {
$property = $this->elements[$currentIndex]; if ($isIndex) {
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) {
if (is_object($objectOrArray) && $this->isIndex[$currentIndex]) {
if (!$objectOrArray instanceof \ArrayAccess) {
throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray))); throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
} }
@ -422,7 +440,6 @@ class PropertyPath implements \IteratorAggregate
// Check if the parent has matching methods to add/remove items // Check if the parent has matching methods to add/remove items
if (is_array($value) || $value instanceof Traversable) { if (is_array($value) || $value instanceof Traversable) {
$singular = $this->singulars[$currentIndex];
if (null !== $singular) { if (null !== $singular) {
$addMethod = 'add' . ucfirst($singular); $addMethod = 'add' . ucfirst($singular);
$removeMethod = 'remove' . ucfirst($singular); $removeMethod = 'remove' . ucfirst($singular);
@ -490,7 +507,7 @@ class PropertyPath implements \IteratorAggregate
if ($addMethod && $removeMethod) { if ($addMethod && $removeMethod) {
$itemsToAdd = is_object($value) ? clone $value : $value; $itemsToAdd = is_object($value) ? clone $value : $value;
$itemToRemove = array(); $itemToRemove = array();
$previousValue = $this->readProperty($objectOrArray, $currentIndex); $previousValue = $this->readProperty($objectOrArray, $property, $isIndex);
if (is_array($previousValue) || $previousValue instanceof Traversable) { if (is_array($previousValue) || $previousValue instanceof Traversable) {
foreach ($previousValue as $previousItem) { foreach ($previousValue as $previousItem) {
@ -538,21 +555,38 @@ class PropertyPath implements \IteratorAggregate
throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"', $property, $setter, $reflClass->getName())); throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"', $property, $setter, $reflClass->getName()));
} }
} else { } else {
$objectOrArray[$property] = $value; throw new InvalidPropertyException(sprintf('Cannot write property "%s" in an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
} }
} }
protected function camelize($property) /**
* Camelizes a given string.
*
* @param string $string Some string.
*
* @return string The camelized version of the string.
*/
protected function camelize($string)
{ {
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $property); return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $string);
} }
private function isAccessible(ReflectionClass $reflClass, $methodName, $numberOfRequiredParameters) /**
* Returns whether a method is public and has a specific number of required parameters.
*
* @param \ReflectionClass $class The class of the method.
* @param string $methodName The method name.
* @param integer $parameters The number of parameters.
*
* @return Boolean Whether the method is public and has $parameters
* required parameters.
*/
private function isAccessible(ReflectionClass $class, $methodName, $parameters)
{ {
if ($reflClass->hasMethod($methodName)) { if ($class->hasMethod($methodName)) {
$method = $reflClass->getMethod($methodName); $method = $class->getMethod($methodName);
if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $numberOfRequiredParameters) { if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) {
return true; return true;
} }
} }