-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[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
Changes from 1 commit
0488389
20e6bf8
6d2af21
4262707
9aee2ad
f7fb855
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
--------- | ||
|
||
|
@@ -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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it not require There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Depends on your POV. My idea was to return There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
* | ||
|
@@ -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; | ||
|
@@ -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 { | ||
|
@@ -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. | ||
* | ||
|
@@ -409,6 +519,8 @@ private function camelize($string) | |
*/ | ||
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) | ||
{ | ||
$exception = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
@@ -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, | ||
|
@@ -430,6 +542,10 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula | |
} | ||
} | ||
|
||
if (null !== $exception) { | ||
throw $exception; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
* | ||
|
@@ -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 @@ 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the goal of There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing
[BC Break]
prefixThere was a problem hiding this comment.
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
There was a problem hiding this comment.
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.