[PropertyAccess] Added isReadable() and isWritable()
This commit is contained in:
parent
20e6bf8f49
commit
6d2af217aa
@ -45,6 +45,47 @@ Form
|
||||
{
|
||||
```
|
||||
|
||||
PropertyAccess
|
||||
--------------
|
||||
|
||||
* The methods `isReadable()` and `isWritable()` were added to
|
||||
`PropertyAccessorInterface`. If you implemented this interface in your own
|
||||
code, you should add these two methods.
|
||||
|
||||
* The methods `getValue()` and `setValue()` now throw an
|
||||
`NoSuchIndexException` instead of a `NoSuchPropertyException` when an index
|
||||
is accessed on an object that does not implement `ArrayAccess`. If you catch
|
||||
this exception in your code, you should adapt the catch statement:
|
||||
|
||||
Before:
|
||||
|
||||
```php
|
||||
$object = new \stdClass();
|
||||
|
||||
try {
|
||||
$propertyAccessor->getValue($object, '[index]');
|
||||
$propertyAccessor->setValue($object, '[index]', 'New value');
|
||||
} catch (NoSuchPropertyException $e) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```php
|
||||
$object = new \stdClass();
|
||||
|
||||
try {
|
||||
$propertyAccessor->getValue($object, '[index]');
|
||||
$propertyAccessor->setValue($object, '[index]', 'New value');
|
||||
} catch (NoSuchIndexException $e) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
A `NoSuchPropertyException` is still thrown when a non-existing property is
|
||||
accessed on an object or an array.
|
||||
|
||||
Validator
|
||||
---------
|
||||
|
||||
|
@ -8,6 +8,7 @@ CHANGELOG
|
||||
* [BC BREAK] when accessing an index on an object that does not implement
|
||||
ArrayAccess, a NoSuchIndexException is now thrown instead of the
|
||||
semantically wrong NoSuchPropertyException
|
||||
* [BC BREAK] added isReadable() and isWritable() to PropertyAccessorInterface
|
||||
|
||||
2.3.0
|
||||
------
|
||||
|
@ -105,6 +105,84 @@ class PropertyAccessor implements PropertyAccessorInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isReadable($objectOrArray, $propertyPath)
|
||||
{
|
||||
if (is_string($propertyPath)) {
|
||||
$propertyPath = new PropertyPath($propertyPath);
|
||||
} elseif (!$propertyPath instanceof PropertyPathInterface) {
|
||||
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->throwExceptionOnInvalidIndex);
|
||||
|
||||
return true;
|
||||
} catch (NoSuchIndexException $e) {
|
||||
return false;
|
||||
} catch (NoSuchPropertyException $e) {
|
||||
return false;
|
||||
} catch (UnexpectedTypeException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isWritable($objectOrArray, $propertyPath, $value)
|
||||
{
|
||||
if (is_string($propertyPath)) {
|
||||
$propertyPath = new PropertyPath($propertyPath);
|
||||
} elseif (!$propertyPath instanceof PropertyPathInterface) {
|
||||
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface');
|
||||
}
|
||||
|
||||
try {
|
||||
$propertyValues = $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1);
|
||||
$overwrite = true;
|
||||
|
||||
// Add the root object to the list
|
||||
array_unshift($propertyValues, array(
|
||||
self::VALUE => $objectOrArray,
|
||||
self::IS_REF => true,
|
||||
));
|
||||
|
||||
for ($i = count($propertyValues) - 1; $i >= 0; --$i) {
|
||||
$objectOrArray = $propertyValues[$i][self::VALUE];
|
||||
|
||||
if ($overwrite) {
|
||||
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$property = $propertyPath->getElement($i);
|
||||
|
||||
if ($propertyPath->isIndex($i)) {
|
||||
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!$this->isPropertyWritable($objectOrArray, $property, $value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$value = $objectOrArray;
|
||||
$overwrite = !$propertyValues[$i][self::IS_REF];
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (NoSuchIndexException $e) {
|
||||
return false;
|
||||
} catch (NoSuchPropertyException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the path from an object up to a given path index.
|
||||
*
|
||||
@ -357,9 +435,9 @@ class PropertyAccessor implements PropertyAccessorInterface
|
||||
$setter = 'set'.$this->camelize($property);
|
||||
$classHasProperty = $reflClass->hasProperty($property);
|
||||
|
||||
if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) {
|
||||
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
|
||||
$object->$setter($value);
|
||||
} elseif ($reflClass->hasMethod('__set') && $reflClass->getMethod('__set')->isPublic()) {
|
||||
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
|
||||
$object->$property = $value;
|
||||
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
|
||||
$object->$property = $value;
|
||||
@ -370,7 +448,7 @@ class PropertyAccessor implements PropertyAccessorInterface
|
||||
// returns true, consequently the following line will result in a
|
||||
// fatal error.
|
||||
$object->$property = $value;
|
||||
} elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
|
||||
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
|
||||
// we call the getter and hope the __call do the job
|
||||
$object->$setter($value);
|
||||
} else {
|
||||
@ -385,6 +463,38 @@ class PropertyAccessor implements PropertyAccessorInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function isPropertyWritable($object, $property, $value)
|
||||
{
|
||||
if (!is_object($object)) {
|
||||
throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
|
||||
}
|
||||
|
||||
$reflClass = new \ReflectionClass($object);
|
||||
$plural = $this->camelize($property);
|
||||
|
||||
// Any of the two methods is required, but not yet known
|
||||
$singulars = (array) StringUtil::singularify($plural);
|
||||
|
||||
if (is_array($value) || $value instanceof \Traversable) {
|
||||
try {
|
||||
if (null !== $this->findAdderAndRemover($reflClass, $singulars)) {
|
||||
return true;
|
||||
}
|
||||
} catch (NoSuchPropertyException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$setter = 'set'.$this->camelize($property);
|
||||
$classHasProperty = $reflClass->hasProperty($property);
|
||||
|
||||
return $this->isMethodAccessible($reflClass, $setter, 1)
|
||||
|| $this->isMethodAccessible($reflClass, '__set', 2)
|
||||
|| ($classHasProperty && $reflClass->getProperty($property)->isPublic())
|
||||
|| (!$classHasProperty && property_exists($object, $property))
|
||||
|| ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Camelizes a given string.
|
||||
*
|
||||
@ -409,6 +519,8 @@ class PropertyAccessor implements PropertyAccessorInterface
|
||||
*/
|
||||
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
|
||||
{
|
||||
$exception = null;
|
||||
|
||||
foreach ($singulars as $singular) {
|
||||
$addMethod = 'add'.$singular;
|
||||
$removeMethod = 'remove'.$singular;
|
||||
@ -420,8 +532,8 @@ class PropertyAccessor implements PropertyAccessorInterface
|
||||
return array($addMethod, $removeMethod);
|
||||
}
|
||||
|
||||
if ($addMethodFound xor $removeMethodFound) {
|
||||
throw new NoSuchPropertyException(sprintf(
|
||||
if ($addMethodFound xor $removeMethodFound && null === $exception) {
|
||||
$exception = new NoSuchPropertyException(sprintf(
|
||||
'Found the public method "%s()", but did not find a public "%s()" on class %s',
|
||||
$addMethodFound ? $addMethod : $removeMethod,
|
||||
$addMethodFound ? $removeMethod : $addMethod,
|
||||
@ -430,6 +542,10 @@ class PropertyAccessor implements PropertyAccessorInterface
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $exception) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ namespace Symfony\Component\PropertyAccess;
|
||||
interface PropertyAccessorInterface
|
||||
{
|
||||
/**
|
||||
* Sets the value at the end of the property path of the object
|
||||
* Sets the value at the end of the property path of the object graph.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
@ -50,7 +50,7 @@ interface PropertyAccessorInterface
|
||||
public function setValue(&$objectOrArray, $propertyPath, $value);
|
||||
|
||||
/**
|
||||
* Returns the value at the end of the property path of the object
|
||||
* Returns the value at the end of the property path of the object graph.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
@ -78,4 +78,31 @@ interface PropertyAccessorInterface
|
||||
* @throws Exception\NoSuchPropertyException If a property does not exist or is not public.
|
||||
*/
|
||||
public function getValue($objectOrArray, $propertyPath);
|
||||
|
||||
/**
|
||||
* Returns whether a value can be written at a given property path.
|
||||
*
|
||||
* Whenever this method returns true, {@link setValue()} is guaranteed not
|
||||
* to throw an exception when called with the same arguments.
|
||||
*
|
||||
* @param object|array $objectOrArray The object or array to check
|
||||
* @param string|PropertyPathInterface $propertyPath The property path to check
|
||||
* @param mixed $value The value to set at the end of the property path
|
||||
*
|
||||
* @return Boolean Whether the value can be set
|
||||
*/
|
||||
public function isWritable($objectOrArray, $propertyPath, $value);
|
||||
|
||||
/**
|
||||
* Returns whether a property path can be read from an object graph.
|
||||
*
|
||||
* Whenever this method returns true, {@link getValue()} is guaranteed not
|
||||
* to throw an exception when called with the same arguments.
|
||||
*
|
||||
* @param object|array $objectOrArray The object or array to check
|
||||
* @param string|PropertyPathInterface $propertyPath The property path to check
|
||||
*
|
||||
* @return Boolean Whether the property path can be read
|
||||
*/
|
||||
public function isReadable($objectOrArray, $propertyPath);
|
||||
}
|
||||
|
@ -210,11 +210,63 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
|
||||
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
|
||||
* @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()", "addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover
|
||||
*/
|
||||
public function testSetValueFailsIfNoAdderAndNoRemoverFound()
|
||||
public function testSetValueFailsIfNoAdderNorRemoverFound()
|
||||
{
|
||||
$car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover');
|
||||
$axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
|
||||
|
||||
$this->propertyAccessor->setValue($car, 'axes', $axes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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'));
|
||||
}
|
||||
|
||||
public function testIsWritableReturnsTrueIfAdderAndRemoverExists()
|
||||
{
|
||||
$car = $this->getMock(__CLASS__.'_Car');
|
||||
$axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
|
||||
|
||||
$this->assertTrue($this->propertyAccessor->isWritable($car, 'axes', $axes));
|
||||
}
|
||||
|
||||
public function testIsWritableReturnsFalseIfOnlyAdderExists()
|
||||
{
|
||||
$car = $this->getMock(__CLASS__.'_CarOnlyAdder');
|
||||
$axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
|
||||
|
||||
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes));
|
||||
}
|
||||
|
||||
public function testIsWritableReturnsFalseIfOnlyRemoverExists()
|
||||
{
|
||||
$car = $this->getMock(__CLASS__.'_CarOnlyRemover');
|
||||
$axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
|
||||
|
||||
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes));
|
||||
}
|
||||
|
||||
public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists()
|
||||
{
|
||||
$car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover');
|
||||
$axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
|
||||
|
||||
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes));
|
||||
}
|
||||
}
|
||||
|
@ -301,4 +301,140 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidPropertyPaths
|
||||
*/
|
||||
public function testIsReadable($objectOrArray, $path)
|
||||
{
|
||||
$this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getPathsWithMissingProperty
|
||||
*/
|
||||
public function testIsReadableReturnsFalseIfPropertyNotFound($objectOrArray, $path)
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getPathsWithMissingIndex
|
||||
*/
|
||||
public function testIsReadableReturnsTrueIfIndexNotFound($objectOrArray, $path)
|
||||
{
|
||||
// Non-existing indices can be read. In this case, null is returned
|
||||
$this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getPathsWithMissingIndex
|
||||
*/
|
||||
public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
|
||||
{
|
||||
$this->propertyAccessor = new PropertyAccessor(false, true);
|
||||
|
||||
// When exceptions are enabled, non-existing indices cannot be read
|
||||
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
|
||||
}
|
||||
|
||||
public function testIsReadableRecognizesMagicGet()
|
||||
{
|
||||
$this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicGet('Bernhard'), 'magicProperty'));
|
||||
}
|
||||
|
||||
public function testIsReadableDoesNotRecognizeMagicCallByDefault()
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
|
||||
}
|
||||
|
||||
public function testIsReadableRecognizesMagicCallIfEnabled()
|
||||
{
|
||||
$this->propertyAccessor = new PropertyAccessor(true);
|
||||
|
||||
$this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
|
||||
}
|
||||
|
||||
public function testIsReadableThrowsExceptionIfNotObjectOrArray()
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isReadable('baz', 'foobar'));
|
||||
}
|
||||
|
||||
public function testIsReadableThrowsExceptionIfNull()
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isReadable(null, 'foobar'));
|
||||
}
|
||||
|
||||
public function testIsReadableThrowsExceptionIfEmpty()
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isReadable('', 'foobar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidPropertyPaths
|
||||
*/
|
||||
public function testIsWritable($objectOrArray, $path)
|
||||
{
|
||||
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path, 'Updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getPathsWithMissingProperty
|
||||
*/
|
||||
public function testIsWritableReturnsFalseIfPropertyNotFound($objectOrArray, $path)
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path, 'Updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getPathsWithMissingIndex
|
||||
*/
|
||||
public function testIsWritableReturnsTrueIfIndexNotFound($objectOrArray, $path)
|
||||
{
|
||||
// Non-existing indices can be written. Arrays are created on-demand.
|
||||
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path, 'Updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getPathsWithMissingIndex
|
||||
*/
|
||||
public function testIsWritableReturnsTrueIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
|
||||
{
|
||||
$this->propertyAccessor = new PropertyAccessor(false, true);
|
||||
|
||||
// Non-existing indices can be written even if exceptions are enabled
|
||||
$this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path, 'Updated'));
|
||||
}
|
||||
|
||||
public function testIsWritableRecognizesMagicSet()
|
||||
{
|
||||
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicGet('Bernhard'), 'magicProperty', 'Updated'));
|
||||
}
|
||||
|
||||
public function testIsWritableDoesNotRecognizeMagicCallByDefault()
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty', 'Updated'));
|
||||
}
|
||||
|
||||
public function testIsWritableRecognizesMagicCallIfEnabled()
|
||||
{
|
||||
$this->propertyAccessor = new PropertyAccessor(true);
|
||||
|
||||
$this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty', 'Updated'));
|
||||
}
|
||||
|
||||
public function testIsWritableThrowsExceptionIfNotObjectOrArray()
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isWritable('baz', 'foobar', 'Updated'));
|
||||
}
|
||||
|
||||
public function testIsWritableThrowsExceptionIfNull()
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isWritable(null, 'foobar', 'Updated'));
|
||||
}
|
||||
|
||||
public function testIsWritableThrowsExceptionIfEmpty()
|
||||
{
|
||||
$this->assertFalse($this->propertyAccessor->isWritable('', 'foobar', 'Updated'));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user