[PropertyAccess] Backport fixes from 2.7

This commit is contained in:
Nicolas Grekas 2016-03-22 10:19:04 +01:00
parent d01a10651b
commit cb1c87ac5f
3 changed files with 131 additions and 45 deletions

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; namespace Symfony\Component\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
@ -32,6 +33,11 @@ class PropertyAccessor implements PropertyAccessorInterface
*/ */
const REF = 1; const REF = 1;
/**
* @internal
*/
const IS_REF_CHAINED = 2;
/** /**
* @internal * @internal
*/ */
@ -87,16 +93,29 @@ class PropertyAccessor implements PropertyAccessorInterface
*/ */
const ACCESS_TYPE_NOT_FOUND = 4; const ACCESS_TYPE_NOT_FOUND = 4;
/**
* @var bool
*/
private $magicCall; private $magicCall;
/**
* @var array
*/
private $readPropertyCache = array(); private $readPropertyCache = array();
/**
* @var array
*/
private $writePropertyCache = array(); private $writePropertyCache = array();
private static $previousErrorHandler; private static $previousErrorHandler = false;
private static $errorHandler = array(__CLASS__, 'handleError'); private static $errorHandler = array(__CLASS__, 'handleError');
private static $resultProto = array(self::VALUE => null); private static $resultProto = array(self::VALUE => null);
/** /**
* Should not be used by application code. Use * Should not be used by application code. Use
* {@link PropertyAccess::createPropertyAccessor()} instead. * {@link PropertyAccess::createPropertyAccessor()} instead.
*
* @param bool $magicCall
*/ */
public function __construct($magicCall = false) public function __construct($magicCall = false)
{ {
@ -137,7 +156,7 @@ class PropertyAccessor implements PropertyAccessorInterface
$overwrite = true; $overwrite = true;
try { try {
if (PHP_VERSION_ID < 70000) { if (PHP_VERSION_ID < 70000 && false === self::$previousErrorHandler) {
self::$previousErrorHandler = set_error_handler(self::$errorHandler); self::$previousErrorHandler = set_error_handler(self::$errorHandler);
} }
@ -145,6 +164,17 @@ class PropertyAccessor implements PropertyAccessorInterface
$zval = $propertyValues[$i]; $zval = $propertyValues[$i];
unset($propertyValues[$i]); unset($propertyValues[$i]);
// 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]'
//
if ($overwrite) { if ($overwrite) {
$property = $propertyPath->getElement($i); $property = $propertyPath->getElement($i);
@ -159,22 +189,31 @@ class PropertyAccessor implements PropertyAccessorInterface
} else { } else {
$this->writeProperty($zval, $property, $value); $this->writeProperty($zval, $property, $value);
} }
// 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])) {
return;
}
} }
$value = $zval[self::VALUE]; $value = $zval[self::VALUE];
} }
} catch (\TypeError $e) { } catch (\TypeError $e) {
try { try {
self::throwUnexpectedTypeException($e->getMessage(), $e->getTrace(), 0); self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0);
} catch (UnexpectedTypeException $e) { } catch (InvalidArgumentException $e) {
} }
} catch (\Exception $e) { } catch (\Exception $e) {
} catch (\Throwable $e) { } catch (\Throwable $e) {
} }
if (PHP_VERSION_ID < 70000) { if (PHP_VERSION_ID < 70000 && false !== self::$previousErrorHandler) {
restore_error_handler(); restore_error_handler();
self::$previousErrorHandler = null; self::$previousErrorHandler = false;
} }
if (isset($e)) { if (isset($e)) {
throw $e; throw $e;
@ -187,19 +226,21 @@ class PropertyAccessor implements PropertyAccessorInterface
public static function handleError($type, $message, $file, $line, $context) public static function handleError($type, $message, $file, $line, $context)
{ {
if (E_RECOVERABLE_ERROR === $type) { if (E_RECOVERABLE_ERROR === $type) {
self::throwUnexpectedTypeException($message, debug_backtrace(false), 1); self::throwInvalidArgumentException($message, debug_backtrace(false), 1);
} }
return null !== self::$previousErrorHandler && false !== call_user_func(self::$previousErrorHandler, $type, $message, $file, $line, $context); return null !== self::$previousErrorHandler && false !== call_user_func(self::$previousErrorHandler, $type, $message, $file, $line, $context);
} }
private static function throwUnexpectedTypeException($message, $trace, $i) private static function throwInvalidArgumentException($message, $trace, $i)
{ {
if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file']) { 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 = strpos($message, $delim = 'must be of the type ') ?: strpos($message, $delim = 'must be an instance of ');
$pos += strlen($delim); $pos += strlen($delim);
$type = $trace[$i]['args'][0];
$type = is_object($type) ? get_class($type) : gettype($type);
throw new UnexpectedTypeException($trace[$i]['args'][0], substr($message, $pos, strpos($message, ',', $pos) - $pos)); throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type));
} }
} }
@ -229,14 +270,15 @@ class PropertyAccessor implements PropertyAccessorInterface
if ($isIndex) { if ($isIndex) {
// Create missing nested arrays on demand // Create missing nested arrays on demand
if ($i + 1 < $propertyPath->getLength() && ( if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
(is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !array_key_exists($property, $zval[self::VALUE])) (is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !array_key_exists($property, $zval[self::VALUE]))
)) { ) {
$zval[self::VALUE][$property] = array(); if ($i + 1 < $propertyPath->getLength()) {
$zval[self::VALUE][$property] = array();
if (isset($zval[self::REF])) { if (isset($zval[self::REF])) {
$zval[self::REF] = $zval[self::VALUE]; $zval[self::REF] = $zval[self::VALUE];
}
} }
} }
@ -250,6 +292,15 @@ class PropertyAccessor implements PropertyAccessorInterface
throw new UnexpectedTypeException($zval[self::VALUE], 'object or array'); throw new UnexpectedTypeException($zval[self::VALUE], 'object or array');
} }
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;
}
$propertyValues[] = $zval; $propertyValues[] = $zval;
} }
@ -412,7 +463,7 @@ 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 array $zval The array containing the array or \ArrayAccess object to write to * @param array $zval The array containing the array or \ArrayAccess object to write to
* @param string|int $index The index to write at * @param string|int $index The index to write at
@ -430,7 +481,7 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
/** /**
* Sets the value of the property at the given index in the path. * Sets the value of a property in the given object.
* *
* @param array $zval The array containing the object to write to * @param array $zval The array containing the object to write to
* @param string $property The property to write * @param string $property The property to write
@ -452,32 +503,7 @@ class PropertyAccessor implements PropertyAccessorInterface
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) { } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]} = $value; $object->{$access[self::ACCESS_NAME]} = $value;
} elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) { } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
// At this point the add and remove methods have been found $this->writeCollection($zval, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
$previousValue = $this->readProperty($zval, $property);
$previousValue = $previousValue[self::VALUE];
if ($previousValue instanceof \Traversable) {
$previousValue = iterator_to_array($previousValue);
}
if ($previousValue && is_array($previousValue)) {
if (is_object($value)) {
$value = iterator_to_array($value);
}
foreach ($previousValue as $key => $item) {
if (!in_array($item, $value, true)) {
unset($previousValue[$key]);
$object->{$access[self::ACCESS_REMOVER]}($item);
}
}
} else {
$previousValue = false;
}
foreach ($value as $item) {
if (!$previousValue || !in_array($item, $previousValue, true)) {
$object->{$access[self::ACCESS_ADDER]}($item);
}
}
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly // Needed to support \stdClass instances. We need to explicitly
// exclude $classHasProperty, otherwise if in the previous clause // exclude $classHasProperty, otherwise if in the previous clause
@ -493,6 +519,45 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
} }
/**
* Adjusts a collection-valued property by calling add*() and remove*() methods.
*
* @param array $zval The array containing 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($zval, $property, $collection, $addMethod, $removeMethod)
{
// At this point the add and remove methods have been found
$previousValue = $this->readProperty($zval, $property);
$previousValue = $previousValue[self::VALUE];
if ($previousValue instanceof \Traversable) {
$previousValue = iterator_to_array($previousValue);
}
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;
}
foreach ($collection as $item) {
if (!$previousValue || !in_array($item, $previousValue, true)) {
$zval[self::VALUE]->{$addMethod}($item);
}
}
}
/** /**
* Guesses how to write the property value. * Guesses how to write the property value.
* *
@ -618,7 +683,7 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
/** /**
* Returns whether a method is public and has a specific number of required parameters. * Returns whether a method is public and has the number of required parameters.
* *
* @param \ReflectionClass $class The class of the method * @param \ReflectionClass $class The class of the method
* @param string $methodName The method name * @param string $methodName The method name

View File

@ -406,7 +406,7 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
} }
/** /**
* @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
* @expectedExceptionMessage Expected argument of type "DateTime", "string" given * @expectedExceptionMessage Expected argument of type "DateTime", "string" given
*/ */
public function testThrowTypeError() public function testThrowTypeError()