From 3ff819e7a1d0ae69a0ea152dc4b446c00a8d1ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Oct 2015 10:40:31 +0200 Subject: [PATCH 1/8] [PropertyAccess] Major performance improvement --- .../PropertyAccess/PropertyAccessor.php | 301 ++++++++++++------ .../Tests/PropertyAccessorTest.php | 2 +- .../Component/PropertyAccess/composer.json | 6 + 3 files changed, 212 insertions(+), 97 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 76ef8c1d5cfb6..37bd80e17d660 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -11,6 +11,7 @@ namespace Symfony\Component\PropertyAccess; +use Doctrine\Common\Cache\Cache; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; @@ -23,16 +24,30 @@ class PropertyAccessor implements PropertyAccessorInterface { const VALUE = 0; const IS_REF = 1; + const ACCESS_TYPE = 0; + const ACCESS_NAME = 1; + const ACCESS_REF = 2; + const ACCESS_ADDER = 3; + const ACCESS_REMOVER = 4; + const ACCESS_TYPE_METHOD = 0; + const ACCESS_TYPE_PROPERTY = 1; + const ACCESS_TYPE_ADDER_AND_REMOVER = 2; + const ACCESS_TYPE_NOT_FOUND = 3; private $magicCall; + private $cache; + private $propertyPathCache = array(); + private $readPropertyCache = array(); + private $writePropertyCache = array(); /** * Should not be used by application code. Use * {@link PropertyAccess::createPropertyAccessor()} instead. */ - public function __construct($magicCall = false) + public function __construct($magicCall = false, Cache $cache = null) { $this->magicCall = $magicCall; + $this->cache = $cache; } /** @@ -40,10 +55,7 @@ public function __construct($magicCall = false) */ public function getValue($objectOrArray, $propertyPath) { - if (!$propertyPath instanceof PropertyPathInterface) { - $propertyPath = new PropertyPath($propertyPath); - } - + $propertyPath = $this->getPropertyPath($propertyPath); $propertyValues = &$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength()); return $propertyValues[count($propertyValues) - 1][self::VALUE]; @@ -54,10 +66,7 @@ public function getValue($objectOrArray, $propertyPath) */ public function setValue(&$objectOrArray, $propertyPath, $value) { - if (!$propertyPath instanceof PropertyPathInterface) { - $propertyPath = new PropertyPath($propertyPath); - } - + $propertyPath = $this->getPropertyPath($propertyPath); $propertyValues = &$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); $overwrite = true; @@ -202,48 +211,92 @@ private function &readProperty(&$object, $property) throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); } - $camelProp = $this->camelize($property); - $reflClass = new \ReflectionClass($object); - $getter = 'get'.$camelProp; - $isser = 'is'.$camelProp; - $hasser = 'has'.$camelProp; - $classHasProperty = $reflClass->hasProperty($property); - - if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { - $result[self::VALUE] = $object->$getter(); - } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) { - $result[self::VALUE] = $object->$isser(); - } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) { - $result[self::VALUE] = $object->$hasser(); - } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) { - $result[self::VALUE] = $object->$property; - } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { - $result[self::VALUE] = &$object->$property; - $result[self::IS_REF] = true; - } elseif (!$classHasProperty && property_exists($object, $property)) { - // Needed to support \stdClass instances. We need to explicitly - // exclude $classHasProperty, otherwise if in the previous clause - // a *protected* property was found on the class, property_exists() - // returns true, consequently the following line will result in a - // fatal error. - $result[self::VALUE] = &$object->$property; - $result[self::IS_REF] = true; - } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { - // we call the getter and hope the __call do the job - $result[self::VALUE] = $object->$getter(); - } else { - $methods = array($getter, $isser, $hasser, '__get'); - if ($this->magicCall) { - $methods[] = '__call'; + $key = spl_object_hash($object).'::'.$property; + + if (isset($this->readPropertyCache[$key])) { + $access = $this->readPropertyCache[$key]; + } elseif (!$this->cache || !$access = $this->cache->fetch('read'.$key)) { + $camelProp = $this->camelize($property); + $reflClass = new \ReflectionClass($object); + $getter = 'get' . $camelProp; + $isser = 'is' . $camelProp; + $hasser = 'has' . $camelProp; + $classHasProperty = $reflClass->hasProperty($property); + + $access = array(); + + if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $getter; + } 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; + } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + $access[self::ACCESS_REF] = true; + + $result[self::VALUE] = &$object->$property; + $result[self::IS_REF] = true; + } elseif (!$classHasProperty && property_exists($object, $property)) { + // Needed to support \stdClass instances. We need to explicitly + // exclude $classHasProperty, otherwise if in the previous clause + // a *protected* property was found on the class, property_exists() + // returns true, consequently the following line will result in a + // fatal error. + + $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_METHOD; + $access[self::ACCESS_NAME] = $getter; + } else { + $methods = array($getter, $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 + ); } - throw new NoSuchPropertyException(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; + if ($this->cache) { + $this->cache->save('read'.$key, $access); + } + } + + switch ($access[self::ACCESS_TYPE]) { + case self::ACCESS_TYPE_METHOD: + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); + break; + + case self::ACCESS_TYPE_PROPERTY: + if ($access[self::ACCESS_REF]) { + $result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]}; + $result[self::IS_REF] = true; + } else { + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}; + } + break; + + default: + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); } // Objects are always passed around by reference @@ -291,16 +344,88 @@ private function writeProperty(&$object, $property, $singular, $value) 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); + $key = spl_object_hash($object).'::'.$property; + + if (isset($this->writePropertyCache[$key])) { + $access = $this->writePropertyCache[$key]; + } elseif (!$this->cache || !$access = $this->cache->fetch('write'.$key)) { + $access = array(); + $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); + + if (is_array($value) || $value instanceof \Traversable) { + $methods = $this->findAdderAndRemover($reflClass, $singulars); + + if (null === $methods) { + // 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) . '()", '; + } else { + $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])) { + $setter = 'set' . $this->camelize($property); + $classHasProperty = $reflClass->hasProperty($property); + + if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $setter; + } elseif ($reflClass->hasMethod('__set') && $reflClass->getMethod('__set')->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + } elseif (!$classHasProperty && property_exists($object, $property)) { + // Needed to support \stdClass instances. We need to explicitly + // exclude $classHasProperty, otherwise if in the previous clause + // a *protected* property was found on the class, property_exists() + // returns true, consequently the following line will result in a + // fatal error. + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + } 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_METHOD; + $access[self::ACCESS_NAME] = $setter; + } 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()", ' . + '"__set()" or "__call()" exist and have public access in class "%s".', + $property, + $guessedAdders, + $setter, + $reflClass->name + ); + } + } + + $this->writePropertyCache[$key] = $access; + if ($this->cache) { + $this->cache->save('write'.$key, $access); + } + } - // Any of the two methods is required, but not yet known - $singulars = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural); + switch ($access[self::ACCESS_TYPE]) { + case self::ACCESS_TYPE_METHOD: + $object->{$access[self::ACCESS_NAME]}($value); + break; - if (is_array($value) || $value instanceof \Traversable) { - $methods = $this->findAdderAndRemover($reflClass, $singulars); + case self::ACCESS_TYPE_PROPERTY: + $object->{$access[self::ACCESS_NAME]} = $value; + break; - if (null !== $methods) { + case self::ACCESS_TYPE_ADDER_AND_REMOVER: // 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 @@ -329,51 +454,16 @@ private function writeProperty(&$object, $property, $singular, $value) } foreach ($itemToRemove as $item) { - call_user_func(array($object, $methods[1]), $item); + call_user_func(array($object, $access[self::ACCESS_REMOVER]), $item); } foreach ($itemsToAdd as $item) { - call_user_func(array($object, $methods[0]), $item); + call_user_func(array($object, $access[self::ACCESS_ADDER]), $item); } + break; - 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()) { - $object->$setter($value); - } elseif ($reflClass->hasMethod('__set') && $reflClass->getMethod('__set')->isPublic()) { - $object->$property = $value; - } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { - $object->$property = $value; - } elseif (!$classHasProperty && property_exists($object, $property)) { - // Needed to support \stdClass instances. We need to explicitly - // exclude $classHasProperty, otherwise if in the previous clause - // a *protected* property was found on the class, property_exists() - // 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()) { - // we call the getter and hope the __call do the job - $object->$setter($value); - } else { - throw new NoSuchPropertyException(sprintf( - 'Neither the property "%s" nor one of the methods %s"%s()", '. - '"__set()" or "__call()" exist and have public access in class "%s".', - $property, - $guessedAdders, - $setter, - $reflClass->name - )); + default: + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); } } @@ -445,4 +535,23 @@ private function isAccessible(\ReflectionClass $class, $methodName, $parameters) return false; } + + /** + * @param PropertyPathInterface|string $propertyPath + * + * @return PropertyPath + */ + private function getPropertyPath($propertyPath) + { + if ($propertyPath instanceof PropertyPathInterface) { + return $propertyPath; + } + + if (isset($this->propertyPathCache[$propertyPath])) { + return $this->propertyPathCache[$propertyPath]; + } + + $this->propertyPathCache[$propertyPath] = new PropertyPath($propertyPath); + return $this->propertyPathCache[$propertyPath]; + } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 87287048a7752..4cd46bb2f4cf9 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -232,7 +232,7 @@ public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p public function testGetValueWhenArrayValueIsNull() { - $this->propertyAccessor = new PropertyAccessor(false, true); + $this->propertyAccessor = new PropertyAccessor(false); $this->assertNull($this->propertyAccessor->getValue(array('index' => array('nullable' => null)), '[index][nullable]')); } diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index 060ff8ef2c133..06f7768327d53 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -18,6 +18,12 @@ "require": { "php": ">=5.3.3" }, + "require-dev": { + "doctrine/cache": "~2.4" + }, + "suggest": { + "doctrine/cache": "To cache access methods" + }, "autoload": { "psr-0": { "Symfony\\Component\\PropertyAccess\\": "" } }, From fa34c9e2a71581ea547f679c57eb94711d82f727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Oct 2015 11:21:34 +0200 Subject: [PATCH 2/8] [PropertyAccess] use get_class instead of spl_object_hash --- src/Symfony/Component/PropertyAccess/PropertyAccessor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 37bd80e17d660..0fef07dd90923 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -211,7 +211,7 @@ private function &readProperty(&$object, $property) throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); } - $key = spl_object_hash($object).'::'.$property; + $key = get_class($object).'::'.$property; if (isset($this->readPropertyCache[$key])) { $access = $this->readPropertyCache[$key]; @@ -344,7 +344,7 @@ private function writeProperty(&$object, $property, $singular, $value) throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); } - $key = spl_object_hash($object).'::'.$property; + $key = get_class($object).'::'.$property; if (isset($this->writePropertyCache[$key])) { $access = $this->writePropertyCache[$key]; From b5dabe7b99026a21b023e866a24d474f197bd761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Oct 2015 13:36:47 +0200 Subject: [PATCH 3/8] Revert Doctrine Cache and PropertyPath optims --- .../PropertyAccess/PropertyAccessor.php | 45 +++++-------------- .../Component/PropertyAccess/composer.json | 6 --- 2 files changed, 11 insertions(+), 40 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 0fef07dd90923..96d5aee34b3cf 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -11,7 +11,6 @@ namespace Symfony\Component\PropertyAccess; -use Doctrine\Common\Cache\Cache; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; @@ -35,8 +34,6 @@ class PropertyAccessor implements PropertyAccessorInterface const ACCESS_TYPE_NOT_FOUND = 3; private $magicCall; - private $cache; - private $propertyPathCache = array(); private $readPropertyCache = array(); private $writePropertyCache = array(); @@ -44,10 +41,9 @@ class PropertyAccessor implements PropertyAccessorInterface * Should not be used by application code. Use * {@link PropertyAccess::createPropertyAccessor()} instead. */ - public function __construct($magicCall = false, Cache $cache = null) + public function __construct($magicCall = false) { $this->magicCall = $magicCall; - $this->cache = $cache; } /** @@ -55,7 +51,10 @@ public function __construct($magicCall = false, Cache $cache = null) */ public function getValue($objectOrArray, $propertyPath) { - $propertyPath = $this->getPropertyPath($propertyPath); + if (!$propertyPath instanceof PropertyPathInterface) { + $propertyPath = new PropertyPath($propertyPath); + } + $propertyValues = &$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength()); return $propertyValues[count($propertyValues) - 1][self::VALUE]; @@ -66,7 +65,10 @@ public function getValue($objectOrArray, $propertyPath) */ public function setValue(&$objectOrArray, $propertyPath, $value) { - $propertyPath = $this->getPropertyPath($propertyPath); + if (!$propertyPath instanceof PropertyPathInterface) { + $propertyPath = new PropertyPath($propertyPath); + } + $propertyValues = &$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); $overwrite = true; @@ -215,7 +217,7 @@ private function &readProperty(&$object, $property) if (isset($this->readPropertyCache[$key])) { $access = $this->readPropertyCache[$key]; - } elseif (!$this->cache || !$access = $this->cache->fetch('read'.$key)) { + } else { $camelProp = $this->camelize($property); $reflClass = new \ReflectionClass($object); $getter = 'get' . $camelProp; @@ -276,9 +278,6 @@ private function &readProperty(&$object, $property) } $this->readPropertyCache[$key] = $access; - if ($this->cache) { - $this->cache->save('read'.$key, $access); - } } switch ($access[self::ACCESS_TYPE]) { @@ -348,7 +347,7 @@ private function writeProperty(&$object, $property, $singular, $value) if (isset($this->writePropertyCache[$key])) { $access = $this->writePropertyCache[$key]; - } elseif (!$this->cache || !$access = $this->cache->fetch('write'.$key)) { + } else { $access = array(); $reflClass = new \ReflectionClass($object); $plural = $this->camelize($property); @@ -411,9 +410,6 @@ private function writeProperty(&$object, $property, $singular, $value) } $this->writePropertyCache[$key] = $access; - if ($this->cache) { - $this->cache->save('write'.$key, $access); - } } switch ($access[self::ACCESS_TYPE]) { @@ -535,23 +531,4 @@ private function isAccessible(\ReflectionClass $class, $methodName, $parameters) return false; } - - /** - * @param PropertyPathInterface|string $propertyPath - * - * @return PropertyPath - */ - private function getPropertyPath($propertyPath) - { - if ($propertyPath instanceof PropertyPathInterface) { - return $propertyPath; - } - - if (isset($this->propertyPathCache[$propertyPath])) { - return $this->propertyPathCache[$propertyPath]; - } - - $this->propertyPathCache[$propertyPath] = new PropertyPath($propertyPath); - return $this->propertyPathCache[$propertyPath]; - } } diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index 06f7768327d53..060ff8ef2c133 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -18,12 +18,6 @@ "require": { "php": ">=5.3.3" }, - "require-dev": { - "doctrine/cache": "~2.4" - }, - "suggest": { - "doctrine/cache": "To cache access methods" - }, "autoload": { "psr-0": { "Symfony\\Component\\PropertyAccess\\": "" } }, From de28914025ec7351bcb7e695aa8a0adc10af064d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Oct 2015 13:39:59 +0200 Subject: [PATCH 4/8] Don't cache dynamic properties. --- .../PropertyAccess/PropertyAccessor.php | 28 +++++++++++-------- .../Tests/PropertyAccessorTest.php | 2 +- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 96d5aee34b3cf..a3738cdf943f0 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -247,16 +247,6 @@ private function &readProperty(&$object, $property) $result[self::VALUE] = &$object->$property; $result[self::IS_REF] = true; - } elseif (!$classHasProperty && property_exists($object, $property)) { - // Needed to support \stdClass instances. We need to explicitly - // exclude $classHasProperty, otherwise if in the previous clause - // a *protected* property was found on the class, property_exists() - // returns true, consequently the following line will result in a - // fatal error. - - $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_METHOD; @@ -295,7 +285,21 @@ private function &readProperty(&$object, $property) break; default: - throw new NoSuchPropertyException($access[self::ACCESS_NAME]); + $reflClass = new \ReflectionClass($object); + + if (!$reflClass->hasProperty($property) && property_exists($object, $property)) { + // Needed to support \stdClass instances. We need to explicitly + // exclude $classHasProperty, otherwise if in the previous clause + // a *protected* property was found on the class, property_exists() + // returns true, consequently the following line will result in a + // fatal error. + + $result[self::VALUE] = &$object->$property; + $result[self::IS_REF] = true; + } else { + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); + } + break; } // Objects are always passed around by reference @@ -456,7 +460,7 @@ private function writeProperty(&$object, $property, $singular, $value) foreach ($itemsToAdd as $item) { call_user_func(array($object, $access[self::ACCESS_ADDER]), $item); } - break; + break; default: throw new NoSuchPropertyException($access[self::ACCESS_NAME]); diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 4cd46bb2f4cf9..87287048a7752 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -232,7 +232,7 @@ public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p public function testGetValueWhenArrayValueIsNull() { - $this->propertyAccessor = new PropertyAccessor(false); + $this->propertyAccessor = new PropertyAccessor(false, true); $this->assertNull($this->propertyAccessor->getValue(array('index' => array('nullable' => null)), '[index][nullable]')); } From 5c16c09bfbbe7c789e81d1e63cc9a23c23855082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Oct 2015 13:51:38 +0200 Subject: [PATCH 5/8] Fix CS --- .../PropertyAccess/PropertyAccessor.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index a3738cdf943f0..e6573ebf0bee9 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -220,9 +220,9 @@ private function &readProperty(&$object, $property) } else { $camelProp = $this->camelize($property); $reflClass = new \ReflectionClass($object); - $getter = 'get' . $camelProp; - $isser = 'is' . $camelProp; - $hasser = 'has' . $camelProp; + $getter = 'get'.$camelProp; + $isser = 'is'.$camelProp; + $hasser = 'has'.$camelProp; $classHasProperty = $reflClass->hasProperty($property); $access = array(); @@ -259,7 +259,7 @@ private function &readProperty(&$object, $property) $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; $access[self::ACCESS_NAME] = sprintf( - 'Neither the property "%s" nor one of the methods "%s()" ' . + 'Neither the property "%s" nor one of the methods "%s()" '. 'exist and have public access in class "%s".', $property, implode('()", "', $methods), @@ -357,7 +357,7 @@ private function writeProperty(&$object, $property, $singular, $value) $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 = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural); if (is_array($value) || $value instanceof \Traversable) { $methods = $this->findAdderAndRemover($reflClass, $singulars); @@ -367,7 +367,7 @@ private function writeProperty(&$object, $property, $singular, $value) // 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) . '()", '; + $guessedAdders = '"add'.implode('()", "add', $singulars).'()", '; } else { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; $access[self::ACCESS_ADDER] = $methods[0]; @@ -376,7 +376,7 @@ private function writeProperty(&$object, $property, $singular, $value) } if (!isset($access[self::ACCESS_TYPE])) { - $setter = 'set' . $this->camelize($property); + $setter = 'set'.$this->camelize($property); $classHasProperty = $reflClass->hasProperty($property); if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) { @@ -403,7 +403,7 @@ private function writeProperty(&$object, $property, $singular, $value) } 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()", ' . + 'Neither the property "%s" nor one of the methods %s"%s()", '. '"__set()" or "__call()" exist and have public access in class "%s".', $property, $guessedAdders, From 8275af1ed92b580cdd12376447432fcd00aaf05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Oct 2015 17:43:08 +0200 Subject: [PATCH 6/8] Fix guess order --- .../PropertyAccess/PropertyAccessor.php | 193 +++++++++--------- 1 file changed, 92 insertions(+), 101 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index e6573ebf0bee9..2ea0e0dd36eb2 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -23,15 +23,17 @@ class PropertyAccessor implements PropertyAccessorInterface { const VALUE = 0; const IS_REF = 1; - const ACCESS_TYPE = 0; - const ACCESS_NAME = 1; - const ACCESS_REF = 2; - const ACCESS_ADDER = 3; - const ACCESS_REMOVER = 4; + const ACCESS_REFLECTION_CLASS = 0; + const ACCESS_TYPE = 1; + const ACCESS_NAME = 2; + const ACCESS_REF = 3; + const ACCESS_ADDER = 4; + const ACCESS_REMOVER = 5; const ACCESS_TYPE_METHOD = 0; const ACCESS_TYPE_PROPERTY = 1; - const ACCESS_TYPE_ADDER_AND_REMOVER = 2; - const ACCESS_TYPE_NOT_FOUND = 3; + const ACCESS_TYPE_MAGIC = 2; + const ACCESS_TYPE_ADDER_AND_REMOVER = 3; + const ACCESS_TYPE_NOT_FOUND = 4; private $magicCall; private $readPropertyCache = array(); @@ -218,15 +220,15 @@ private function &readProperty(&$object, $property) if (isset($this->readPropertyCache[$key])) { $access = $this->readPropertyCache[$key]; } else { + $access = array(); + + $access[self::ACCESS_REFLECTION_CLASS] = $reflClass = new \ReflectionClass($object); $camelProp = $this->camelize($property); - $reflClass = new \ReflectionClass($object); - $getter = 'get'.$camelProp; - $isser = 'is'.$camelProp; - $hasser = 'has'.$camelProp; + $getter = 'get' . $camelProp; + $isser = 'is' . $camelProp; + $hasser = 'has' . $camelProp; $classHasProperty = $reflClass->hasProperty($property); - $access = array(); - if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; $access[self::ACCESS_NAME] = $getter; @@ -249,7 +251,7 @@ private function &readProperty(&$object, $property) $result[self::IS_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_METHOD; + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; $access[self::ACCESS_NAME] = $getter; } else { $methods = array($getter, $isser, $hasser, '__get'); @@ -259,7 +261,7 @@ private function &readProperty(&$object, $property) $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; $access[self::ACCESS_NAME] = sprintf( - 'Neither the property "%s" nor one of the methods "%s()" '. + 'Neither the property "%s" nor one of the methods "%s()" ' . 'exist and have public access in class "%s".', $property, implode('()", "', $methods), @@ -270,36 +272,29 @@ private function &readProperty(&$object, $property) $this->readPropertyCache[$key] = $access; } - switch ($access[self::ACCESS_TYPE]) { - case self::ACCESS_TYPE_METHOD: - $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); - break; - - case self::ACCESS_TYPE_PROPERTY: - if ($access[self::ACCESS_REF]) { - $result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]}; - $result[self::IS_REF] = true; - } else { - $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}; - } - break; - - default: - $reflClass = new \ReflectionClass($object); - - if (!$reflClass->hasProperty($property) && property_exists($object, $property)) { - // Needed to support \stdClass instances. We need to explicitly - // exclude $classHasProperty, otherwise if in the previous clause - // a *protected* property was found on the class, property_exists() - // returns true, consequently the following line will result in a - // fatal error. - - $result[self::VALUE] = &$object->$property; - $result[self::IS_REF] = true; - } else { - throw new NoSuchPropertyException($access[self::ACCESS_NAME]); - } - break; + 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]) { + if ($access[self::ACCESS_REF]) { + $result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]}; + $result[self::IS_REF] = true; + } else { + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}; + } + } elseif (!$access[self::ACCESS_REFLECTION_CLASS]->hasProperty($property) && property_exists($object, $property)) { + // Needed to support \stdClass instances. We need to explicitly + // exclude $classHasProperty, otherwise if in the previous clause + // a *protected* property was found on the class, property_exists() + // returns true, consequently the following line will result in a + // fatal error. + + $result[self::VALUE] = &$object->$property; + $result[self::IS_REF] = true; + } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { + // we call the getter and hope the __call do the job + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); + } else { + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); } // Objects are always passed around by reference @@ -353,11 +348,12 @@ private function writeProperty(&$object, $property, $singular, $value) $access = $this->writePropertyCache[$key]; } else { $access = array(); - $reflClass = new \ReflectionClass($object); + + $access[self::ACCESS_REFLECTION_CLASS] = $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 = null !== $singular ? array($singular) : (array)StringUtil::singularify($plural); if (is_array($value) || $value instanceof \Traversable) { $methods = $this->findAdderAndRemover($reflClass, $singulars); @@ -367,7 +363,7 @@ private function writeProperty(&$object, $property, $singular, $value) // 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).'()", '; + $guessedAdders = '"add' . implode('()", "add', $singulars) . '()", '; } else { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; $access[self::ACCESS_ADDER] = $methods[0]; @@ -376,7 +372,7 @@ private function writeProperty(&$object, $property, $singular, $value) } if (!isset($access[self::ACCESS_TYPE])) { - $setter = 'set'.$this->camelize($property); + $setter = 'set' . $this->camelize($property); $classHasProperty = $reflClass->hasProperty($property); if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) { @@ -388,22 +384,14 @@ private function writeProperty(&$object, $property, $singular, $value) } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; $access[self::ACCESS_NAME] = $property; - } elseif (!$classHasProperty && property_exists($object, $property)) { - // Needed to support \stdClass instances. We need to explicitly - // exclude $classHasProperty, otherwise if in the previous clause - // a *protected* property was found on the class, property_exists() - // returns true, consequently the following line will result in a - // fatal error. - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; - $access[self::ACCESS_NAME] = $property; } 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_METHOD; + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; $access[self::ACCESS_NAME] = $setter; } 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()", '. + 'Neither the property "%s" nor one of the methods %s"%s()", ' . '"__set()" or "__call()" exist and have public access in class "%s".', $property, $guessedAdders, @@ -416,54 +404,57 @@ private function writeProperty(&$object, $property, $singular, $value) $this->writePropertyCache[$key] = $access; } - switch ($access[self::ACCESS_TYPE]) { - case self::ACCESS_TYPE_METHOD: - $object->{$access[self::ACCESS_NAME]}($value); - break; - - case self::ACCESS_TYPE_PROPERTY: - $object->{$access[self::ACCESS_NAME]} = $value; - break; - - case self::ACCESS_TYPE_ADDER_AND_REMOVER: - // 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]; - // remove reference to avoid modifications - unset($propertyValue); - - 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; - } + 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]) { + // 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]; + // remove reference to avoid modifications + unset($propertyValue); + + 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, $access[self::ACCESS_REMOVER]), $item); + // Item not found, add to remove list + $itemToRemove[] = $previousItem; } + } - foreach ($itemsToAdd as $item) { - call_user_func(array($object, $access[self::ACCESS_ADDER]), $item); - } - break; + foreach ($itemToRemove as $item) { + call_user_func(array($object, $access[self::ACCESS_REMOVER]), $item); + } - default: - throw new NoSuchPropertyException($access[self::ACCESS_NAME]); + foreach ($itemsToAdd as $item) { + call_user_func(array($object, $access[self::ACCESS_ADDER]), $item); + } + } elseif (!$access[self::ACCESS_REFLECTION_CLASS]->hasProperty($property) && property_exists($object, $property)) { + // Needed to support \stdClass instances. We need to explicitly + // exclude $classHasProperty, otherwise if in the previous clause + // a *protected* property was found on the class, property_exists() + // returns true, consequently the following line will result in a + // fatal error. + + $object->{$access[self::ACCESS_NAME]} = $value; + } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { + $object->{$access[self::ACCESS_NAME]}($value); + } else { + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); } } From 50d7c0f6341f8265b31703b2f8da10ee8f8371d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Oct 2015 17:45:05 +0200 Subject: [PATCH 7/8] Fix CS --- .../PropertyAccess/PropertyAccessor.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 2ea0e0dd36eb2..3e54a80839851 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -224,9 +224,9 @@ private function &readProperty(&$object, $property) $access[self::ACCESS_REFLECTION_CLASS] = $reflClass = new \ReflectionClass($object); $camelProp = $this->camelize($property); - $getter = 'get' . $camelProp; - $isser = 'is' . $camelProp; - $hasser = 'has' . $camelProp; + $getter = 'get'.$camelProp; + $isser = 'is'.$camelProp; + $hasser = 'has'.$camelProp; $classHasProperty = $reflClass->hasProperty($property); if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { @@ -261,7 +261,7 @@ private function &readProperty(&$object, $property) $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; $access[self::ACCESS_NAME] = sprintf( - 'Neither the property "%s" nor one of the methods "%s()" ' . + 'Neither the property "%s" nor one of the methods "%s()" '. 'exist and have public access in class "%s".', $property, implode('()", "', $methods), @@ -353,7 +353,7 @@ private function writeProperty(&$object, $property, $singular, $value) $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 = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural); if (is_array($value) || $value instanceof \Traversable) { $methods = $this->findAdderAndRemover($reflClass, $singulars); @@ -363,7 +363,7 @@ private function writeProperty(&$object, $property, $singular, $value) // 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) . '()", '; + $guessedAdders = '"add'.implode('()", "add', $singulars).'()", '; } else { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; $access[self::ACCESS_ADDER] = $methods[0]; @@ -372,7 +372,7 @@ private function writeProperty(&$object, $property, $singular, $value) } if (!isset($access[self::ACCESS_TYPE])) { - $setter = 'set' . $this->camelize($property); + $setter = 'set'.$this->camelize($property); $classHasProperty = $reflClass->hasProperty($property); if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) { @@ -391,7 +391,7 @@ private function writeProperty(&$object, $property, $singular, $value) } 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()", ' . + 'Neither the property "%s" nor one of the methods %s"%s()", '. '"__set()" or "__call()" exist and have public access in class "%s".', $property, $guessedAdders, From 97d351c2a3cd829db5aa4a39416c614accddf4c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sun, 25 Oct 2015 23:18:22 +0100 Subject: [PATCH 8/8] Fix @stof comments --- .../PropertyAccess/PropertyAccessor.php | 209 ++++++++++-------- 1 file changed, 121 insertions(+), 88 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 3e54a80839851..c9466b8cbdf56 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -23,7 +23,7 @@ class PropertyAccessor implements PropertyAccessorInterface { const VALUE = 0; const IS_REF = 1; - const ACCESS_REFLECTION_CLASS = 0; + const ACCESS_HAS_PROPERTY = 0; const ACCESS_TYPE = 1; const ACCESS_NAME = 2; const ACCESS_REF = 3; @@ -215,6 +215,51 @@ private function &readProperty(&$object, $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); + + 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]) { + if ($access[self::ACCESS_REF]) { + $result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]}; + $result[self::IS_REF] = true; + } else { + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}; + } + } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { + // Needed to support \stdClass instances. We need to explicitly + // exclude $classHasProperty, otherwise if in the previous clause + // a *protected* property was found on the class, property_exists() + // returns true, consequently the following line will result in a + // fatal error. + + $result[self::VALUE] = &$object->$property; + $result[self::IS_REF] = true; + } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { + // we call the getter and hope the __call do the job + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); + } else { + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); + } + + // Objects are always passed around by reference + if (is_object($result[self::VALUE])) { + $result[self::IS_REF] = true; + } + + return $result; + } + + /** + * Guesses how to read the property value. + * + * @param string $object + * @param string $property + * + * @return array + */ + private function getReadAccessInfo($object, $property) + { $key = get_class($object).'::'.$property; if (isset($this->readPropertyCache[$key])) { @@ -222,7 +267,8 @@ private function &readProperty(&$object, $property) } else { $access = array(); - $access[self::ACCESS_REFLECTION_CLASS] = $reflClass = new \ReflectionClass($object); + $reflClass = new \ReflectionClass($object); + $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); $camelProp = $this->camelize($property); $getter = 'get'.$camelProp; $isser = 'is'.$camelProp; @@ -272,37 +318,7 @@ private function &readProperty(&$object, $property) $this->readPropertyCache[$key] = $access; } - 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]) { - if ($access[self::ACCESS_REF]) { - $result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]}; - $result[self::IS_REF] = true; - } else { - $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}; - } - } elseif (!$access[self::ACCESS_REFLECTION_CLASS]->hasProperty($property) && property_exists($object, $property)) { - // Needed to support \stdClass instances. We need to explicitly - // exclude $classHasProperty, otherwise if in the previous clause - // a *protected* property was found on the class, property_exists() - // returns true, consequently the following line will result in a - // fatal error. - - $result[self::VALUE] = &$object->$property; - $result[self::IS_REF] = true; - } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { - // we call the getter and hope the __call do the job - $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); - } else { - throw new NoSuchPropertyException($access[self::ACCESS_NAME]); - } - - // Objects are always passed around by reference - if (is_object($result[self::VALUE])) { - $result[self::IS_REF] = true; - } - - return $result; + return $access; } /** @@ -336,20 +352,88 @@ private function writeIndex(&$array, $index, $value) */ private function writeProperty(&$object, $property, $singular, $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)); } + $access = $this->getWriteAccessInfo($object, $property, $singular, $value); + + 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]) { + // 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]; + // remove reference to avoid modifications + unset($propertyValue); + + 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, $access[self::ACCESS_REMOVER]), $item); + } + + foreach ($itemsToAdd as $item) { + call_user_func(array($object, $access[self::ACCESS_ADDER]), $item); + } + } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { + // Needed to support \stdClass instances. We need to explicitly + // exclude $classHasProperty, otherwise if in the previous clause + // a *protected* property was found on the class, property_exists() + // returns true, consequently the following line will result in a + // fatal error. + + $object->{$access[self::ACCESS_NAME]} = $value; + } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { + $object->{$access[self::ACCESS_NAME]}($value); + } else { + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); + } + } + + /** + * Guesses how to write the property value. + * + * @param string $object + * @param string $property + * @param string|null $singular + * @param mixed $value + * + * @return array + */ + private function getWriteAccessInfo($object, $property, $singular, $value) + { $key = get_class($object).'::'.$property; + $guessedAdders = ''; if (isset($this->writePropertyCache[$key])) { $access = $this->writePropertyCache[$key]; } else { $access = array(); - $access[self::ACCESS_REFLECTION_CLASS] = $reflClass = new \ReflectionClass($object); + $reflClass = new \ReflectionClass($object); + $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); $plural = $this->camelize($property); // Any of the two methods is required, but not yet known @@ -404,58 +488,7 @@ private function writeProperty(&$object, $property, $singular, $value) $this->writePropertyCache[$key] = $access; } - 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]) { - // 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]; - // remove reference to avoid modifications - unset($propertyValue); - - 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, $access[self::ACCESS_REMOVER]), $item); - } - - foreach ($itemsToAdd as $item) { - call_user_func(array($object, $access[self::ACCESS_ADDER]), $item); - } - } elseif (!$access[self::ACCESS_REFLECTION_CLASS]->hasProperty($property) && property_exists($object, $property)) { - // Needed to support \stdClass instances. We need to explicitly - // exclude $classHasProperty, otherwise if in the previous clause - // a *protected* property was found on the class, property_exists() - // returns true, consequently the following line will result in a - // fatal error. - - $object->{$access[self::ACCESS_NAME]} = $value; - } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { - $object->{$access[self::ACCESS_NAME]}($value); - } else { - throw new NoSuchPropertyException($access[self::ACCESS_NAME]); - } + return $access; } /**