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

This PR was merged into the 2.5-dev branch.

Discussion
----------

[PropertyAccess] Added isReadable() and isWritable()

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #8659
| License       | MIT
| Doc PR        | symfony/symfony-docs#3729

This PR introduces BC breaks that are described in detail in the UPGRADE file. The BC breaks conform to our policy. They shouldn't affect many people, so I think we can safely do them.

Commits
-------

f7fb855 [PropertyAccess] Added missing exceptions to phpdoc
9aee2ad [PropertyAccess] Removed the argument $value from isWritable()
4262707 [PropertyAccess] Fixed CS and added missing documentation
6d2af21 [PropertyAccess] Added isReadable() and isWritable()
20e6bf8 [PropertyAccess] Refactored PropertyAccessorCollectionTest
0488389 [PropertyAccess] Refactored PropertyAccessorTest
This commit is contained in:
Fabien Potencier 2014-03-31 13:04:12 +02:00
commit c5a3008123
13 changed files with 878 additions and 654 deletions

View File

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

View File

@ -5,6 +5,10 @@ CHANGELOG
------
* allowed non alpha numeric characters in second level and deeper object properties names
* [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
------

View File

@ -0,0 +1,21 @@
<?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\Exception;
/**
* Base InvalidArgumentException for the PropertyAccess component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
@ -33,7 +34,7 @@ class PropertyAccessor implements PropertyAccessorInterface
/**
* @var Boolean
*/
private $throwExceptionOnInvalidIndex;
private $ignoreInvalidIndices;
/**
* Should not be used by application code. Use
@ -42,7 +43,7 @@ class PropertyAccessor implements PropertyAccessorInterface
public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false)
{
$this->magicCall = $magicCall;
$this->throwExceptionOnInvalidIndex = $throwExceptionOnInvalidIndex;
$this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex;
}
/**
@ -53,10 +54,15 @@ class PropertyAccessor implements PropertyAccessorInterface
if (is_string($propertyPath)) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface');
throw new InvalidArgumentException(sprintf(
'The property path should be a string or an instance of '.
'"Symfony\Component\PropertyAccess\PropertyPathInterface". '.
'Got: "%s"',
is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath)
));
}
$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];
}
@ -69,7 +75,12 @@ class PropertyAccessor implements PropertyAccessorInterface
if (is_string($propertyPath)) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface');
throw new InvalidArgumentException(sprintf(
'The property path should be a string or an instance of '.
'"Symfony\Component\PropertyAccess\PropertyPathInterface". '.
'Got: "%s"',
is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath)
));
}
$propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1);
@ -90,13 +101,11 @@ class PropertyAccessor implements PropertyAccessorInterface
}
$property = $propertyPath->getElement($i);
//$singular = $propertyPath->singulars[$i];
$singular = null;
if ($propertyPath->isIndex($i)) {
$this->writeIndex($objectOrArray, $property, $value);
} else {
$this->writeProperty($objectOrArray, $property, $singular, $value);
$this->writeProperty($objectOrArray, $property, $value);
}
}
@ -105,18 +114,108 @@ class PropertyAccessor implements PropertyAccessorInterface
}
}
/**
* {@inheritdoc}
*/
public function isReadable($objectOrArray, $propertyPath)
{
if (is_string($propertyPath)) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new InvalidArgumentException(sprintf(
'The property path should be a string or an instance of '.
'"Symfony\Component\PropertyAccess\PropertyPathInterface". '.
'Got: "%s"',
is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath)
));
}
try {
$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return true;
} catch (NoSuchIndexException $e) {
return false;
} catch (NoSuchPropertyException $e) {
return false;
} catch (UnexpectedTypeException $e) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function isWritable($objectOrArray, $propertyPath)
{
if (is_string($propertyPath)) {
$propertyPath = new PropertyPath($propertyPath);
} elseif (!$propertyPath instanceof PropertyPathInterface) {
throw new InvalidArgumentException(sprintf(
'The property path should be a string or an instance of '.
'"Symfony\Component\PropertyAccess\PropertyPathInterface". '.
'Got: "%s"',
is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath)
));
}
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)) {
return false;
}
}
}
$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.
*
* @param object|array $objectOrArray The object or array to read from
* @param PropertyPathInterface $propertyPath The property path to read
* @param integer $lastIndex The index up to which should be read
* @param object|array $objectOrArray The object or array to read from
* @param PropertyPathInterface $propertyPath The property path to read
* @param integer $lastIndex The index up to which should be read
* @param Boolean $ignoreInvalidIndices Whether to ignore invalid indices
* or throw an exception
*
* @return array The values read in the path.
*
* @throws UnexpectedTypeException If a value within the path is neither object nor array.
* @throws NoSuchIndexException If a non-existing index is accessed
*/
private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $throwExceptionOnNonexistantIndex = false)
private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = true)
{
$propertyValues = array();
@ -131,9 +230,10 @@ class PropertyAccessor implements PropertyAccessorInterface
// Create missing nested arrays on demand
if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) {
if ($throwExceptionOnNonexistantIndex) {
if (!$ignoreInvalidIndices) {
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;
}
@ -159,12 +259,12 @@ class PropertyAccessor implements PropertyAccessorInterface
*
* @return mixed The value of the key
*
* @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array
* @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
*/
private function &readIndex(&$array, $index)
{
if (!$array instanceof \ArrayAccess && !is_array($array)) {
throw new NoSuchPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array)));
throw new NoSuchIndexException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array)));
}
// Use an array instead of an object since performance is very crucial here
@ -264,101 +364,60 @@ class PropertyAccessor implements PropertyAccessorInterface
}
/**
* Sets the value of the property at the given index in the path
* Sets the value of an index in a given array-accessible value.
*
* @param \ArrayAccess|array $array An array or \ArrayAccess object to write to
* @param string|integer $index The index to write at
* @param mixed $value The value to write
*
* @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array
* @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
*/
private function writeIndex(&$array, $index, $value)
{
if (!$array instanceof \ArrayAccess && !is_array($array)) {
throw new NoSuchPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array)));
throw new NoSuchIndexException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array)));
}
$array[$index] = $value;
}
/**
* Sets the value of the property at the given index in the path
* Sets the value of a property in the given object
*
* @param object|array $object The object or array to write to
* @param string $property The property to write
* @param string|null $singular The singular form of the property name or null
* @param mixed $value The value to write
* @param object $object The object to write to
* @param string $property The property to write
* @param mixed $value The value to write
*
* @throws NoSuchPropertyException If the property does not exist or is not
* public.
*/
private function writeProperty(&$object, $property, $singular, $value)
private function writeProperty(&$object, $property, $value)
{
$guessedAdders = '';
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 = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural);
$singulars = (array) StringUtil::singularify($plural);
if (is_array($value) || $value instanceof \Traversable) {
$methods = $this->findAdderAndRemover($reflClass, $singulars);
// Use addXxx() and removeXxx() to write the collection
if (null !== $methods) {
// At this point the add and remove methods have been found
// Use iterator_to_array() instead of clone in order to prevent side effects
// see https://github.com/symfony/symfony/issues/4670
$itemsToAdd = is_object($value) ? iterator_to_array($value) : $value;
$itemToRemove = array();
$propertyValue = $this->readProperty($object, $property);
$previousValue = $propertyValue[self::VALUE];
if (is_array($previousValue) || $previousValue instanceof \Traversable) {
foreach ($previousValue as $previousItem) {
foreach ($value as $key => $item) {
if ($item === $previousItem) {
// Item found, don't add
unset($itemsToAdd[$key]);
// Next $previousItem
continue 2;
}
}
// Item not found, add to remove list
$itemToRemove[] = $previousItem;
}
}
foreach ($itemToRemove as $item) {
call_user_func(array($object, $methods[1]), $item);
}
foreach ($itemsToAdd as $item) {
call_user_func(array($object, $methods[0]), $item);
}
$this->writeCollection($object, $property, $value, $methods[0], $methods[1]);
return;
} else {
// It is sufficient to include only the adders in the error
// message. If the user implements the adder but not the remover,
// an exception will be thrown in findAdderAndRemover() that
// the remover has to be implemented as well.
$guessedAdders = '"add'.implode('()", "add', $singulars).'()", ';
}
}
$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;
@ -369,7 +428,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 {
@ -377,13 +436,100 @@ class PropertyAccessor implements PropertyAccessorInterface
'Neither the property "%s" nor one of the methods %s"%s()", '.
'"__set()" or "__call()" exist and have public access in class "%s".',
$property,
$guessedAdders,
implode('', array_map(function ($singular) {
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
}, $singulars)),
$setter,
$reflClass->name
));
}
}
/**
* Adjusts a collection-valued property by calling add*() and remove*()
* methods.
*
* @param object $object The object to write to
* @param string $property The property to write
* @param array|\Traversable $collection The collection to write
* @param string $addMethod The add*() method
* @param string $removeMethod The remove*() method
*/
private function writeCollection($object, $property, $collection, $addMethod, $removeMethod)
{
// At this point the add and remove methods have been found
// Use iterator_to_array() instead of clone in order to prevent side effects
// see https://github.com/symfony/symfony/issues/4670
$itemsToAdd = is_object($collection) ? iterator_to_array($collection) : $collection;
$itemToRemove = array();
$propertyValue = $this->readProperty($object, $property);
$previousValue = $propertyValue[self::VALUE];
if (is_array($previousValue) || $previousValue instanceof \Traversable) {
foreach ($previousValue as $previousItem) {
foreach ($collection as $key => $item) {
if ($item === $previousItem) {
// Item found, don't add
unset($itemsToAdd[$key]);
// Next $previousItem
continue 2;
}
}
// Item not found, add to remove list
$itemToRemove[] = $previousItem;
}
}
foreach ($itemToRemove as $item) {
call_user_func(array($object, $removeMethod), $item);
}
foreach ($itemsToAdd as $item) {
call_user_func(array($object, $addMethod), $item);
}
}
/**
* Returns whether a property is writable in the given object.
*
* @param object $object The object to write to
* @param string $property The property to write
*
* @return Boolean Whether the property is writable
*/
private function isPropertyWritable($object, $property)
{
if (!is_object($object)) {
return false;
}
$reflClass = new \ReflectionClass($object);
$setter = 'set'.$this->camelize($property);
$classHasProperty = $reflClass->hasProperty($property);
if ($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))) {
return true;
}
$plural = $this->camelize($property);
// Any of the two methods is required, but not yet known
$singulars = (array) StringUtil::singularify($plural);
if (null !== $this->findAdderAndRemover($reflClass, $singulars)) {
return true;
}
return false;
}
/**
* Camelizes a given string.
*
@ -403,30 +549,21 @@ class PropertyAccessor implements PropertyAccessorInterface
* @param array $singulars The singular form of the property name or null
*
* @return array|null An array containing the adder and remover when found, null otherwise
*
* @throws NoSuchPropertyException If the property does not exist
*/
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
{
$exception = null;
foreach ($singulars as $singular) {
$addMethod = 'add'.$singular;
$removeMethod = 'remove'.$singular;
$addMethodFound = $this->isAccessible($reflClass, $addMethod, 1);
$removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1);
$addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1);
$removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1);
if ($addMethodFound && $removeMethodFound) {
return array($addMethod, $removeMethod);
}
if ($addMethodFound xor $removeMethodFound) {
throw new NoSuchPropertyException(sprintf(
'Found the public method "%s()", but did not find a public "%s()" on class %s',
$addMethodFound ? $addMethod : $removeMethod,
$addMethodFound ? $removeMethod : $addMethod,
$reflClass->name
));
}
}
return null;
@ -442,7 +579,7 @@ class PropertyAccessor implements PropertyAccessorInterface
* @return Boolean Whether the method is public and has $parameters
* required parameters
*/
private function isAccessible(\ReflectionClass $class, $methodName, $parameters)
private function isMethodAccessible(\ReflectionClass $class, $methodName, $parameters)
{
if ($class->hasMethod($methodName)) {
$method = $class->getMethod($methodName);

View File

@ -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:
*
@ -43,14 +43,15 @@ interface PropertyAccessorInterface
* @param string|PropertyPathInterface $propertyPath The property path to modify
* @param mixed $value The value to set at the end of the property path
*
* @throws Exception\NoSuchPropertyException If a property does not exist or is not public.
* @throws Exception\UnexpectedTypeException If a value within the path is neither object
* nor array
* @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public
* @throws Exception\UnexpectedTypeException If a value within the path is neither object
* nor array
*/
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:
*
@ -75,7 +76,40 @@ interface PropertyAccessorInterface
*
* @return mixed The value at the end of the property path
*
* @throws Exception\NoSuchPropertyException If a property does not exist or is not public.
* @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public
* @throws Exception\UnexpectedTypeException If a value within the path is neither object
* nor array
*/
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
*
* @return Boolean Whether the value can be set
*
* @throws Exception\InvalidArgumentException If the property path is invalid
*/
public function isWritable($objectOrArray, $propertyPath);
/**
* 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
*
* @throws Exception\InvalidArgumentException If the property path is invalid
*/
public function isReadable($objectOrArray, $propertyPath);
}

View File

@ -1,71 +0,0 @@
<?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;
class Author
{
public $firstName;
private $lastName;
private $australian;
public $child;
private $readPermissions;
private $privateProperty;
public function setLastName($lastName)
{
$this->lastName = $lastName;
}
public function getLastName()
{
return $this->lastName;
}
private function getPrivateGetter()
{
return 'foobar';
}
public function setAustralian($australian)
{
$this->australian = $australian;
}
public function isAustralian()
{
return $this->australian;
}
public function setReadPermissions($bool)
{
$this->readPermissions = $bool;
}
public function hasReadPermissions()
{
return $this->readPermissions;
}
private function isPrivateIsser()
{
return true;
}
public function getPrivateSetter()
{
}
private function setPrivateSetter($data)
{
}
}

View File

@ -1,27 +0,0 @@
<?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;
class Magician
{
private $foobar;
public function __set($property, $value)
{
$this->$property = $value;
}
public function __get($property)
{
return isset($this->$property) ? $this->$property : null;
}
}

View File

@ -1,28 +0,0 @@
<?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;
class MagicianCall
{
private $foobar;
public function __call($name, $args)
{
$property = lcfirst(substr($name, 3));
if ('get' === substr($name, 0, 3)) {
return isset($this->$property) ? $this->$property : null;
} elseif ('set' === substr($name, 0, 3)) {
$value = 1 == count($args) ? $args[0] : null;
$this->$property = $value;
}
}
}

View File

@ -0,0 +1,115 @@
<?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;
class TestClass
{
public $publicProperty;
protected $protectedProperty;
private $privateProperty;
private $publicAccessor;
private $publicIsAccessor;
private $publicHasAccessor;
public function __construct($value)
{
$this->publicProperty = $value;
$this->publicAccessor = $value;
$this->publicIsAccessor = $value;
$this->publicHasAccessor = $value;
}
public function setPublicAccessor($value)
{
$this->publicAccessor = $value;
}
public function getPublicAccessor()
{
return $this->publicAccessor;
}
public function setPublicIsAccessor($value)
{
$this->publicIsAccessor = $value;
}
public function isPublicIsAccessor()
{
return $this->publicIsAccessor;
}
public function setPublicHasAccessor($value)
{
$this->publicHasAccessor = $value;
}
public function hasPublicHasAccessor()
{
return $this->publicHasAccessor;
}
protected function setProtectedAccessor($value)
{
}
protected function getProtectedAccessor()
{
return 'foobar';
}
protected function setProtectedIsAccessor($value)
{
}
protected function isProtectedIsAccessor()
{
return 'foobar';
}
protected function setProtectedHasAccessor($value)
{
}
protected function hasProtectedHasAccessor()
{
return 'foobar';
}
private function setPrivateAccessor($value)
{
}
private function getPrivateAccessor()
{
return 'foobar';
}
private function setPrivateIsAccessor($value)
{
}
private function isPrivateIsAccessor()
{
return 'foobar';
}
private function setPrivateHasAccessor($value)
{
}
private function hasPrivateHasAccessor()
{
return 'foobar';
}
}

View File

@ -0,0 +1,39 @@
<?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;
class TestClassMagicCall
{
private $magicCallProperty;
public function __construct($value)
{
$this->magicCallProperty = $value;
}
public function __call($method, array $args)
{
if ('getMagicCallProperty' === $method) {
return $this->magicCallProperty;
}
if ('getConstantMagicCallProperty' === $method) {
return 'constant value';
}
if ('setMagicCallProperty' === $method) {
$this->magicCallProperty = reset($args);
}
return null;
}
}

View File

@ -0,0 +1,42 @@
<?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;
class TestClassMagicGet
{
private $magicProperty;
public function __construct($value)
{
$this->magicProperty = $value;
}
public function __set($property, $value)
{
if ('magicProperty' === $property) {
$this->magicProperty = $value;
}
}
public function __get($property)
{
if ('magicProperty' === $property) {
return $this->magicProperty;
}
if ('constantMagicProperty' === $property) {
return 'constant value';
}
return null;
}
}

View File

@ -11,9 +11,7 @@
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\Exception\ExceptionInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\StringUtil;
class PropertyAccessorCollectionTest_Car
{
@ -47,19 +45,6 @@ class PropertyAccessorCollectionTest_Car
}
}
class PropertyAccessorCollectionTest_CarCustomSingular
{
public function addFoo($axis) {}
public function removeFoo($axis) {}
public function getAxes() {}
}
class PropertyAccessorCollectionTest_Engine
{
}
class PropertyAccessorCollectionTest_CarOnlyAdder
{
public function addAxis($axis) {}
@ -79,13 +64,6 @@ class PropertyAccessorCollectionTest_CarNoAdderAndRemover
public function getAxes() {}
}
class PropertyAccessorCollectionTest_CarNoAdderAndRemoverWithProperty
{
protected $axes = array();
public function getAxes() {}
}
class PropertyAccessorCollectionTest_CompositeCar
{
public function getStructure() {}
@ -116,52 +94,34 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
abstract protected function getCollection(array $array);
public function testGetValueReadsArrayAccess()
public function getValidPropertyPaths()
{
$object = $this->getCollection(array('firstName' => 'Bernhard'));
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, '[firstName]'));
}
public function testGetValueReadsNestedArrayAccess()
{
$object = $this->getCollection(array('person' => array('firstName' => 'Bernhard')));
$this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, '[person][firstName]'));
return array(
array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'),
array(array('person' => array('firstName' => 'Bernhard')), '[person][firstName]', 'Bernhard'),
);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* @dataProvider getValidPropertyPaths
*/
public function testGetValueThrowsExceptionIfArrayAccessExpected()
public function testGetValue(array $array, $path, $value)
{
$this->propertyAccessor->getValue(new \stdClass(), '[firstName]');
}
$collection = $this->getCollection($array);
public function testSetValueUpdatesArrayAccess()
{
$object = $this->getCollection(array());
$this->propertyAccessor->setValue($object, '[firstName]', 'Bernhard');
$this->assertEquals('Bernhard', $object['firstName']);
}
public function testSetValueUpdatesNestedArrayAccess()
{
$object = $this->getCollection(array());
$this->propertyAccessor->setValue($object, '[person][firstName]', 'Bernhard');
$this->assertEquals('Bernhard', $object['person']['firstName']);
$this->assertSame($value, $this->propertyAccessor->getValue($collection, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* @dataProvider getValidPropertyPaths
*/
public function testSetValueThrowsExceptionIfArrayAccessExpected()
public function testSetValue(array $array, $path)
{
$this->propertyAccessor->setValue(new \stdClass(), '[firstName]', 'Bernhard');
$collection = $this->getCollection($array);
$this->propertyAccessor->setValue($collection, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($collection, $path));
}
public function testSetValueCallsAdderAndRemoverForCollections()
@ -210,115 +170,67 @@ abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCas
$this->propertyAccessor->setValue($car, 'structure.axes', $axesAfter);
}
public function testSetValueCallsCustomAdderAndRemover()
{
$this->markTestSkipped('This feature is temporarily disabled as of 2.1');
$car = $this->getMock(__CLASS__.'_CarCustomSingular');
$axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth'));
$axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
$car->expects($this->at(0))
->method('getAxes')
->will($this->returnValue($axesBefore));
$car->expects($this->at(1))
->method('removeFoo')
->with('fourth');
$car->expects($this->at(2))
->method('addFoo')
->with('first');
$car->expects($this->at(3))
->method('addFoo')
->with('third');
$this->propertyAccessor->setValue($car, 'axes|foo', $axesAfter);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()"/"removeAx()", "addAxe()"/"removeAxe()", "addAxis()"/"removeAxis()", "setAxes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover
*/
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'));
$car->expects($this->any())
->method('getAxes')
->will($this->returnValue($axesBefore));
$this->propertyAccessor->setValue($car, 'axes', $axesAfter);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
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'));
$car->expects($this->any())
->method('getAxes')
->will($this->returnValue($axesBefore));
$this->propertyAccessor->setValue($car, 'axes', $axesAfter);
}
/**
* @dataProvider noAdderRemoverData
*/
public function testNoAdderAndRemoverThrowsSensibleError($car, $path, $message)
public function testSetValueFailsIfNoAdderNorRemoverFound()
{
$car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover');
$axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third'));
try {
$this->propertyAccessor->setValue($car, $path, $axes);
$this->fail('An expected exception was not thrown!');
} catch (ExceptionInterface $e) {
$this->assertEquals($message, $e->getMessage());
}
$this->propertyAccessor->setValue($car, 'axes', $axes);
}
public function noAdderRemoverData()
/**
* @dataProvider getValidPropertyPaths
*/
public function testIsReadable(array $array, $path)
{
$data = array();
$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');
$propertyPath = 'axes';
$expectedMessage = sprintf(
'Neither the property "axes" nor one of the methods "addAx()", '.
'"addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have '.
'public access in class "%s".',
get_class($car)
);
$data[] = array($car, $propertyPath, $expectedMessage);
$axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third'));
/*
Temporarily disabled in 2.1
$propertyPath = new PropertyPath('axes|boo');
$expectedMessage = sprintf(
'Neither element "axes" nor method "setAxes()" exists in class '
.'"%s", nor could adders and removers be found based on the '
.'passed singular: %s',
get_class($car),
'boo'
);
$data[] = array($car, $propertyPath, $expectedMessage);
*/
$car = $this->getMock(__CLASS__.'_CarNoAdderAndRemoverWithProperty');
$propertyPath = 'axes';
$expectedMessage = sprintf(
'Neither the property "axes" nor one of the methods "addAx()", '.
'"addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have '.
'public access in class "%s".',
get_class($car)
);
$data[] = array($car, $propertyPath, $expectedMessage);
return $data;
$this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes));
}
}

View File

@ -12,228 +12,161 @@
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\Tests\Fixtures\Author;
use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician;
use Symfony\Component\PropertyAccess\Tests\Fixtures\MagicianCall;
use Symfony\Component\PropertyAccess\PropertyAccessorBuilder;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet;
class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
{
/**
* @var PropertyAccessorBuilder
* @var PropertyAccessor
*/
private $propertyAccessorBuilder;
private $propertyAccessor;
protected function setUp()
{
$this->propertyAccessorBuilder = new PropertyAccessorBuilder();
$this->propertyAccessor = new PropertyAccessor();
}
public function getValidPropertyPaths()
{
return array(
array(array('Bernhard', 'Schussek'), '[0]', 'Bernhard'),
array(array('Bernhard', 'Schussek'), '[1]', 'Schussek'),
array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'),
array(array('index' => array('firstName' => 'Bernhard')), '[index][firstName]', 'Bernhard'),
array((object) array('firstName' => 'Bernhard'), 'firstName', 'Bernhard'),
array((object) array('property' => array('firstName' => 'Bernhard')), 'property[firstName]', 'Bernhard'),
array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].firstName', 'Bernhard'),
array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.firstName', 'Bernhard'),
// Accessor methods
array(new TestClass('Bernhard'), 'publicProperty', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'),
array(new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'),
// Methods are camelized
array(new TestClass('Bernhard'), 'public_accessor', 'Bernhard'),
// Missing indices
array(array('index' => array()), '[index][firstName]', null),
array(array('root' => array('index' => array())), '[root][index][firstName]', null),
// Special chars
array(array('%!@$§.' => 'Bernhard'), '[%!@$§.]', 'Bernhard'),
array(array('index' => array('%!@$§.' => 'Bernhard')), '[index][%!@$§.]', 'Bernhard'),
array((object) array('%!@$§' => 'Bernhard'), '%!@$§', 'Bernhard'),
array((object) array('property' => (object) array('%!@$§' => 'Bernhard')), 'property.%!@$§', 'Bernhard'),
);
}
public function getPathsWithMissingProperty()
{
return array(
array((object) array('firstName' => 'Bernhard'), 'lastName'),
array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.lastName'),
array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].lastName'),
array(new TestClass('Bernhard'), 'protectedProperty'),
array(new TestClass('Bernhard'), 'privateProperty'),
array(new TestClass('Bernhard'), 'protectedAccessor'),
array(new TestClass('Bernhard'), 'protectedIsAccessor'),
array(new TestClass('Bernhard'), 'protectedHasAccessor'),
array(new TestClass('Bernhard'), 'privateAccessor'),
array(new TestClass('Bernhard'), 'privateIsAccessor'),
array(new TestClass('Bernhard'), 'privateHasAccessor'),
// Properties are not camelized
array(new TestClass('Bernhard'), 'public_property'),
);
}
public function getPathsWithMissingIndex()
{
return array(
array(array('firstName' => 'Bernhard'), '[lastName]'),
array(array(), '[index][lastName]'),
array(array('index' => array()), '[index][lastName]'),
array(array('index' => array('firstName' => 'Bernhard')), '[index][lastName]'),
array((object) array('property' => array('firstName' => 'Bernhard')), 'property[lastName]'),
);
}
/**
* Get PropertyAccessor configured
*
* @param string $withMagicCall
* @param string $throwExceptionOnInvalidIndex
* @return PropertyAccessorInterface
* @dataProvider getValidPropertyPaths
*/
protected function getPropertyAccessor($withMagicCall = false, $throwExceptionOnInvalidIndex = false)
public function testGetValue($objectOrArray, $path, $value)
{
if ($withMagicCall) {
$this->propertyAccessorBuilder->enableMagicCall();
} else {
$this->propertyAccessorBuilder->disableMagicCall();
}
if ($throwExceptionOnInvalidIndex) {
$this->propertyAccessorBuilder->enableExceptionOnInvalidIndex();
} else {
$this->propertyAccessorBuilder->disableExceptionOnInvalidIndex();
}
return $this->propertyAccessorBuilder->getPropertyAccessor();
}
public function testGetValueReadsArray()
{
$array = array('firstName' => 'Bernhard');
$this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[firstName]'));
$this->assertSame($value, $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingProperty
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueThrowsExceptionIfIndexNotationExpected()
public function testGetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path)
{
$array = array('firstName' => 'Bernhard');
$this->getPropertyAccessor()->getValue($array, 'firstName');
}
public function testGetValueReadsZeroIndex()
{
$array = array('Bernhard');
$this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[0]'));
}
public function testGetValueReadsIndexWithSpecialChars()
{
$array = array('%!@$§.' => 'Bernhard');
$this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[%!@$§.]'));
}
public function testGetValueReadsNestedIndexWithSpecialChars()
{
$array = array('root' => array('%!@$§.' => 'Bernhard'));
$this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[root][%!@$§.]'));
}
public function testGetValueReadsArrayWithCustomPropertyPath()
{
$array = array('child' => array('index' => array('firstName' => 'Bernhard')));
$this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[child][index][firstName]'));
}
public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath()
{
$array = array('child' => array('index' => array()));
// No BC break
$this->assertNull($this->getPropertyAccessor()->getValue($array, '[child][index][firstName]'));
try {
$this->getPropertyAccessor(false, true)->getValue($array, '[child][index][firstName]');
$this->fail('Getting value on a nonexistent path from array should throw a Symfony\Component\PropertyAccess\Exception\NoSuchIndexException exception');
} catch (\Exception $e) {
$this->assertInstanceof('Symfony\Component\PropertyAccess\Exception\NoSuchIndexException', $e, 'Getting value on a nonexistent path from array should throw a Symfony\Component\PropertyAccess\Exception\NoSuchIndexException exception');
}
}
public function testGetValueReadsProperty()
{
$object = new Author();
$object->firstName = 'Bernhard';
$this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($object, 'firstName'));
}
public function testGetValueIgnoresSingular()
{
$this->markTestSkipped('This feature is temporarily disabled as of 2.1');
$object = (object) array('children' => 'Many');
$this->assertEquals('Many', $this->getPropertyAccessor()->getValue($object, 'children|child'));
}
public function testGetValueReadsPropertyWithSpecialCharsExceptDot()
{
$array = (object) array('%!@$§' => 'Bernhard');
$this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '%!@$§'));
}
public function testGetValueReadsPropertyWithSpecialCharsExceptDotNested()
{
$object = (object) array('nested' => (object) array('@child' => 'foo'));
$this->assertEquals('foo', $this->getPropertyAccessor()->getValue($object, 'nested.@child'));
}
public function testGetValueReadsPropertyWithCustomPropertyPath()
{
$object = new Author();
$object->child = array();
$object->child['index'] = new Author();
$object->child['index']->firstName = 'Bernhard';
$this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($object, 'child[index].firstName'));
$this->propertyAccessor->getValue($objectOrArray, $path);
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* @dataProvider getPathsWithMissingIndex
*/
public function testGetValueThrowsExceptionIfPropertyIsNotPublic()
public function testGetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path)
{
$this->getPropertyAccessor()->getValue(new Author(), 'privateProperty');
}
public function testGetValueReadsGetters()
{
$object = new Author();
$object->setLastName('Schussek');
$this->assertEquals('Schussek', $this->getPropertyAccessor()->getValue($object, 'lastName'));
}
public function testGetValueCamelizesGetterNames()
{
$object = new Author();
$object->setLastName('Schussek');
$this->assertEquals('Schussek', $this->getPropertyAccessor()->getValue($object, 'last_name'));
$this->assertNull($this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* @dataProvider getPathsWithMissingIndex
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testGetValueThrowsExceptionIfGetterIsNotPublic()
public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->getPropertyAccessor()->getValue(new Author(), 'privateGetter');
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor->getValue($objectOrArray, $path);
}
public function testGetValueReadsIssers()
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testGetValueThrowsExceptionIfNotArrayAccess()
{
$object = new Author();
$object->setAustralian(false);
$this->assertFalse($this->getPropertyAccessor()->getValue($object, 'australian'));
}
public function testGetValueReadHassers()
{
$object = new Author();
$object->setReadPermissions(true);
$this->assertTrue($this->getPropertyAccessor()->getValue($object, 'read_permissions'));
$this->propertyAccessor->getValue(new \stdClass(), '[index]');
}
public function testGetValueReadsMagicGet()
{
$object = new Magician();
$object->__set('magicProperty', 'foobar');
$this->assertSame('foobar', $this->getPropertyAccessor()->getValue($object, 'magicProperty'));
$this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'magicProperty'));
}
/*
* https://github.com/symfony/symfony/pull/4450
*/
// https://github.com/symfony/symfony/pull/4450
public function testGetValueReadsMagicGetThatReturnsConstant()
{
$object = new Magician();
$this->assertNull($this->getPropertyAccessor()->getValue($object, 'magicProperty'));
$this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'constantMagicProperty'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueThrowsExceptionIfIsserIsNotPublic()
public function testGetValueDoesNotReadMagicCallByDefault()
{
$this->getPropertyAccessor()->getValue(new Author(), 'privateIsser');
$this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testGetValueThrowsExceptionIfPropertyDoesNotExist()
public function testGetValueReadsMagicCallIfEnabled()
{
$this->getPropertyAccessor()->getValue(new Author(), 'foobar');
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty'));
}
// https://github.com/symfony/symfony/pull/4450
public function testGetValueReadsMagicCallThatReturnsConstant()
{
$this->propertyAccessor = new PropertyAccessor(true);
$this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'constantMagicCallProperty'));
}
/**
@ -241,7 +174,7 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
*/
public function testGetValueThrowsExceptionIfNotObjectOrArray()
{
$this->getPropertyAccessor()->getValue('baz', 'foobar');
$this->propertyAccessor->getValue('baz', 'foobar');
}
/**
@ -249,7 +182,7 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
*/
public function testGetValueThrowsExceptionIfNull()
{
$this->getPropertyAccessor()->getValue(null, 'foobar');
$this->propertyAccessor->getValue(null, 'foobar');
}
/**
@ -257,101 +190,85 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
*/
public function testGetValueThrowsExceptionIfEmpty()
{
$this->getPropertyAccessor()->getValue('', 'foobar');
$this->propertyAccessor->getValue('', 'foobar');
}
public function testSetValueUpdatesArrays()
/**
* @dataProvider getValidPropertyPaths
*/
public function testSetValue($objectOrArray, $path)
{
$array = array();
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->getPropertyAccessor()->setValue($array, '[firstName]', 'Bernhard');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
}
$this->assertEquals(array('firstName' => 'Bernhard'), $array);
/**
* @dataProvider getPathsWithMissingProperty
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testSetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path)
{
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->propertyAccessor->setValue($objectOrArray, $path, 'Updated');
$this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException
*/
public function testSetValueThrowsExceptionIfNotArrayAccess()
{
$this->propertyAccessor->setValue(new \stdClass(), '[index]', 'Updated');
}
public function testSetValueUpdatesMagicSet()
{
$author = new TestClassMagicGet('Bernhard');
$this->propertyAccessor->setValue($author, 'magicProperty', 'Updated');
$this->assertEquals('Updated', $author->__get('magicProperty'));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueThrowsExceptionIfIndexNotationExpected()
public function testSetValueDoesNotUpdateMagicCallByDefault()
{
$array = array();
$author = new TestClassMagicCall('Bernhard');
$this->getPropertyAccessor()->setValue($array, 'firstName', 'Bernhard');
$this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated');
}
public function testSetValueUpdatesArraysWithCustomPropertyPath()
public function testSetValueUpdatesMagicCallIfEnabled()
{
$array = array();
$this->propertyAccessor = new PropertyAccessor(true);
$this->getPropertyAccessor()->setValue($array, '[child][index][firstName]', 'Bernhard');
$author = new TestClassMagicCall('Bernhard');
$this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array);
}
$this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated');
public function testSetValueUpdatesProperties()
{
$object = new Author();
$this->getPropertyAccessor()->setValue($object, 'firstName', 'Bernhard');
$this->assertEquals('Bernhard', $object->firstName);
}
public function testSetValueUpdatesPropertiesWithCustomPropertyPath()
{
$object = new Author();
$object->child = array();
$object->child['index'] = new Author();
$this->getPropertyAccessor()->setValue($object, 'child[index].firstName', 'Bernhard');
$this->assertEquals('Bernhard', $object->child['index']->firstName);
}
public function testSetValueUpdateMagicSet()
{
$object = new Magician();
$this->getPropertyAccessor()->setValue($object, 'magicProperty', 'foobar');
$this->assertEquals('foobar', $object->__get('magicProperty'));
}
public function testSetValueUpdatesSetters()
{
$object = new Author();
$this->getPropertyAccessor()->setValue($object, 'lastName', 'Schussek');
$this->assertEquals('Schussek', $object->getLastName());
}
public function testSetValueCamelizesSetterNames()
{
$object = new Author();
$this->getPropertyAccessor()->setValue($object, 'last_name', 'Schussek');
$this->assertEquals('Schussek', $object->getLastName());
}
public function testSetValueWithSpecialCharsNested()
{
$object = new \stdClass();
$person = new \stdClass();
$person->{'@email'} = null;
$object->person = $person;
$this->getPropertyAccessor()->setValue($object, 'person.@email', 'bar');
$this->assertEquals('bar', $object->person->{'@email'});
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
*/
public function testSetValueThrowsExceptionIfGetterIsNotPublic()
{
$this->getPropertyAccessor()->setValue(new Author(), 'privateSetter', 'foobar');
$this->assertEquals('Updated', $author->__call('getMagicCallProperty', array()));
}
/**
@ -361,7 +278,7 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
{
$value = 'baz';
$this->getPropertyAccessor()->setValue($value, 'foobar', 'bam');
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
}
/**
@ -371,7 +288,7 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
{
$value = null;
$this->getPropertyAccessor()->setValue($value, 'foobar', 'bam');
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
}
/**
@ -381,54 +298,142 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
{
$value = '';
$this->getPropertyAccessor()->setValue($value, 'foobar', 'bam');
$this->propertyAccessor->setValue($value, 'foobar', 'bam');
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* @dataProvider getValidPropertyPaths
*/
public function testSetValueFailsIfMagicCallDisabled()
public function testIsReadable($objectOrArray, $path)
{
$value = new MagicianCall();
$this->getPropertyAccessor()->setValue($value, 'foobar', 'bam');
$this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path));
}
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
* @dataProvider getPathsWithMissingProperty
*/
public function testGetValueFailsIfMagicCallDisabled()
public function testIsReadableReturnsFalseIfPropertyNotFound($objectOrArray, $path)
{
$value = new MagicianCall();
$this->getPropertyAccessor()->getValue($value, 'foobar', 'bam');
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
}
public function testGetValueReadsMagicCall()
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsReadableReturnsTrueIfIndexNotFound($objectOrArray, $path)
{
$propertyAccessor = new PropertyAccessor(true);
$object = new MagicianCall();
$object->setMagicProperty('foobar');
$this->assertSame('foobar', $propertyAccessor->getValue($object, 'magicProperty'));
// Non-existing indices can be read. In this case, null is returned
$this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path));
}
public function testGetValueReadsMagicCallThatReturnsConstant()
/**
* @dataProvider getPathsWithMissingIndex
*/
public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path)
{
$propertyAccessor = new PropertyAccessor(true);
$object = new MagicianCall();
$this->propertyAccessor = new PropertyAccessor(false, true);
$this->assertNull($propertyAccessor->getValue($object, 'MagicProperty'));
// When exceptions are enabled, non-existing indices cannot be read
$this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path));
}
public function testSetValueUpdatesMagicCall()
public function testIsReadableRecognizesMagicGet()
{
$propertyAccessor = new PropertyAccessor(true);
$object = new MagicianCall();
$propertyAccessor->setValue($object, 'magicProperty', 'foobar');
$this->assertEquals('foobar', $object->getMagicProperty());
$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'));
}
}