2013-01-07 08:19:31 +00:00
< ? 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 ;
2016-03-22 09:02:09 +00:00
use Symfony\Component\Inflector\Inflector ;
2015-02-21 12:49:08 +00:00
use Symfony\Component\PropertyAccess\Exception\AccessException ;
2016-03-22 08:55:46 +00:00
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException ;
2013-01-07 08:19:31 +00:00
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException ;
2013-09-27 15:14:57 +01:00
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException ;
2013-01-07 08:19:31 +00:00
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException ;
/**
* Default implementation of { @ link PropertyAccessorInterface } .
*
* @ author Bernhard Schussek < bschussek @ gmail . com >
2015-11-03 18:24:23 +00:00
* @ author Kévin Dunglas < dunglas @ gmail . com >
2016-03-17 14:00:21 +00:00
* @ author Nicolas Grekas < p @ tchwork . com >
2013-01-07 08:19:31 +00:00
*/
class PropertyAccessor implements PropertyAccessorInterface
{
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2013-01-07 08:19:31 +00:00
const VALUE = 0 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2016-03-17 14:00:21 +00:00
const REF = 1 ;
2015-12-04 23:04:49 +00:00
2015-12-15 01:31:17 +00:00
/**
* @ internal
*/
[property-access] Improvement for Accessing Reference Chain
Improve performance for the following scenarios:
- Example 1:
```php
$a = array(
'a' => array(
'b' => array(
'c' => 'old-value'
)
)
);
$pa->setValue($a, '[a][b][c]', 'new-value');
// The PropertyAccessor will try to set values for
// $a['a']['b']['c'], $a['a']['b'] and $a['a'],
// but in fact it may terminate the loop
// right after the value of $a[a][b][c] is set,
// because $a, $[a], $[a][b] and $[a][b][c]
// are all passed as reference - the reference chain is not broken.
```
- Example 2
```php
$b = array(
'a' => array(
'b' => array(
'c' => 'old-value'
)
)
)
$a = new Foo($b);
// In this example, the reference chain of $b is broken,
// because it's passed to $a.value as value
// But its elements are all passed as reference,
// so after setting the value for $b[a][b][c], there is no need
// to set value for $b[a][b] and $b[a]
$pa->setValue($a, 'value[a][b][c]', 'new-value');
```
| Q | A
| ------------- | ---
| Bug fix? | no
| New feature? | no
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | n/a
| License | MIT
| Doc PR | n/a
2015-08-03 08:13:18 +01:00
const IS_REF_CHAINED = 2 ;
2015-12-15 01:31:17 +00:00
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_HAS_PROPERTY = 0 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_TYPE = 1 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_NAME = 2 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_REF = 3 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_ADDER = 4 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_REMOVER = 5 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_TYPE_METHOD = 0 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_TYPE_PROPERTY = 1 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_TYPE_MAGIC = 2 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_TYPE_ADDER_AND_REMOVER = 3 ;
2015-12-04 23:04:49 +00:00
/**
* @ internal
*/
2015-11-03 18:24:23 +00:00
const ACCESS_TYPE_NOT_FOUND = 4 ;
2013-01-07 08:19:31 +00:00
2013-09-27 15:14:57 +01:00
/**
2014-04-16 11:34:42 +01:00
* @ var bool
2013-09-27 15:14:57 +01:00
*/
2013-03-04 22:59:45 +00:00
private $magicCall ;
2013-09-27 15:14:57 +01:00
/**
2014-04-16 11:34:42 +01:00
* @ var bool
2013-09-27 15:14:57 +01:00
*/
2014-03-28 18:21:04 +00:00
private $ignoreInvalidIndices ;
2013-09-27 15:14:57 +01:00
2015-11-03 18:24:23 +00:00
/**
* @ var array
*/
private $readPropertyCache = array ();
/**
* @ var array
*/
private $writePropertyCache = array ();
2016-03-22 08:55:46 +00:00
private static $previousErrorHandler = false ;
2016-03-17 14:00:21 +00:00
private static $errorHandler = array ( __CLASS__ , 'handleError' );
private static $resultProto = array ( self :: VALUE => null );
2015-11-03 18:24:23 +00:00
2013-01-07 08:19:31 +00:00
/**
* Should not be used by application code . Use
2013-08-24 23:47:14 +01:00
* { @ link PropertyAccess :: createPropertyAccessor ()} instead .
2015-11-26 21:41:57 +00:00
*
* @ param bool $magicCall
* @ param bool $throwExceptionOnInvalidIndex
2013-01-07 08:19:31 +00:00
*/
2013-09-27 15:14:57 +01:00
public function __construct ( $magicCall = false , $throwExceptionOnInvalidIndex = false )
2013-01-07 08:19:31 +00:00
{
2013-03-04 22:59:45 +00:00
$this -> magicCall = $magicCall ;
2014-03-28 18:21:04 +00:00
$this -> ignoreInvalidIndices = ! $throwExceptionOnInvalidIndex ;
2013-01-07 08:19:31 +00:00
}
/**
* { @ inheritdoc }
*/
public function getValue ( $objectOrArray , $propertyPath )
{
2015-02-19 12:40:39 +00:00
if ( ! $propertyPath instanceof PropertyPathInterface ) {
2013-01-07 08:19:31 +00:00
$propertyPath = new PropertyPath ( $propertyPath );
}
2016-03-17 14:00:21 +00:00
$zval = array (
self :: VALUE => $objectOrArray ,
);
2016-03-22 08:55:46 +00:00
$propertyValues = $this -> readPropertiesUntil ( $zval , $propertyPath , $propertyPath -> getLength (), $this -> ignoreInvalidIndices );
2013-01-07 08:19:31 +00:00
return $propertyValues [ count ( $propertyValues ) - 1 ][ self :: VALUE ];
}
/**
* { @ inheritdoc }
*/
public function setValue ( & $objectOrArray , $propertyPath , $value )
{
2015-02-19 12:40:39 +00:00
if ( ! $propertyPath instanceof PropertyPathInterface ) {
2013-01-07 08:19:31 +00:00
$propertyPath = new PropertyPath ( $propertyPath );
}
2016-03-17 14:00:21 +00:00
$zval = array (
self :: VALUE => $objectOrArray ,
self :: REF => & $objectOrArray ,
);
$propertyValues = $this -> readPropertiesUntil ( $zval , $propertyPath , $propertyPath -> getLength () - 1 );
2013-01-07 08:19:31 +00:00
$overwrite = true ;
2015-11-03 18:24:23 +00:00
2016-03-17 08:10:46 +00:00
try {
2016-03-22 08:55:46 +00:00
if ( PHP_VERSION_ID < 70000 && false === self :: $previousErrorHandler ) {
2016-03-17 14:00:21 +00:00
self :: $previousErrorHandler = set_error_handler ( self :: $errorHandler );
2016-03-17 08:10:46 +00:00
}
2013-01-07 08:19:31 +00:00
2016-03-17 14:00:21 +00:00
for ( $i = count ( $propertyValues ) - 1 ; 0 <= $i ; -- $i ) {
$zval = $propertyValues [ $i ];
unset ( $propertyValues [ $i ]);
2013-01-07 08:19:31 +00:00
2016-03-22 08:55:46 +00:00
// You only need set value for current element if:
// 1. it's the parent of the last index element
// OR
// 2. its child is not passed by reference
//
// This may avoid uncessary value setting process for array elements.
// For example:
// '[a][b][c]' => 'old-value'
// If you want to change its value to 'new-value',
// you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]'
//
2016-03-17 08:10:46 +00:00
if ( $overwrite ) {
$property = $propertyPath -> getElement ( $i );
if ( $propertyPath -> isIndex ( $i )) {
2016-03-17 14:00:21 +00:00
if ( $overwrite = ! isset ( $zval [ self :: REF ])) {
$ref = & $zval [ self :: REF ];
2016-04-05 17:42:48 +01:00
$ref = $zval [ self :: VALUE ];
2016-03-17 14:00:21 +00:00
}
$this -> writeIndex ( $zval , $property , $value );
if ( $overwrite ) {
$zval [ self :: VALUE ] = $zval [ self :: REF ];
}
2016-03-17 08:10:46 +00:00
} else {
2016-03-17 14:00:21 +00:00
$this -> writeProperty ( $zval , $property , $value );
2016-03-17 08:10:46 +00:00
}
2013-01-07 08:19:31 +00:00
2016-03-22 08:55:46 +00:00
// if current element is an object
// OR
// if current element's reference chain is not broken - current element
// as well as all its ancients in the property path are all passed by reference,
// then there is no need to continue the value setting process
if ( is_object ( $zval [ self :: VALUE ]) || isset ( $zval [ self :: IS_REF_CHAINED ])) {
2016-03-23 09:13:06 +00:00
break ;
2016-03-22 08:55:46 +00:00
}
[property-access] Improvement for Accessing Reference Chain
Improve performance for the following scenarios:
- Example 1:
```php
$a = array(
'a' => array(
'b' => array(
'c' => 'old-value'
)
)
);
$pa->setValue($a, '[a][b][c]', 'new-value');
// The PropertyAccessor will try to set values for
// $a['a']['b']['c'], $a['a']['b'] and $a['a'],
// but in fact it may terminate the loop
// right after the value of $a[a][b][c] is set,
// because $a, $[a], $[a][b] and $[a][b][c]
// are all passed as reference - the reference chain is not broken.
```
- Example 2
```php
$b = array(
'a' => array(
'b' => array(
'c' => 'old-value'
)
)
)
$a = new Foo($b);
// In this example, the reference chain of $b is broken,
// because it's passed to $a.value as value
// But its elements are all passed as reference,
// so after setting the value for $b[a][b][c], there is no need
// to set value for $b[a][b] and $b[a]
$pa->setValue($a, 'value[a][b][c]', 'new-value');
```
| Q | A
| ------------- | ---
| Bug fix? | no
| New feature? | no
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | n/a
| License | MIT
| Doc PR | n/a
2015-08-03 08:13:18 +01:00
}
2016-03-17 14:00:21 +00:00
$value = $zval [ self :: VALUE ];
2016-03-17 08:10:46 +00:00
}
} catch ( \TypeError $e ) {
2016-08-15 10:28:45 +01:00
self :: throwInvalidArgumentException ( $e -> getMessage (), $e -> getTrace (), 0 );
} finally {
if ( PHP_VERSION_ID < 70000 && false !== self :: $previousErrorHandler ) {
restore_error_handler ();
self :: $previousErrorHandler = false ;
2013-01-07 08:19:31 +00:00
}
2016-03-17 08:10:46 +00:00
}
}
/**
* @ internal
*/
public static function handleError ( $type , $message , $file , $line , $context )
{
if ( E_RECOVERABLE_ERROR === $type ) {
2016-03-22 08:55:46 +00:00
self :: throwInvalidArgumentException ( $message , debug_backtrace ( false ), 1 );
2016-03-17 08:10:46 +00:00
}
return null !== self :: $previousErrorHandler && false !== call_user_func ( self :: $previousErrorHandler , $type , $message , $file , $line , $context );
}
2016-03-22 08:55:46 +00:00
private static function throwInvalidArgumentException ( $message , $trace , $i )
2016-03-17 08:10:46 +00:00
{
if ( isset ( $trace [ $i ][ 'file' ]) && __FILE__ === $trace [ $i ][ 'file' ]) {
$pos = strpos ( $message , $delim = 'must be of the type ' ) ? : strpos ( $message , $delim = 'must be an instance of ' );
$pos += strlen ( $delim );
2016-03-22 08:55:46 +00:00
$type = $trace [ $i ][ 'args' ][ 0 ];
$type = is_object ( $type ) ? get_class ( $type ) : gettype ( $type );
2013-01-07 08:19:31 +00:00
2016-03-22 08:55:46 +00:00
throw new InvalidArgumentException ( sprintf ( 'Expected argument of type "%s", "%s" given' , substr ( $message , $pos , strpos ( $message , ',' , $pos ) - $pos ), $type ));
2013-01-07 08:19:31 +00:00
}
}
2014-03-28 16:21:37 +00:00
/**
* { @ inheritdoc }
*/
public function isReadable ( $objectOrArray , $propertyPath )
{
2015-02-21 12:49:08 +00:00
if ( ! $propertyPath instanceof PropertyPathInterface ) {
2014-03-28 16:21:37 +00:00
$propertyPath = new PropertyPath ( $propertyPath );
}
try {
2016-03-22 08:55:46 +00:00
$zval = array (
self :: VALUE => $objectOrArray ,
);
$this -> readPropertiesUntil ( $zval , $propertyPath , $propertyPath -> getLength (), $this -> ignoreInvalidIndices );
2014-03-28 16:21:37 +00:00
return true ;
2015-02-21 12:49:08 +00:00
} catch ( AccessException $e ) {
2014-03-28 16:21:37 +00:00
return false ;
} catch ( UnexpectedTypeException $e ) {
return false ;
}
}
/**
* { @ inheritdoc }
*/
2014-03-30 17:19:15 +01:00
public function isWritable ( $objectOrArray , $propertyPath )
2014-03-28 16:21:37 +00:00
{
2015-02-21 12:49:08 +00:00
if ( ! $propertyPath instanceof PropertyPathInterface ) {
2014-03-28 16:21:37 +00:00
$propertyPath = new PropertyPath ( $propertyPath );
}
try {
2016-03-22 08:55:46 +00:00
$zval = array (
2014-03-28 16:21:37 +00:00
self :: VALUE => $objectOrArray ,
2016-03-22 08:55:46 +00:00
);
$propertyValues = $this -> readPropertiesUntil ( $zval , $propertyPath , $propertyPath -> getLength () - 1 );
2014-03-28 16:21:37 +00:00
2016-03-22 08:55:46 +00:00
for ( $i = count ( $propertyValues ) - 1 ; 0 <= $i ; -- $i ) {
$zval = $propertyValues [ $i ];
unset ( $propertyValues [ $i ]);
2015-03-02 20:56:10 +00:00
if ( $propertyPath -> isIndex ( $i )) {
2016-03-22 08:55:46 +00:00
if ( ! $zval [ self :: VALUE ] instanceof \ArrayAccess && ! is_array ( $zval [ self :: VALUE ])) {
2015-03-02 20:56:10 +00:00
return false ;
}
} else {
2016-03-22 08:55:46 +00:00
if ( ! $this -> isPropertyWritable ( $zval [ self :: VALUE ], $propertyPath -> getElement ( $i ))) {
2015-03-02 20:56:10 +00:00
return false ;
2014-03-28 16:21:37 +00:00
}
}
2016-03-22 08:55:46 +00:00
if ( is_object ( $zval [ self :: VALUE ])) {
2015-03-02 20:56:10 +00:00
return true ;
}
2014-03-28 16:21:37 +00:00
}
return true ;
2015-02-21 12:49:08 +00:00
} catch ( AccessException $e ) {
2014-03-28 16:21:37 +00:00
return false ;
2015-02-21 12:49:08 +00:00
} catch ( UnexpectedTypeException $e ) {
2014-03-28 16:21:37 +00:00
return false ;
}
}
2013-01-07 08:19:31 +00:00
/**
* Reads the path from an object up to a given path index .
*
2016-03-22 08:55:46 +00:00
* @ param array $zval The array containing the object or array to read from
2014-03-28 18:21:04 +00:00
* @ param PropertyPathInterface $propertyPath The property path to read
2014-04-16 09:08:40 +01:00
* @ param int $lastIndex The index up to which should be read
2016-03-22 08:55:46 +00:00
* @ param bool $ignoreInvalidIndices Whether to ignore invalid indices or throw an exception
2013-01-07 08:19:31 +00:00
*
2016-06-28 06:50:50 +01:00
* @ return array The values read in the path
2013-01-07 08:19:31 +00:00
*
* @ throws UnexpectedTypeException If a value within the path is neither object nor array .
2014-12-04 20:26:11 +00:00
* @ throws NoSuchIndexException If a non - existing index is accessed
2013-01-07 08:19:31 +00:00
*/
2016-03-22 08:55:46 +00:00
private function readPropertiesUntil ( $zval , PropertyPathInterface $propertyPath , $lastIndex , $ignoreInvalidIndices = true )
2013-01-07 08:19:31 +00:00
{
2016-03-17 14:00:21 +00:00
if ( ! is_object ( $zval [ self :: VALUE ]) && ! is_array ( $zval [ self :: VALUE ])) {
2016-03-22 08:55:46 +00:00
throw new UnexpectedTypeException ( $zval [ self :: VALUE ], $propertyPath , 0 );
2015-02-18 20:52:22 +00:00
}
2016-03-17 14:00:21 +00:00
// Add the root object to the list
$propertyValues = array ( $zval );
2013-01-07 08:19:31 +00:00
2012-07-31 12:15:57 +01:00
for ( $i = 0 ; $i < $lastIndex ; ++ $i ) {
2013-01-07 08:19:31 +00:00
$property = $propertyPath -> getElement ( $i );
$isIndex = $propertyPath -> isIndex ( $i );
2016-03-17 14:00:21 +00:00
if ( $isIndex ) {
// Create missing nested arrays on demand
2016-03-22 08:55:46 +00:00
if (( $zval [ self :: VALUE ] instanceof \ArrayAccess && ! $zval [ self :: VALUE ] -> offsetExists ( $property )) ||
2016-03-17 14:00:21 +00:00
( is_array ( $zval [ self :: VALUE ]) && ! isset ( $zval [ self :: VALUE ][ $property ]) && ! array_key_exists ( $property , $zval [ self :: VALUE ]))
2016-03-22 08:55:46 +00:00
) {
if ( ! $ignoreInvalidIndices ) {
if ( ! is_array ( $zval [ self :: VALUE ])) {
if ( ! $zval [ self :: VALUE ] instanceof \Traversable ) {
throw new NoSuchIndexException ( sprintf (
'Cannot read index "%s" while trying to traverse path "%s".' ,
$property ,
( string ) $propertyPath
));
}
$zval [ self :: VALUE ] = iterator_to_array ( $zval [ self :: VALUE ]);
2014-05-19 16:05:32 +01:00
}
2016-03-22 08:55:46 +00:00
throw new NoSuchIndexException ( sprintf (
'Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".' ,
$property ,
( string ) $propertyPath ,
print_r ( array_keys ( $zval [ self :: VALUE ]), true )
));
2014-05-19 16:05:32 +01:00
}
2016-03-22 08:55:46 +00:00
if ( $i + 1 < $propertyPath -> getLength ()) {
if ( isset ( $zval [ self :: REF ])) {
2016-04-20 16:20:31 +01:00
$zval [ self :: VALUE ][ $property ] = array ();
2016-03-22 08:55:46 +00:00
$zval [ self :: REF ] = $zval [ self :: VALUE ];
2016-04-20 16:20:31 +01:00
} else {
$zval [ self :: VALUE ] = array ( $property => array ());
2016-03-22 08:55:46 +00:00
}
2016-03-17 14:00:21 +00:00
}
2015-10-02 22:43:46 +01:00
}
2013-01-07 08:19:31 +00:00
2016-03-17 14:00:21 +00:00
$zval = $this -> readIndex ( $zval , $property );
2012-07-31 12:15:57 +01:00
} else {
2016-03-17 14:00:21 +00:00
$zval = $this -> readProperty ( $zval , $property );
2012-07-31 12:15:57 +01:00
}
2015-02-18 20:52:22 +00:00
// the final value of the path must not be validated
2016-03-17 14:00:21 +00:00
if ( $i + 1 < $propertyPath -> getLength () && ! is_object ( $zval [ self :: VALUE ]) && ! is_array ( $zval [ self :: VALUE ])) {
2016-03-22 08:55:46 +00:00
throw new UnexpectedTypeException ( $zval [ self :: VALUE ], $propertyPath , $i + 1 );
2015-02-18 20:52:22 +00:00
}
2016-03-22 08:55:46 +00:00
if ( isset ( $zval [ self :: REF ]) && ( 0 === $i || isset ( $propertyValues [ $i - 1 ][ self :: IS_REF_CHAINED ]))) {
// Set the IS_REF_CHAINED flag to true if:
// current property is passed by reference and
// it is the first element in the property path or
// the IS_REF_CHAINED flag of its parent element is true
// Basically, this flag is true only when the reference chain from the top element to current element is not broken
$zval [ self :: IS_REF_CHAINED ] = true ;
2015-02-18 20:52:22 +00:00
}
[property-access] Improvement for Accessing Reference Chain
Improve performance for the following scenarios:
- Example 1:
```php
$a = array(
'a' => array(
'b' => array(
'c' => 'old-value'
)
)
);
$pa->setValue($a, '[a][b][c]', 'new-value');
// The PropertyAccessor will try to set values for
// $a['a']['b']['c'], $a['a']['b'] and $a['a'],
// but in fact it may terminate the loop
// right after the value of $a[a][b][c] is set,
// because $a, $[a], $[a][b] and $[a][b][c]
// are all passed as reference - the reference chain is not broken.
```
- Example 2
```php
$b = array(
'a' => array(
'b' => array(
'c' => 'old-value'
)
)
)
$a = new Foo($b);
// In this example, the reference chain of $b is broken,
// because it's passed to $a.value as value
// But its elements are all passed as reference,
// so after setting the value for $b[a][b][c], there is no need
// to set value for $b[a][b] and $b[a]
$pa->setValue($a, 'value[a][b][c]', 'new-value');
```
| Q | A
| ------------- | ---
| Bug fix? | no
| New feature? | no
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | n/a
| License | MIT
| Doc PR | n/a
2015-08-03 08:13:18 +01:00
2016-03-17 14:00:21 +00:00
$propertyValues [] = $zval ;
2013-01-07 08:19:31 +00:00
}
return $propertyValues ;
}
2012-07-31 12:15:57 +01:00
/**
* Reads a key from an array - like structure .
*
2016-03-17 14:00:21 +00:00
* @ param array $zval The array containing the array or \ArrayAccess object to read from
* @ param string | int $index The key to read
2012-07-31 12:15:57 +01:00
*
2016-03-17 14:00:21 +00:00
* @ return array The array containing the value of the key
2012-07-31 12:15:57 +01:00
*
2014-03-28 16:07:20 +00:00
* @ throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
2012-07-31 12:15:57 +01:00
*/
2016-03-17 14:00:21 +00:00
private function readIndex ( $zval , $index )
2012-07-31 12:15:57 +01:00
{
2016-03-17 14:00:21 +00:00
if ( ! $zval [ self :: VALUE ] instanceof \ArrayAccess && ! is_array ( $zval [ self :: VALUE ])) {
2016-03-22 08:55:46 +00:00
throw new NoSuchIndexException ( sprintf ( 'Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.' , $index , get_class ( $zval [ self :: VALUE ])));
2012-07-31 12:15:57 +01:00
}
2016-03-17 14:00:21 +00:00
$result = self :: $resultProto ;
2012-07-31 12:15:57 +01:00
2016-03-17 14:00:21 +00:00
if ( isset ( $zval [ self :: VALUE ][ $index ])) {
$result [ self :: VALUE ] = $zval [ self :: VALUE ][ $index ];
if ( ! isset ( $zval [ self :: REF ])) {
// Save creating references when doing read-only lookups
} elseif ( is_array ( $zval [ self :: VALUE ])) {
$result [ self :: REF ] = & $zval [ self :: REF ][ $index ];
} elseif ( is_object ( $result [ self :: VALUE ])) {
$result [ self :: REF ] = $result [ self :: VALUE ];
2012-07-31 12:15:57 +01:00
}
}
return $result ;
}
2013-01-07 08:19:31 +00:00
/**
2016-03-17 14:00:21 +00:00
* Reads the a property from an object .
2013-01-07 08:19:31 +00:00
*
2016-03-17 14:00:21 +00:00
* @ param array $zval The array containing the object to read from
2016-06-28 06:50:50 +01:00
* @ param string $property The property to read
2013-01-07 08:19:31 +00:00
*
2016-03-17 14:00:21 +00:00
* @ return array The array containing the value of the property
2013-01-07 08:19:31 +00:00
*
2016-03-17 14:00:21 +00:00
* @ throws NoSuchPropertyException If the property does not exist or is not public .
2013-01-07 08:19:31 +00:00
*/
2016-03-17 14:00:21 +00:00
private function readProperty ( $zval , $property )
2013-01-07 08:19:31 +00:00
{
2016-03-17 14:00:21 +00:00
if ( ! is_object ( $zval [ self :: VALUE ])) {
2015-02-21 15:36:02 +00:00
throw new NoSuchPropertyException ( sprintf ( 'Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%s]" instead.' , $property , $property ));
2012-07-31 12:15:57 +01:00
}
2013-01-07 08:19:31 +00:00
2016-03-17 14:00:21 +00:00
$result = self :: $resultProto ;
$object = $zval [ self :: VALUE ];
$access = $this -> getReadAccessInfo ( get_class ( $object ), $property );
2015-11-03 18:24:23 +00:00
if ( self :: ACCESS_TYPE_METHOD === $access [ self :: ACCESS_TYPE ]) {
$result [ self :: VALUE ] = $object -> { $access [ self :: ACCESS_NAME ]}();
} elseif ( self :: ACCESS_TYPE_PROPERTY === $access [ self :: ACCESS_TYPE ]) {
2016-03-17 14:00:21 +00:00
$result [ self :: VALUE ] = $object -> { $access [ self :: ACCESS_NAME ]};
if ( $access [ self :: ACCESS_REF ] && isset ( $zval [ self :: REF ])) {
$result [ self :: REF ] = & $object -> { $access [ self :: ACCESS_NAME ]};
2015-11-03 18:24:23 +00:00
}
} elseif ( ! $access [ self :: ACCESS_HAS_PROPERTY ] && property_exists ( $object , $property )) {
2013-04-18 15:39:54 +01:00
// Needed to support \stdClass instances. We need to explicitly
2015-11-26 21:41:57 +00:00
// exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
2013-04-18 15:39:54 +01:00
// a *protected* property was found on the class, property_exists()
// returns true, consequently the following line will result in a
// fatal error.
2015-11-03 18:24:23 +00:00
2016-03-17 14:00:21 +00:00
$result [ self :: VALUE ] = $object -> $property ;
if ( isset ( $zval [ self :: REF ])) {
$result [ self :: REF ] = & $object -> $property ;
}
2015-11-03 18:24:23 +00:00
} elseif ( self :: ACCESS_TYPE_MAGIC === $access [ self :: ACCESS_TYPE ]) {
2013-03-04 22:59:45 +00:00
// we call the getter and hope the __call do the job
2015-11-03 18:24:23 +00:00
$result [ self :: VALUE ] = $object -> { $access [ self :: ACCESS_NAME ]}();
2013-01-07 08:19:31 +00:00
} else {
2015-11-03 18:24:23 +00:00
throw new NoSuchPropertyException ( $access [ self :: ACCESS_NAME ]);
2013-01-07 08:19:31 +00:00
}
// Objects are always passed around by reference
2016-03-17 14:00:21 +00:00
if ( isset ( $zval [ self :: REF ]) && is_object ( $result [ self :: VALUE ])) {
$result [ self :: REF ] = $result [ self :: VALUE ];
2013-01-07 08:19:31 +00:00
}
return $result ;
}
2015-11-03 18:24:23 +00:00
/**
* Guesses how to read the property value .
*
2016-03-17 14:00:21 +00:00
* @ param string $class
2015-11-03 18:24:23 +00:00
* @ param string $property
*
* @ return array
*/
2016-03-17 14:00:21 +00:00
private function getReadAccessInfo ( $class , $property )
2015-11-03 18:24:23 +00:00
{
2016-03-17 14:00:21 +00:00
$key = $class . '::' . $property ;
2015-11-03 18:24:23 +00:00
if ( isset ( $this -> readPropertyCache [ $key ])) {
$access = $this -> readPropertyCache [ $key ];
} else {
$access = array ();
2016-03-17 14:00:21 +00:00
$reflClass = new \ReflectionClass ( $class );
2015-11-03 18:24:23 +00:00
$access [ self :: ACCESS_HAS_PROPERTY ] = $reflClass -> hasProperty ( $property );
$camelProp = $this -> camelize ( $property );
$getter = 'get' . $camelProp ;
$getsetter = lcfirst ( $camelProp ); // jQuery style, e.g. read: last(), write: last($item)
$isser = 'is' . $camelProp ;
$hasser = 'has' . $camelProp ;
if ( $reflClass -> hasMethod ( $getter ) && $reflClass -> getMethod ( $getter ) -> isPublic ()) {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_METHOD ;
$access [ self :: ACCESS_NAME ] = $getter ;
} elseif ( $reflClass -> hasMethod ( $getsetter ) && $reflClass -> getMethod ( $getsetter ) -> isPublic ()) {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_METHOD ;
$access [ self :: ACCESS_NAME ] = $getsetter ;
} elseif ( $reflClass -> hasMethod ( $isser ) && $reflClass -> getMethod ( $isser ) -> isPublic ()) {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_METHOD ;
$access [ self :: ACCESS_NAME ] = $isser ;
} elseif ( $reflClass -> hasMethod ( $hasser ) && $reflClass -> getMethod ( $hasser ) -> isPublic ()) {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_METHOD ;
$access [ self :: ACCESS_NAME ] = $hasser ;
} elseif ( $reflClass -> hasMethod ( '__get' ) && $reflClass -> getMethod ( '__get' ) -> isPublic ()) {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_PROPERTY ;
$access [ self :: ACCESS_NAME ] = $property ;
$access [ self :: ACCESS_REF ] = false ;
2015-11-26 21:41:57 +00:00
} elseif ( $access [ self :: ACCESS_HAS_PROPERTY ] && $reflClass -> getProperty ( $property ) -> isPublic ()) {
2015-11-03 18:24:23 +00:00
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_PROPERTY ;
$access [ self :: ACCESS_NAME ] = $property ;
$access [ self :: ACCESS_REF ] = true ;
} elseif ( $this -> magicCall && $reflClass -> hasMethod ( '__call' ) && $reflClass -> getMethod ( '__call' ) -> isPublic ()) {
// we call the getter and hope the __call do the job
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_MAGIC ;
$access [ self :: ACCESS_NAME ] = $getter ;
} else {
$methods = array ( $getter , $getsetter , $isser , $hasser , '__get' );
if ( $this -> magicCall ) {
$methods [] = '__call' ;
}
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_NOT_FOUND ;
$access [ self :: ACCESS_NAME ] = sprintf (
'Neither the property "%s" nor one of the methods "%s()" ' .
'exist and have public access in class "%s".' ,
$property ,
implode ( '()", "' , $methods ),
$reflClass -> name
);
}
$this -> readPropertyCache [ $key ] = $access ;
}
return $access ;
}
2013-01-07 08:19:31 +00:00
/**
2014-03-30 17:19:15 +01:00
* Sets the value of an index in a given array - accessible value .
2013-01-07 08:19:31 +00:00
*
2016-03-17 14:00:21 +00:00
* @ param array $zval The array containing the array or \ArrayAccess object to write to
* @ param string | int $index The index to write at
* @ param mixed $value The value to write
2012-07-31 12:15:57 +01:00
*
2014-03-28 16:07:20 +00:00
* @ throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
2012-07-31 12:15:57 +01:00
*/
2016-03-17 14:00:21 +00:00
private function writeIndex ( $zval , $index , $value )
2012-07-31 12:15:57 +01:00
{
2016-03-17 14:00:21 +00:00
if ( ! $zval [ self :: VALUE ] instanceof \ArrayAccess && ! is_array ( $zval [ self :: VALUE ])) {
2016-03-22 08:55:46 +00:00
throw new NoSuchIndexException ( sprintf ( 'Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess' , $index , get_class ( $zval [ self :: VALUE ])));
2012-07-31 12:15:57 +01:00
}
2016-03-17 14:00:21 +00:00
$zval [ self :: REF ][ $index ] = $value ;
2012-07-31 12:15:57 +01:00
}
/**
2014-12-22 16:29:52 +00:00
* Sets the value of a property in the given object .
2012-07-31 12:15:57 +01:00
*
2016-03-17 14:00:21 +00:00
* @ param array $zval The array containing the object to write to
2014-03-30 17:19:15 +01:00
* @ param string $property The property to write
* @ param mixed $value The value to write
2013-01-07 08:19:31 +00:00
*
2016-03-17 08:10:46 +00:00
* @ throws NoSuchPropertyException If the property does not exist or is not public .
2013-01-07 08:19:31 +00:00
*/
2016-03-17 14:00:21 +00:00
private function writeProperty ( $zval , $property , $value )
2013-01-07 08:19:31 +00:00
{
2016-03-17 14:00:21 +00:00
if ( ! is_object ( $zval [ self :: VALUE ])) {
2012-07-31 12:15:57 +01:00
throw new NoSuchPropertyException ( sprintf ( 'Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?' , $property , $property ));
}
2013-01-07 08:19:31 +00:00
2016-03-17 14:00:21 +00:00
$object = $zval [ self :: VALUE ];
$access = $this -> getWriteAccessInfo ( get_class ( $object ), $property , $value );
2012-07-31 12:15:57 +01:00
2015-11-03 18:24:23 +00:00
if ( self :: ACCESS_TYPE_METHOD === $access [ self :: ACCESS_TYPE ]) {
$object -> { $access [ self :: ACCESS_NAME ]}( $value );
} elseif ( self :: ACCESS_TYPE_PROPERTY === $access [ self :: ACCESS_TYPE ]) {
$object -> { $access [ self :: ACCESS_NAME ]} = $value ;
} elseif ( self :: ACCESS_TYPE_ADDER_AND_REMOVER === $access [ self :: ACCESS_TYPE ]) {
2016-03-22 08:55:46 +00:00
$this -> writeCollection ( $zval , $property , $value , $access [ self :: ACCESS_ADDER ], $access [ self :: ACCESS_REMOVER ]);
2015-11-03 18:24:23 +00:00
} elseif ( ! $access [ self :: ACCESS_HAS_PROPERTY ] && property_exists ( $object , $property )) {
2013-04-18 15:39:54 +01:00
// Needed to support \stdClass instances. We need to explicitly
2015-11-26 21:41:57 +00:00
// exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
2013-04-18 15:39:54 +01:00
// a *protected* property was found on the class, property_exists()
// returns true, consequently the following line will result in a
// fatal error.
2015-11-03 18:24:23 +00:00
2012-07-31 12:15:57 +01:00
$object -> $property = $value ;
2015-11-03 18:24:23 +00:00
} elseif ( self :: ACCESS_TYPE_MAGIC === $access [ self :: ACCESS_TYPE ]) {
$object -> { $access [ self :: ACCESS_NAME ]}( $value );
2013-01-07 08:19:31 +00:00
} else {
2015-11-03 18:24:23 +00:00
throw new NoSuchPropertyException ( $access [ self :: ACCESS_NAME ]);
2013-01-07 08:19:31 +00:00
}
}
2014-03-30 17:19:15 +01:00
/**
2016-03-22 08:55:46 +00:00
* Adjusts a collection - valued property by calling add * () and remove * () methods .
2014-03-30 17:19:15 +01:00
*
2016-03-22 08:55:46 +00:00
* @ param array $zval The array containing the object to write to
2014-03-30 17:19:15 +01:00
* @ 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
*/
2016-03-22 08:55:46 +00:00
private function writeCollection ( $zval , $property , $collection , $addMethod , $removeMethod )
2014-03-28 16:21:37 +00:00
{
2014-03-30 17:19:15 +01:00
// At this point the add and remove methods have been found
2016-03-22 08:55:46 +00:00
$previousValue = $this -> readProperty ( $zval , $property );
$previousValue = $previousValue [ self :: VALUE ];
2014-03-30 17:19:15 +01:00
2016-03-22 08:55:46 +00:00
if ( $previousValue instanceof \Traversable ) {
$previousValue = iterator_to_array ( $previousValue );
2014-03-28 16:21:37 +00:00
}
2016-03-22 08:55:46 +00:00
if ( $previousValue && is_array ( $previousValue )) {
if ( is_object ( $collection )) {
$collection = iterator_to_array ( $collection );
}
foreach ( $previousValue as $key => $item ) {
if ( ! in_array ( $item , $collection , true )) {
unset ( $previousValue [ $key ]);
$zval [ self :: VALUE ] -> { $removeMethod }( $item );
}
}
} else {
$previousValue = false ;
2014-03-30 17:19:15 +01:00
}
2014-03-28 16:21:37 +00:00
2016-03-22 08:55:46 +00:00
foreach ( $collection as $item ) {
if ( ! $previousValue || ! in_array ( $item , $previousValue , true )) {
$zval [ self :: VALUE ] -> { $addMethod }( $item );
}
2014-03-30 17:19:15 +01:00
}
}
2014-03-28 16:21:37 +00:00
2015-11-03 18:24:23 +00:00
/**
* Guesses how to write the property value .
*
2016-03-17 14:00:21 +00:00
* @ param string $class
2015-11-03 18:24:23 +00:00
* @ param string $property
* @ param mixed $value
*
* @ return array
*/
2016-03-17 14:00:21 +00:00
private function getWriteAccessInfo ( $class , $property , $value )
2015-11-03 18:24:23 +00:00
{
2016-03-17 14:00:21 +00:00
$key = $class . '::' . $property ;
2015-11-03 18:24:23 +00:00
if ( isset ( $this -> writePropertyCache [ $key ])) {
$access = $this -> writePropertyCache [ $key ];
} else {
$access = array ();
2016-03-17 14:00:21 +00:00
$reflClass = new \ReflectionClass ( $class );
2015-11-03 18:24:23 +00:00
$access [ self :: ACCESS_HAS_PROPERTY ] = $reflClass -> hasProperty ( $property );
$camelized = $this -> camelize ( $property );
2016-03-22 09:02:09 +00:00
$singulars = ( array ) Inflector :: singularize ( $camelized );
2015-11-03 18:24:23 +00:00
if ( is_array ( $value ) || $value instanceof \Traversable ) {
$methods = $this -> findAdderAndRemover ( $reflClass , $singulars );
2015-11-26 21:41:57 +00:00
if ( null !== $methods ) {
2015-11-03 18:24:23 +00:00
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_ADDER_AND_REMOVER ;
$access [ self :: ACCESS_ADDER ] = $methods [ 0 ];
$access [ self :: ACCESS_REMOVER ] = $methods [ 1 ];
}
}
if ( ! isset ( $access [ self :: ACCESS_TYPE ])) {
2015-11-26 21:41:57 +00:00
$setter = 'set' . $camelized ;
2015-11-03 18:24:23 +00:00
$getsetter = lcfirst ( $camelized ); // jQuery style, e.g. read: last(), write: last($item)
if ( $this -> isMethodAccessible ( $reflClass , $setter , 1 )) {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_METHOD ;
$access [ self :: ACCESS_NAME ] = $setter ;
} elseif ( $this -> isMethodAccessible ( $reflClass , $getsetter , 1 )) {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_METHOD ;
$access [ self :: ACCESS_NAME ] = $getsetter ;
} elseif ( $this -> isMethodAccessible ( $reflClass , '__set' , 2 )) {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_PROPERTY ;
$access [ self :: ACCESS_NAME ] = $property ;
2015-11-26 21:41:57 +00:00
} elseif ( $access [ self :: ACCESS_HAS_PROPERTY ] && $reflClass -> getProperty ( $property ) -> isPublic ()) {
2015-11-03 18:24:23 +00:00
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_PROPERTY ;
$access [ self :: ACCESS_NAME ] = $property ;
} elseif ( $this -> magicCall && $this -> isMethodAccessible ( $reflClass , '__call' , 2 )) {
// we call the getter and hope the __call do the job
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_MAGIC ;
$access [ self :: ACCESS_NAME ] = $setter ;
2016-05-09 17:57:57 +01:00
} elseif ( null !== $methods = $this -> findAdderAndRemover ( $reflClass , $singulars )) {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_NOT_FOUND ;
$access [ self :: ACCESS_NAME ] = sprintf (
'The property "%s" in class "%s" can be defined with the methods "%s()" but ' .
'the new value must be an array or an instance of \Traversable, ' .
'"%s" given.' ,
$property ,
$reflClass -> name ,
implode ( '()", "' , $methods ),
is_object ( $value ) ? get_class ( $value ) : gettype ( $value )
);
2015-11-03 18:24:23 +00:00
} else {
$access [ self :: ACCESS_TYPE ] = self :: ACCESS_TYPE_NOT_FOUND ;
$access [ self :: ACCESS_NAME ] = sprintf (
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", ' .
'"__set()" or "__call()" exist and have public access in class "%s".' ,
$property ,
implode ( '' , array_map ( function ( $singular ) {
return '"add' . $singular . '()"/"remove' . $singular . '()", ' ;
}, $singulars )),
$setter ,
$getsetter ,
$reflClass -> name
);
}
}
$this -> writePropertyCache [ $key ] = $access ;
}
return $access ;
}
2014-03-30 17:19:15 +01:00
/**
* 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
*
2014-12-04 20:26:11 +00:00
* @ return bool Whether the property is writable
2014-03-30 17:19:15 +01:00
*/
private function isPropertyWritable ( $object , $property )
{
if ( ! is_object ( $object )) {
return false ;
2014-03-28 16:21:37 +00:00
}
2016-03-22 09:40:06 +00:00
$access = $this -> getWriteAccessInfo ( get_class ( $object ), $property , array ());
2014-03-30 17:19:15 +01:00
2016-03-22 09:40:06 +00:00
return self :: ACCESS_TYPE_METHOD === $access [ self :: ACCESS_TYPE ]
|| self :: ACCESS_TYPE_PROPERTY === $access [ self :: ACCESS_TYPE ]
|| self :: ACCESS_TYPE_ADDER_AND_REMOVER === $access [ self :: ACCESS_TYPE ]
|| ( ! $access [ self :: ACCESS_HAS_PROPERTY ] && property_exists ( $object , $property ))
|| self :: ACCESS_TYPE_MAGIC === $access [ self :: ACCESS_TYPE ];
2014-03-28 16:21:37 +00:00
}
2013-01-07 08:19:31 +00:00
/**
* Camelizes a given string .
*
2014-11-30 13:33:44 +00:00
* @ param string $string Some string
2013-01-07 08:19:31 +00:00
*
2013-03-27 22:40:36 +00:00
* @ return string The camelized version of the string
2013-01-07 08:19:31 +00:00
*/
private function camelize ( $string )
{
2016-03-22 09:40:06 +00:00
return str_replace ( ' ' , '' , ucwords ( str_replace ( '_' , ' ' , $string )));
2013-01-07 08:19:31 +00:00
}
/**
* Searches for add and remove methods .
*
* @ param \ReflectionClass $reflClass The reflection class for the given object
2013-03-27 22:40:36 +00:00
* @ param array $singulars The singular form of the property name or null
2013-01-07 08:19:31 +00:00
*
2013-03-27 22:40:36 +00:00
* @ return array | null An array containing the adder and remover when found , null otherwise
2013-01-07 08:19:31 +00:00
*/
private function findAdderAndRemover ( \ReflectionClass $reflClass , array $singulars )
{
foreach ( $singulars as $singular ) {
2013-04-02 10:39:57 +01:00
$addMethod = 'add' . $singular ;
$removeMethod = 'remove' . $singular ;
2013-01-07 08:19:31 +00:00
2014-03-28 14:50:34 +00:00
$addMethodFound = $this -> isMethodAccessible ( $reflClass , $addMethod , 1 );
$removeMethodFound = $this -> isMethodAccessible ( $reflClass , $removeMethod , 1 );
2013-01-07 08:19:31 +00:00
if ( $addMethodFound && $removeMethodFound ) {
return array ( $addMethod , $removeMethod );
}
2014-03-28 16:21:37 +00:00
}
2013-01-07 08:19:31 +00:00
}
/**
2014-04-01 10:04:09 +01:00
* Returns whether a method is public and has the number of required parameters .
2013-01-07 08:19:31 +00:00
*
2014-11-30 13:33:44 +00:00
* @ param \ReflectionClass $class The class of the method
* @ param string $methodName The method name
* @ param int $parameters The number of parameters
2013-01-07 08:19:31 +00:00
*
2016-03-17 14:00:21 +00:00
* @ return bool Whether the method is public and has $parameters required parameters
2013-01-07 08:19:31 +00:00
*/
2014-03-28 14:50:34 +00:00
private function isMethodAccessible ( \ReflectionClass $class , $methodName , $parameters )
2013-01-07 08:19:31 +00:00
{
if ( $class -> hasMethod ( $methodName )) {
$method = $class -> getMethod ( $methodName );
2014-04-01 10:04:09 +01:00
if ( $method -> isPublic ()
&& $method -> getNumberOfRequiredParameters () <= $parameters
&& $method -> getNumberOfParameters () >= $parameters ) {
2013-01-07 08:19:31 +00:00
return true ;
}
}
return false ;
}
}