Skip to content

[PropertyAccess] Added isReadable() and isWritable() #10570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 31, 2014
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
[PropertyAccess] Added isReadable() and isWritable()
  • Loading branch information
webmozart committed Mar 28, 2014
commit 6d2af217aafd03e3f1600ce0ebc9c30cf0a7fc70
43 changes: 42 additions & 1 deletion UPGRADE-2.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,47 @@ Form
{
```

PropertyAccess
--------------

* The methods `isReadable()` and `isWritable()` were added to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing [BC Break] prefix

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, sorry, no. This file documents only BC breaks, so not needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never have this prefix in the UPGRADE file.

`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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I also had this one on my to-do list

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
---------

Expand All @@ -56,7 +97,7 @@ Validator

After:

Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be
Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be
valid. This is the default behaviour.

Strict email validation has to be explicitly activated in the configuration file by adding
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/PropertyAccess/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
------
Expand Down
126 changes: 121 additions & 5 deletions src/Symfony/Component/PropertyAccess/PropertyAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,84 @@ public function setValue(&$objectOrArray, $propertyPath, $value)
}
}

/**
* {@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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it not require $this->throwExceptionOnInvalidIndex = true? otherwise the method says it's readable when it's actually not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on your POV. My idea was to return true from isReadable() whenever getValue() can be called successfully. The other possible POV is to only return true when a path actually exists in the graph. Which do you prefer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense that isReadable is the equivalent to getValue


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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imho UnexpectedTypeException is not semantically correct because its defined as RuntimeException but here it is clearly a LogicException. UnexpectedTypeException is correctly used for checks whether value in path is array or object. But we should destringuish these two cases.
So for this case here, we should instead throw a InvalidPropertyPathException and it should extend LogicException instead.
Currently all methods in AccessorInterface are missing phpdoc for InvalidPropertyPathException anyway which can be raised by new PropertyPath($propertyPath).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently isReadable and isWriteable phpdoc are missing UnexpectedTypeException and InvalidPropertyPathException. With my suggestion above, only InvalidPropertyPathException could be raised.

}

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.
*
Expand Down Expand Up @@ -357,9 +435,9 @@ private function writeProperty(&$object, $property, $singular, $value)
$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;
Expand All @@ -370,7 +448,7 @@ private function writeProperty(&$object, $property, $singular, $value)
// 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 {
Expand All @@ -385,6 +463,38 @@ private function writeProperty(&$object, $property, $singular, $value)
}
}

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.
*
Expand All @@ -409,6 +519,8 @@ private function camelize($string)
*/
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
{
$exception = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like a dead assignment


foreach ($singulars as $singular) {
$addMethod = 'add'.$singular;
$removeMethod = 'remove'.$singular;
Expand All @@ -420,8 +532,8 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula
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,
Expand All @@ -430,6 +542,10 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula
}
}

if (null !== $exception) {
throw $exception;
}

return null;
}

Expand Down
31 changes: 29 additions & 2 deletions src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
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:
*
Expand Down Expand Up @@ -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:
*
Expand Down Expand Up @@ -78,4 +78,31 @@ public function setValue(&$objectOrArray, $propertyPath, $value);
* @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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the goal of $value in this method ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To find out whether adders/removers should be checked (only if value is a collection).


/**
* 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,63 @@ public function testSetValueFailsIfOnlyRemoverFound()
* @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));
}
}
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy