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

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'));
}
}