[PropertyAccess] Remove most ref mismatches to improve perf

This commit is contained in:
Nicolas Grekas 2016-03-17 15:00:21 +01:00
parent 06146f3f2b
commit 72940d7588
2 changed files with 135 additions and 150 deletions

View File

@ -18,6 +18,7 @@ use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
* Default implementation of {@link PropertyAccessorInterface}. * Default implementation of {@link PropertyAccessorInterface}.
* *
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/ */
class PropertyAccessor implements PropertyAccessorInterface class PropertyAccessor implements PropertyAccessorInterface
{ {
@ -29,7 +30,7 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* @internal * @internal
*/ */
const IS_REF = 1; const REF = 1;
/** /**
* @internal * @internal
@ -90,6 +91,8 @@ class PropertyAccessor implements PropertyAccessorInterface
private $readPropertyCache = array(); private $readPropertyCache = array();
private $writePropertyCache = array(); private $writePropertyCache = array();
private static $previousErrorHandler; private static $previousErrorHandler;
private static $errorHandler = array(__CLASS__, 'handleError');
private static $resultProto = array(self::VALUE => null);
/** /**
* Should not be used by application code. Use * Should not be used by application code. Use
@ -109,7 +112,10 @@ class PropertyAccessor implements PropertyAccessorInterface
$propertyPath = new PropertyPath($propertyPath); $propertyPath = new PropertyPath($propertyPath);
} }
$propertyValues = &$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength()); $zval = array(
self::VALUE => $objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength());
return $propertyValues[count($propertyValues) - 1][self::VALUE]; return $propertyValues[count($propertyValues) - 1][self::VALUE];
} }
@ -123,37 +129,39 @@ class PropertyAccessor implements PropertyAccessorInterface
$propertyPath = new PropertyPath($propertyPath); $propertyPath = new PropertyPath($propertyPath);
} }
$propertyValues = &$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); $zval = array(
self::VALUE => $objectOrArray,
self::REF => &$objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1);
$overwrite = true; $overwrite = true;
// Add the root object to the list
array_unshift($propertyValues, array(
self::VALUE => &$objectOrArray,
self::IS_REF => true,
));
try { try {
if (PHP_VERSION_ID < 70000) { if (PHP_VERSION_ID < 70000) {
self::$previousErrorHandler = set_error_handler(array(__CLASS__, 'handleError')); self::$previousErrorHandler = set_error_handler(self::$errorHandler);
} }
for ($i = count($propertyValues) - 1; $i >= 0; --$i) { for ($i = count($propertyValues) - 1; 0 <= $i; --$i) {
$objectOrArray = &$propertyValues[$i][self::VALUE]; $zval = $propertyValues[$i];
unset($propertyValues[$i]);
if ($overwrite) { if ($overwrite) {
$property = $propertyPath->getElement($i); $property = $propertyPath->getElement($i);
//$singular = $propertyPath->singulars[$i];
$singular = null;
if ($propertyPath->isIndex($i)) { if ($propertyPath->isIndex($i)) {
$this->writeIndex($objectOrArray, $property, $value); if ($overwrite = !isset($zval[self::REF])) {
$ref = &$zval[self::REF];
}
$this->writeIndex($zval, $property, $value);
if ($overwrite) {
$zval[self::VALUE] = $zval[self::REF];
}
} else { } else {
$this->writeProperty($objectOrArray, $property, $singular, $value); $this->writeProperty($zval, $property, $value);
} }
} }
$value = &$objectOrArray; $value = $zval[self::VALUE];
$overwrite = !$propertyValues[$i][self::IS_REF];
} }
} catch (\TypeError $e) { } catch (\TypeError $e) {
try { try {
@ -198,53 +206,51 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* Reads the path from an object up to a given path index. * Reads the path from an object up to a given path index.
* *
* @param object|array $objectOrArray The object or array to read from * @param array $zval The array containing the object or array to read from
* @param PropertyPathInterface $propertyPath The property path to read * @param PropertyPathInterface $propertyPath The property path to read
* @param int $lastIndex The index up to which should be read * @param int $lastIndex The index up to which should be read
* *
* @return array The values read in the path. * @return array The values read in the path.
* *
* @throws UnexpectedTypeException If a value within the path is neither object nor array. * @throws UnexpectedTypeException If a value within the path is neither object nor array.
*/ */
private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex) private function readPropertiesUntil($zval, PropertyPathInterface $propertyPath, $lastIndex)
{ {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) { if (!is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
throw new UnexpectedTypeException($objectOrArray, 'object or array'); throw new UnexpectedTypeException($zval[self::VALUE], 'object or array');
} }
$propertyValues = array(); // Add the root object to the list
$propertyValues = array($zval);
for ($i = 0; $i < $lastIndex; ++$i) { for ($i = 0; $i < $lastIndex; ++$i) {
$property = $propertyPath->getElement($i); $property = $propertyPath->getElement($i);
$isIndex = $propertyPath->isIndex($i); $isIndex = $propertyPath->isIndex($i);
// Create missing nested arrays on demand
if (
$isIndex &&
(
($objectOrArray instanceof \ArrayAccess && !isset($objectOrArray[$property])) ||
(is_array($objectOrArray) && !array_key_exists($property, $objectOrArray))
)
) {
if ($i + 1 < $propertyPath->getLength()) {
$objectOrArray[$property] = array();
}
}
if ($isIndex) { if ($isIndex) {
$propertyValue = &$this->readIndex($objectOrArray, $property); // Create missing nested arrays on demand
} else { if ($i + 1 < $propertyPath->getLength() && (
$propertyValue = &$this->readProperty($objectOrArray, $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]))
)) {
$zval[self::VALUE][$property] = array();
$objectOrArray = &$propertyValue[self::VALUE]; if (isset($zval[self::REF])) {
$zval[self::REF] = $zval[self::VALUE];
}
}
$zval = $this->readIndex($zval, $property);
} else {
$zval = $this->readProperty($zval, $property);
}
// the final value of the path must not be validated // the final value of the path must not be validated
if ($i + 1 < $propertyPath->getLength() && !is_object($objectOrArray) && !is_array($objectOrArray)) { if ($i + 1 < $propertyPath->getLength() && !is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
throw new UnexpectedTypeException($objectOrArray, 'object or array'); throw new UnexpectedTypeException($zval[self::VALUE], 'object or array');
} }
$propertyValues[] = &$propertyValue; $propertyValues[] = $zval;
} }
return $propertyValues; return $propertyValues;
@ -253,33 +259,30 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* Reads a key from an array-like structure. * Reads a key from an array-like structure.
* *
* @param \ArrayAccess|array $array The array or \ArrayAccess object to read from * @param array $zval The array containing the array or \ArrayAccess object to read from
* @param string|int $index The key to read * @param string|int $index The key to read
* *
* @return mixed The value of the key * @return array The array containing the value of the key
* *
* @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array
*/ */
private function &readIndex(&$array, $index) private function readIndex($zval, $index)
{ {
if (!$array instanceof \ArrayAccess && !is_array($array)) { if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
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 NoSuchPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($zval[self::VALUE])));
} }
// Use an array instead of an object since performance is very crucial here $result = self::$resultProto;
$result = array(
self::VALUE => null,
self::IS_REF => false,
);
if (isset($array[$index])) { if (isset($zval[self::VALUE][$index])) {
if (is_array($array)) { $result[self::VALUE] = $zval[self::VALUE][$index];
$result[self::VALUE] = &$array[$index];
$result[self::IS_REF] = true; if (!isset($zval[self::REF])) {
} else { // Save creating references when doing read-only lookups
$result[self::VALUE] = $array[$index]; } elseif (is_array($zval[self::VALUE])) {
// Objects are always passed around by reference $result[self::REF] = &$zval[self::REF][$index];
$result[self::IS_REF] = is_object($array[$index]) ? true : false; } elseif (is_object($result[self::VALUE])) {
$result[self::REF] = $result[self::VALUE];
} }
} }
@ -287,39 +290,32 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
/** /**
* Reads the a property from an object or array. * Reads the a property from an object.
* *
* @param object $object The object to read from. * @param array $zval The array containing the object to read from
* @param string $property The property to read. * @param string $property The property to read.
* *
* @return mixed The value of the read property * @return array The array containing the value of the property
* *
* @throws NoSuchPropertyException If the property does not exist or is not * @throws NoSuchPropertyException If the property does not exist or is not public.
* public.
*/ */
private function &readProperty(&$object, $property) private function readProperty($zval, $property)
{ {
// Use an array instead of an object since performance is if (!is_object($zval[self::VALUE])) {
// very crucial here
$result = array(
self::VALUE => null,
self::IS_REF => false,
);
if (!is_object($object)) {
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
} }
$access = $this->getReadAccessInfo($object, $property); $result = self::$resultProto;
$object = $zval[self::VALUE];
$access = $this->getReadAccessInfo(get_class($object), $property);
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) { if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) { } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
if ($access[self::ACCESS_REF]) { $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
$result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]};
$result[self::IS_REF] = true; if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
} else { $result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
} }
} 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
@ -328,8 +324,10 @@ class PropertyAccessor implements PropertyAccessorInterface
// returns true, consequently the following line will result in a // returns true, consequently the following line will result in a
// fatal error. // fatal error.
$result[self::VALUE] = &$object->$property; $result[self::VALUE] = $object->$property;
$result[self::IS_REF] = true; if (isset($zval[self::REF])) {
$result[self::REF] = &$object->$property;
}
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
// we call the getter and hope the __call do the job // we call the getter and hope the __call do the job
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
@ -338,8 +336,8 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
// Objects are always passed around by reference // Objects are always passed around by reference
if (is_object($result[self::VALUE])) { if (isset($zval[self::REF]) && is_object($result[self::VALUE])) {
$result[self::IS_REF] = true; $result[self::REF] = $result[self::VALUE];
} }
return $result; return $result;
@ -348,21 +346,21 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* Guesses how to read the property value. * Guesses how to read the property value.
* *
* @param string $object * @param string $class
* @param string $property * @param string $property
* *
* @return array * @return array
*/ */
private function getReadAccessInfo($object, $property) private function getReadAccessInfo($class, $property)
{ {
$key = get_class($object).'::'.$property; $key = $class.'::'.$property;
if (isset($this->readPropertyCache[$key])) { if (isset($this->readPropertyCache[$key])) {
$access = $this->readPropertyCache[$key]; $access = $this->readPropertyCache[$key];
} else { } else {
$access = array(); $access = array();
$reflClass = new \ReflectionClass($object); $reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelProp = $this->camelize($property); $camelProp = $this->camelize($property);
$getter = 'get'.$camelProp; $getter = 'get'.$camelProp;
@ -387,9 +385,6 @@ class PropertyAccessor implements PropertyAccessorInterface
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property; $access[self::ACCESS_NAME] = $property;
$access[self::ACCESS_REF] = true; $access[self::ACCESS_REF] = true;
$result[self::VALUE] = &$object->$property;
$result[self::IS_REF] = true;
} elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
// we call the getter and hope the __call do the job // we call the getter and hope the __call do the job
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
@ -419,38 +414,38 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* Sets the value of the property at the given index in the path. * Sets the value of the property at the given index in the path.
* *
* @param \ArrayAccess|array $array An 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
* @param mixed $value The value to write * @param mixed $value The value to write
* *
* @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array
*/ */
private function writeIndex(&$array, $index, $value) private function writeIndex($zval, $index, $value)
{ {
if (!$array instanceof \ArrayAccess && !is_array($array)) { if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
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 NoSuchPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($zval[self::VALUE])));
} }
$array[$index] = $value; $zval[self::REF][$index] = $value;
} }
/** /**
* Sets the value of the property at the given index in the path. * Sets the value of the property at the given index in the path.
* *
* @param object|array $object The object or array 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
* @param string|null $singular The singular form of the property name or null * @param mixed $value The value to write
* @param mixed $value The value to write
* *
* @throws NoSuchPropertyException If the property does not exist or is not public. * @throws NoSuchPropertyException If the property does not exist or is not public.
*/ */
private function writeProperty(&$object, $property, $singular, $value) private function writeProperty($zval, $property, $value)
{ {
if (!is_object($object)) { if (!is_object($zval[self::VALUE])) {
throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
} }
$access = $this->getWriteAccessInfo($object, $property, $singular, $value); $object = $zval[self::VALUE];
$access = $this->getWriteAccessInfo(get_class($object), $property, $value);
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) { if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]}($value); $object->{$access[self::ACCESS_NAME]}($value);
@ -458,38 +453,30 @@ class PropertyAccessor implements PropertyAccessorInterface
$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 // At this point the add and remove methods have been found
// Use iterator_to_array() instead of clone in order to prevent side effects $previousValue = $this->readProperty($zval, $property);
// see https://github.com/symfony/symfony/issues/4670 $previousValue = $previousValue[self::VALUE];
$itemsToAdd = is_object($value) ? iterator_to_array($value) : $value;
$itemToRemove = array();
$propertyValue = &$this->readProperty($object, $property);
$previousValue = $propertyValue[self::VALUE];
// remove reference to avoid modifications
unset($propertyValue);
if (is_array($previousValue) || $previousValue instanceof \Traversable) { if ($previousValue instanceof \Traversable) {
foreach ($previousValue as $previousItem) { $previousValue = iterator_to_array($previousValue);
foreach ($value as $key => $item) { }
if ($item === $previousItem) { if ($previousValue && is_array($previousValue)) {
// Item found, don't add if (is_object($value)) {
unset($itemsToAdd[$key]); $value = iterator_to_array($value);
// Next $previousItem
continue 2;
}
}
// Item not found, add to remove list
$itemToRemove[] = $previousItem;
} }
foreach ($previousValue as $key => $item) {
if (!in_array($item, $value, true)) {
unset($previousValue[$key]);
$object->{$access[self::ACCESS_REMOVER]}($item);
}
}
} else {
$previousValue = false;
} }
foreach ($itemToRemove as $item) { foreach ($value as $item) {
$object->{$access[self::ACCESS_REMOVER]}($item); if (!$previousValue || !in_array($item, $previousValue, true)) {
} $object->{$access[self::ACCESS_ADDER]}($item);
}
foreach ($itemsToAdd as $item) {
$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
@ -509,16 +496,15 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* Guesses how to write the property value. * Guesses how to write the property value.
* *
* @param string $object * @param string $class
* @param string $property * @param string $property
* @param string|null $singular * @param mixed $value
* @param mixed $value
* *
* @return array * @return array
*/ */
private function getWriteAccessInfo($object, $property, $singular, $value) private function getWriteAccessInfo($class, $property, $value)
{ {
$key = get_class($object).'::'.$property; $key = $class.'::'.$property;
$guessedAdders = ''; $guessedAdders = '';
if (isset($this->writePropertyCache[$key])) { if (isset($this->writePropertyCache[$key])) {
@ -526,12 +512,12 @@ class PropertyAccessor implements PropertyAccessorInterface
} else { } else {
$access = array(); $access = array();
$reflClass = new \ReflectionClass($object); $reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$plural = $this->camelize($property); $plural = $this->camelize($property);
// Any of the two methods is required, but not yet known // 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) { if (is_array($value) || $value instanceof \Traversable) {
$methods = $this->findAdderAndRemover($reflClass, $singulars); $methods = $this->findAdderAndRemover($reflClass, $singulars);
@ -638,8 +624,7 @@ class PropertyAccessor implements PropertyAccessorInterface
* @param string $methodName The method name * @param string $methodName The method name
* @param int $parameters The number of parameters * @param int $parameters The number of parameters
* *
* @return bool Whether the method is public and has $parameters * @return bool Whether the method is public and has $parameters required parameters
* required parameters
*/ */
private function isAccessible(\ReflectionClass $class, $methodName, $parameters) private function isAccessible(\ReflectionClass $class, $methodName, $parameters)
{ {

View File

@ -91,7 +91,7 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
$remaining = $propertyPath; $remaining = $propertyPath;
// first element is evaluated differently - no leading dot for properties // first element is evaluated differently - no leading dot for properties
$pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/'; $pattern = '/^(([^\.\[]++)|\[([^\]]++)\])(.*)/';
while (preg_match($pattern, $remaining, $matches)) { while (preg_match($pattern, $remaining, $matches)) {
if ('' !== $matches[2]) { if ('' !== $matches[2]) {
@ -106,7 +106,7 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
$position += strlen($matches[1]); $position += strlen($matches[1]);
$remaining = $matches[4]; $remaining = $matches[4];
$pattern = '/^(\.(\w+)|\[([^\]]+)\])(.*)/'; $pattern = '/^(\.(\w++)|\[([^\]]++)\])(.*)/';
} }
if ('' !== $remaining) { if ('' !== $remaining) {