From fc250863a8825f8f47a77ded13eff812bbf79f2c Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 26 Mar 2019 12:55:18 +0100 Subject: [PATCH 1/3] [PropertyInfo] Add accessor and mutator extractor interface and implementation on reflection --- .../PropertyAccess/PropertyAccessor.php | 373 +++++------------- .../Component/PropertyAccess/composer.json | 3 +- .../Extractor/ReflectionExtractor.php | 332 ++++++++++++++-- .../PropertyInfo/PropertyReadInfo.php | 101 +++++ .../PropertyReadInfoExtractorInterface.php | 29 ++ .../PropertyInfo/PropertyWriteInfo.php | 133 +++++++ .../PropertyWriteInfoExtractorInterface.php | 29 ++ .../Extractor/ReflectionExtractorTest.php | 121 +++++- .../Tests/Fixtures/Php71Dummy.php | 4 + 9 files changed, 799 insertions(+), 326 deletions(-) create mode 100644 src/Symfony/Component/PropertyInfo/PropertyReadInfo.php create mode 100644 src/Symfony/Component/PropertyInfo/PropertyReadInfoExtractorInterface.php create mode 100644 src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php create mode 100644 src/Symfony/Component/PropertyInfo/PropertyWriteInfoExtractorInterface.php diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index e4626aa72fd0b..9fc955efd309e 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -17,12 +17,16 @@ use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; -use Symfony\Component\Inflector\Inflector; use Symfony\Component\PropertyAccess\Exception\AccessException; use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyReadInfo; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfo; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; /** * Default implementation of {@link PropertyAccessorInterface}. @@ -36,17 +40,6 @@ class PropertyAccessor implements PropertyAccessorInterface private const VALUE = 0; private const REF = 1; private const IS_REF_CHAINED = 2; - private const ACCESS_HAS_PROPERTY = 0; - private const ACCESS_TYPE = 1; - private const ACCESS_NAME = 2; - private const ACCESS_REF = 3; - private const ACCESS_ADDER = 4; - private const ACCESS_REMOVER = 5; - private const ACCESS_TYPE_METHOD = 0; - private const ACCESS_TYPE_PROPERTY = 1; - private const ACCESS_TYPE_MAGIC = 2; - private const ACCESS_TYPE_ADDER_AND_REMOVER = 3; - private const ACCESS_TYPE_NOT_FOUND = 4; private const CACHE_PREFIX_READ = 'r'; private const CACHE_PREFIX_WRITE = 'w'; private const CACHE_PREFIX_PROPERTY_PATH = 'p'; @@ -64,6 +57,17 @@ class PropertyAccessor implements PropertyAccessorInterface private $cacheItemPool; private $propertyPathCache = []; + + /** + * @var PropertyReadInfoExtractorInterface + */ + private $readInfoExtractor; + + /** + * @var PropertyWriteInfoExtractorInterface + */ + private $writeInfoExtractor; + private $readPropertyCache = []; private $writePropertyCache = []; private static $resultProto = [self::VALUE => null]; @@ -78,6 +82,13 @@ public function __construct(bool $magicCall = false, bool $throwExceptionOnInval $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex; $this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value $this->ignoreInvalidProperty = !$throwExceptionOnInvalidPropertyPath; + $this->readInfoExtractor = $this->writeInfoExtractor = new ReflectionExtractor( + ['set'], + ['get', 'is', 'has', 'can'], + ['add', 'remove'], + false, + ReflectionExtractor::ALLOW_PUBLIC + ); } /** @@ -376,17 +387,22 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid $result = self::$resultProto; $object = $zval[self::VALUE]; - $access = $this->getReadAccessInfo(\get_class($object), $property); + $class = \get_class($object); + $access = $this->getReadInfo($class, $property); + + if (null !== $access) { + if (PropertyReadInfo::TYPE_METHOD === $access->getType()) { + $result[self::VALUE] = $object->{$access->getName()}(); + } - 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]) { - $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}; + if (PropertyReadInfo::TYPE_PROPERTY === $access->getType()) { + $result[self::VALUE] = $object->{$access->getName()}; - if ($access[self::ACCESS_REF] && isset($zval[self::REF])) { - $result[self::REF] = &$object->{$access[self::ACCESS_NAME]}; + if (isset($zval[self::REF]) && $access->canBeReference()) { + $result[self::REF] = &$object->{$access->getName()}; + } } - } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { + } elseif ($object instanceof \stdClass && property_exists($object, $property)) { // Needed to support \stdClass instances. We need to explicitly // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if // a *protected* property was found on the class, property_exists() @@ -397,11 +413,12 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid if (isset($zval[self::REF])) { $result[self::REF] = &$object->$property; } - } 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]}(); } elseif (!$ignoreInvalidProperty) { - throw new NoSuchPropertyException($access[self::ACCESS_NAME]); + throw new NoSuchPropertyException(sprintf( + 'Can get a way to read the property "%s" in class "%s".', + $property, + $class + )); } // Objects are always passed around by reference @@ -415,7 +432,7 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid /** * Guesses how to read the property value. */ - private function getReadAccessInfo(string $class, string $property): array + private function getReadInfo(string $class, string $property): ?PropertyReadInfo { $key = str_replace('\\', '.', $class).'..'.$property; @@ -430,65 +447,17 @@ private function getReadAccessInfo(string $class, string $property): array } } - $access = []; - - $reflClass = new \ReflectionClass($class); - $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; - $canAccessor = 'can'.$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($canAccessor) && $reflClass->getMethod($canAccessor)->isPublic()) { - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; - $access[self::ACCESS_NAME] = $canAccessor; - } 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 ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) { - $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 = [$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 - ); - } + $accessor = $this->readInfoExtractor->getReadInfo($class, $property, [ + 'enable_getter_setter_extraction' => true, + 'enable_magic_call_extraction' => $this->magicCall, + 'enable_constructor_extraction' => false, + ]); if (isset($item)) { - $this->cacheItemPool->save($item->set($access)); + $this->cacheItemPool->save($item->set($accessor)); } - return $this->readPropertyCache[$key] = $access; + return $this->readPropertyCache[$key] = $accessor; } /** @@ -522,15 +491,22 @@ private function writeProperty(array $zval, string $property, $value) } $object = $zval[self::VALUE]; - $access = $this->getWriteAccessInfo(\get_class($object), $property, $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]) { - $this->writeCollection($zval, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]); - } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { + $class = \get_class($object); + $mutator = $this->getWriteInfo($class, $property, $value); + + if (null !== $mutator) { + if (PropertyWriteInfo::TYPE_METHOD === $mutator->getType()) { + $object->{$mutator->getName()}($value); + } + + if (PropertyWriteInfo::TYPE_PROPERTY === $mutator->getType()) { + $object->{$mutator->getName()} = $value; + } + + if (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $mutator->getType()) { + $this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo()); + } + } elseif ($object instanceof \stdClass && property_exists($object, $property)) { // Needed to support \stdClass instances. We need to explicitly // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if // a *protected* property was found on the class, property_exists() @@ -538,19 +514,21 @@ private function writeProperty(array $zval, string $property, $value) // fatal error. $object->$property = $value; - } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { - $object->{$access[self::ACCESS_NAME]}($value); - } elseif (self::ACCESS_TYPE_NOT_FOUND === $access[self::ACCESS_TYPE]) { - throw new NoSuchPropertyException(sprintf('Could not determine access type for property "%s" in class "%s"%s', $property, \get_class($object), isset($access[self::ACCESS_NAME]) ? ': '.$access[self::ACCESS_NAME] : '.')); } else { - throw new NoSuchPropertyException($access[self::ACCESS_NAME]); + throw new NoSuchPropertyException(sprintf('Could not determine access type for property "%s" in class "%s".', $property, \get_class($object))); } } /** * 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 iterable $collection The collection to write + * @param PropertyWriteInfo $addMethod The add*() method + * @param PropertyWriteInfo $removeMethod The remove*() method */ - private function writeCollection(array $zval, string $property, iterable $collection, string $addMethod, string $removeMethod) + private function writeCollection(array $zval, string $property, iterable $collection, PropertyWriteInfo $addMethod, PropertyWriteInfo $removeMethod) { // At this point the add and remove methods have been found $previousValue = $this->readProperty($zval, $property); @@ -566,7 +544,7 @@ private function writeCollection(array $zval, string $property, iterable $collec foreach ($previousValue as $key => $item) { if (!\in_array($item, $collection, true)) { unset($previousValue[$key]); - $zval[self::VALUE]->{$removeMethod}($item); + $zval[self::VALUE]->{$removeMethod->getName()}($item); } } } else { @@ -575,17 +553,12 @@ private function writeCollection(array $zval, string $property, iterable $collec foreach ($collection as $item) { if (!$previousValue || !\in_array($item, $previousValue, true)) { - $zval[self::VALUE]->{$addMethod}($item); + $zval[self::VALUE]->{$addMethod->getName()}($item); } } } - /** - * Guesses how to write the property value. - * - * @param mixed $value - */ - private function getWriteAccessInfo(string $class, string $property, $value): array + private function getWriteInfo(string $class, string $property, $value): ?PropertyWriteInfo { $useAdderAndRemover = \is_array($value) || $value instanceof \Traversable; $key = str_replace('\\', '.', $class).'..'.$property.'..'.(int) $useAdderAndRemover; @@ -601,124 +574,18 @@ private function getWriteAccessInfo(string $class, string $property, $value): ar } } - $access = []; - - $reflClass = new \ReflectionClass($class); - $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); - $camelized = $this->camelize($property); - $singulars = (array) Inflector::singularize($camelized); - $errors = []; - - if ($useAdderAndRemover) { - foreach ($this->findAdderAndRemover($reflClass, $singulars) as $methods) { - if (3 === \count($methods)) { - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; - $access[self::ACCESS_ADDER] = $methods[self::ACCESS_ADDER]; - $access[self::ACCESS_REMOVER] = $methods[self::ACCESS_REMOVER]; - break; - } - - if (isset($methods[self::ACCESS_ADDER])) { - $errors[] = sprintf('The add method "%s" in class "%s" was found, but the corresponding remove method "%s" was not found', $methods['methods'][self::ACCESS_ADDER], $reflClass->name, $methods['methods'][self::ACCESS_REMOVER]); - } - - if (isset($methods[self::ACCESS_REMOVER])) { - $errors[] = sprintf('The remove method "%s" in class "%s" was found, but the corresponding add method "%s" was not found', $methods['methods'][self::ACCESS_REMOVER], $reflClass->name, $methods['methods'][self::ACCESS_ADDER]); - } - } - } - - if (!isset($access[self::ACCESS_TYPE])) { - $setter = 'set'.$camelized; - $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; - } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) { - $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; - } else { - foreach ($this->findAdderAndRemover($reflClass, $singulars) as $methods) { - if (3 === \count($methods)) { - $errors[] = 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[self::ACCESS_ADDER], $methods[self::ACCESS_REMOVER]]), - \is_object($value) ? \get_class($value) : \gettype($value) - ); - } - } - - if (!isset($access[self::ACCESS_NAME])) { - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; - - $triedMethods = [ - $setter => 1, - $getsetter => 1, - '__set' => 2, - '__call' => 2, - ]; - - foreach ($singulars as $singular) { - $triedMethods['add'.$singular] = 1; - $triedMethods['remove'.$singular] = 1; - } - - foreach ($triedMethods as $methodName => $parameters) { - if (!$reflClass->hasMethod($methodName)) { - continue; - } - - $method = $reflClass->getMethod($methodName); - - if (!$method->isPublic()) { - $errors[] = sprintf('The method "%s" in class "%s" was found but does not have public access', $methodName, $reflClass->name); - continue; - } - - if ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) { - $errors[] = sprintf('The method "%s" in class "%s" requires %d arguments, but should accept only %d', $methodName, $reflClass->name, $method->getNumberOfRequiredParameters(), $parameters); - } - } - - if (\count($errors)) { - $access[self::ACCESS_NAME] = implode('. ', $errors).'.'; - } else { - $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 - ); - } - } - } - } + $mutator = $this->writeInfoExtractor->getWriteInfo($class, $property, [ + 'enable_getter_setter_extraction' => true, + 'enable_magic_call_extraction' => $this->magicCall, + 'enable_constructor_extraction' => false, + 'enable_adder_remover_extraction' => $useAdderAndRemover, + ]); if (isset($item)) { - $this->cacheItemPool->save($item->set($access)); + $this->cacheItemPool->save($item->set($mutator)); } - return $this->writePropertyCache[$key] = $access; + return $this->writePropertyCache[$key] = $mutator; } /** @@ -732,79 +599,15 @@ private function isPropertyWritable($object, string $property): bool return false; } - $access = $this->getWriteAccessInfo(\get_class($object), $property, []); + $mutatorForArray = $this->getWriteInfo(\get_class($object), $property, []); - $isWritable = 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]; - - if ($isWritable) { + if (null !== $mutatorForArray || ($object instanceof \stdClass && property_exists($object, $property))) { return true; } - $access = $this->getWriteAccessInfo(\get_class($object), $property, ''); - - 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]; - } - - /** - * Camelizes a given string. - */ - private function camelize(string $string): string - { - return str_replace(' ', '', ucwords(str_replace('_', ' ', $string))); - } - - /** - * Searches for add and remove methods. - */ - private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars): iterable - { - foreach ($singulars as $singular) { - $addMethod = 'add'.$singular; - $removeMethod = 'remove'.$singular; - $result = ['methods' => [self::ACCESS_ADDER => $addMethod, self::ACCESS_REMOVER => $removeMethod]]; - - $addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1); - - if ($addMethodFound) { - $result[self::ACCESS_ADDER] = $addMethod; - } - - $removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1); - - if ($removeMethodFound) { - $result[self::ACCESS_REMOVER] = $removeMethod; - } - - yield $result; - } - - return null; - } - - /** - * Returns whether a method is public and has the number of required parameters. - */ - private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): bool - { - if ($class->hasMethod($methodName)) { - $method = $class->getMethod($methodName); - - if ($method->isPublic() - && $method->getNumberOfRequiredParameters() <= $parameters - && $method->getNumberOfParameters() >= $parameters) { - return true; - } - } + $mutator = $this->getWriteInfo(\get_class($object), $property, ''); - return false; + return null !== $mutator || ($object instanceof \stdClass && property_exists($object, $property)); } /** diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index 91c41273a7084..a423c79e30f75 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -17,7 +17,8 @@ ], "require": { "php": "^7.2.5", - "symfony/inflector": "^4.4|^5.0" + "symfony/inflector": "^4.4|^5.0", + "symfony/property-info": "^4.4|^5.0" }, "require-dev": { "symfony/cache": "^4.4|^5.0" diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index b62dd25a75d09..33d77c3ef6ace 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -15,7 +15,11 @@ use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfo; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfo; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; use Symfony\Component\PropertyInfo\Type; /** @@ -25,7 +29,7 @@ * * @final */ -class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface +class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface { /** * @internal @@ -56,7 +60,8 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp private $accessorPrefixes; private $arrayMutatorPrefixes; private $enableConstructorExtraction; - private $accessFlags; + private $methodReflectionFlags; + private $propertyReflectionFlags; /** * @param string[]|null $mutatorPrefixes @@ -69,7 +74,8 @@ public function __construct(array $mutatorPrefixes = null, array $accessorPrefix $this->accessorPrefixes = null !== $accessorPrefixes ? $accessorPrefixes : self::$defaultAccessorPrefixes; $this->arrayMutatorPrefixes = null !== $arrayMutatorPrefixes ? $arrayMutatorPrefixes : self::$defaultArrayMutatorPrefixes; $this->enableConstructorExtraction = $enableConstructorExtraction; - $this->accessFlags = $accessFlags; + $this->methodReflectionFlags = $this->getMethodsFlags($accessFlags); + $this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags); } /** @@ -83,34 +89,16 @@ public function getProperties(string $class, array $context = []): ?array return null; } - $propertyFlags = 0; - $methodFlags = 0; - - if ($this->accessFlags & self::ALLOW_PUBLIC) { - $propertyFlags = $propertyFlags | \ReflectionProperty::IS_PUBLIC; - $methodFlags = $methodFlags | \ReflectionMethod::IS_PUBLIC; - } - - if ($this->accessFlags & self::ALLOW_PRIVATE) { - $propertyFlags = $propertyFlags | \ReflectionProperty::IS_PRIVATE; - $methodFlags = $methodFlags | \ReflectionMethod::IS_PRIVATE; - } - - if ($this->accessFlags & self::ALLOW_PROTECTED) { - $propertyFlags = $propertyFlags | \ReflectionProperty::IS_PROTECTED; - $methodFlags = $methodFlags | \ReflectionMethod::IS_PROTECTED; - } - $reflectionProperties = $reflectionClass->getProperties(); $properties = []; foreach ($reflectionProperties as $reflectionProperty) { - if ($reflectionProperty->getModifiers() & $propertyFlags) { + if ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags) { $properties[$reflectionProperty->name] = $reflectionProperty->name; } } - foreach ($reflectionClass->getMethods($methodFlags) as $reflectionMethod) { + foreach ($reflectionClass->getMethods($this->methodReflectionFlags) as $reflectionMethod) { if ($reflectionMethod->isStatic()) { continue; } @@ -176,9 +164,7 @@ public function isReadable(string $class, string $property, array $context = []) return true; } - list($reflectionMethod) = $this->getAccessorMethod($class, $property); - - return null !== $reflectionMethod; + return null !== $this->getReadInfo($class, $property, $context); } /** @@ -223,6 +209,135 @@ public function isInitializable(string $class, string $property, array $context return false; } + /** + * {@inheritdoc} + */ + public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo + { + try { + $reflClass = new \ReflectionClass($class); + } catch (\ReflectionException $e) { + return null; + } + + $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false; + $allowMagicCall = $context['enable_magic_call_extraction'] ?? false; + + $hasProperty = $reflClass->hasProperty($property); + $camelProp = $this->camelize($property); + $getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item) + + foreach ($this->accessorPrefixes as $prefix) { + $methodName = $prefix.$camelProp; + + if ($reflClass->hasMethod($methodName) && ($reflClass->getMethod($methodName)->getModifiers() & $this->methodReflectionFlags)) { + $method = $reflClass->getMethod($methodName); + + return PropertyReadInfo::forMethod($methodName, $this->getReadVisiblityForMethod($method), $method->isStatic()); + } + } + + if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($reflClass->getMethod($getsetter)->getModifiers() & $this->methodReflectionFlags)) { + $method = $reflClass->getMethod($getsetter); + + return PropertyReadInfo::forMethod($getsetter, $this->getReadVisiblityForMethod($method), $method->isStatic()); + } + + if ($hasProperty && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { + $reflProperty = $reflClass->getProperty($property); + + return PropertyReadInfo::forProperty($property, $this->getReadVisiblityForProperty($reflProperty), $reflProperty->isStatic(), true); + } + + if ($reflClass->hasMethod('__get') && ($reflClass->getMethod('__get')->getModifiers() & $this->methodReflectionFlags)) { + return PropertyReadInfo::forProperty($property, PropertyReadInfo::VISIBILITY_PUBLIC, false, false); + } + + if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) { + return PropertyReadInfo::forMethod('get'.$camelProp, PropertyReadInfo::VISIBILITY_PUBLIC, false); + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo + { + try { + $reflClass = new \ReflectionClass($class); + } catch (\ReflectionException $e) { + return null; + } + + $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false; + $allowMagicCall = $context['enable_magic_call_extraction'] ?? false; + $allowConstruct = $context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction; + $allowAdderRemover = $context['enable_adder_remover_extraction'] ?? true; + + $camelized = $this->camelize($property); + $constructor = $reflClass->getConstructor(); + + if (null !== $constructor && $allowConstruct) { + foreach ($constructor->getParameters() as $parameter) { + if ($parameter->getName() === $property) { + return PropertyWriteInfo::forConstructor($property); + } + } + } + + if ($allowAdderRemover && null !== $methods = $this->findAdderAndRemover($reflClass, (array) Inflector::singularize($camelized))) { + [$adderAccessName, $removerAccessName] = $methods; + + $adderMethod = $reflClass->getMethod($adderAccessName); + $removerMethod = $reflClass->getMethod($removerAccessName); + + return PropertyWriteInfo::forAdderAndRemover( + PropertyWriteInfo::forMethod($adderAccessName, $this->getWriteVisiblityForMethod($adderMethod), $adderMethod->isStatic()), + PropertyWriteInfo::forMethod($removerAccessName, $this->getWriteVisiblityForMethod($removerMethod), $removerMethod->isStatic()) + ); + } + + foreach ($this->mutatorPrefixes as $mutatorPrefix) { + $methodName = $mutatorPrefix.$camelized; + + if (!$this->isMethodAccessible($reflClass, $methodName, 1)) { + continue; + } + + $method = $reflClass->getMethod($methodName); + + if (!\in_array($mutatorPrefix, $this->arrayMutatorPrefixes, true)) { + return PropertyWriteInfo::forMethod($methodName, $this->getWriteVisiblityForMethod($method), $method->isStatic()); + } + } + + $getsetter = lcfirst($camelized); + + if ($allowGetterSetter && $this->isMethodAccessible($reflClass, $getsetter, 1)) { + $method = $reflClass->getMethod($getsetter); + + return PropertyWriteInfo::forMethod($getsetter, $this->getWriteVisiblityForMethod($method), $method->isStatic()); + } + + if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { + $reflProperty = $reflClass->getProperty($property); + + return PropertyWriteInfo::forProperty($property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic()); + } + + if ($this->isMethodAccessible($reflClass, '__set', 2)) { + return PropertyWriteInfo::forProperty($property, PropertyWriteInfo::VISIBILITY_PUBLIC, false); + } + + if ($allowMagicCall && $this->isMethodAccessible($reflClass, '__call', 2)) { + return PropertyWriteInfo::forMethod('set'.$camelized, PropertyWriteInfo::VISIBILITY_PUBLIC, false); + } + + return null; + } + /** * @return Type[]|null */ @@ -360,19 +475,7 @@ private function isAllowedProperty(string $class, string $property): bool try { $reflectionProperty = new \ReflectionProperty($class, $property); - if ($this->accessFlags & self::ALLOW_PUBLIC && $reflectionProperty->isPublic()) { - return true; - } - - if ($this->accessFlags & self::ALLOW_PROTECTED && $reflectionProperty->isProtected()) { - return true; - } - - if ($this->accessFlags & self::ALLOW_PRIVATE && $reflectionProperty->isPrivate()) { - return true; - } - - return false; + return $reflectionProperty->getModifiers() & $this->propertyReflectionFlags; } catch (\ReflectionException $e) { // Return false if the property doesn't exist } @@ -465,4 +568,155 @@ private function getPropertyName(string $methodName, array $reflectionProperties return null; } + + /** + * Searches for add and remove methods. + * + * @param \ReflectionClass $reflClass The reflection class for the given object + * @param array $singulars The singular form of the property name or null + * + * @return array|null An array containing the adder and remover when found, null otherwise + */ + private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) + { + if (!\is_array($this->arrayMutatorPrefixes) && 2 !== \count($this->arrayMutatorPrefixes)) { + return null; + } + + [$addPrefix, $removePrefix] = $this->arrayMutatorPrefixes; + + foreach ($singulars as $singular) { + $addMethod = $addPrefix.$singular; + $removeMethod = $removePrefix.$singular; + + $addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1); + $removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1); + + if ($addMethodFound && $removeMethodFound) { + return [$addMethod, $removeMethod]; + } + } + } + + /** + * Returns whether a method is public and has the number of required parameters. + */ + private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): bool + { + if ($class->hasMethod($methodName)) { + $method = $class->getMethod($methodName); + + if (($method->getModifiers() & $this->methodReflectionFlags) + && $method->getNumberOfRequiredParameters() <= $parameters + && $method->getNumberOfParameters() >= $parameters) { + return true; + } + } + + return false; + } + + /** + * Camelizes a given string. + */ + private function camelize(string $string): string + { + return str_replace(' ', '', ucwords(str_replace('_', ' ', $string))); + } + + /** + * Return allowed reflection method flags. + */ + private function getMethodsFlags(int $accessFlags): int + { + $methodFlags = 0; + + if ($accessFlags & self::ALLOW_PUBLIC) { + $methodFlags |= \ReflectionMethod::IS_PUBLIC; + } + + if ($accessFlags & self::ALLOW_PRIVATE) { + $methodFlags |= \ReflectionMethod::IS_PRIVATE; + } + + if ($accessFlags & self::ALLOW_PROTECTED) { + $methodFlags |= \ReflectionMethod::IS_PROTECTED; + } + + return $methodFlags; + } + + /** + * Return allowed reflection property flags. + */ + private function getPropertyFlags(int $accessFlags): int + { + $propertyFlags = 0; + + if ($accessFlags & self::ALLOW_PUBLIC) { + $propertyFlags |= \ReflectionProperty::IS_PUBLIC; + } + + if ($accessFlags & self::ALLOW_PRIVATE) { + $propertyFlags |= \ReflectionProperty::IS_PRIVATE; + } + + if ($accessFlags & self::ALLOW_PROTECTED) { + $propertyFlags |= \ReflectionProperty::IS_PROTECTED; + } + + return $propertyFlags; + } + + private function getReadVisiblityForProperty(\ReflectionProperty $reflectionProperty): string + { + if ($reflectionProperty->isPrivate()) { + return PropertyReadInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionProperty->isProtected()) { + return PropertyReadInfo::VISIBILITY_PROTECTED; + } + + return PropertyReadInfo::VISIBILITY_PUBLIC; + } + + private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod): string + { + if ($reflectionMethod->isPrivate()) { + return PropertyReadInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionMethod->isProtected()) { + return PropertyReadInfo::VISIBILITY_PROTECTED; + } + + return PropertyReadInfo::VISIBILITY_PUBLIC; + } + + private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string + { + if ($reflectionProperty->isPrivate()) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionProperty->isProtected()) { + return PropertyWriteInfo::VISIBILITY_PROTECTED; + } + + return PropertyWriteInfo::VISIBILITY_PUBLIC; + } + + private function getWriteVisiblityForMethod(\ReflectionMethod $reflectionMethod): string + { + if ($reflectionMethod->isPrivate()) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionMethod->isProtected()) { + return PropertyWriteInfo::VISIBILITY_PROTECTED; + } + + return PropertyWriteInfo::VISIBILITY_PUBLIC; + } } diff --git a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php new file mode 100644 index 0000000000000..4ec0f3ef76d22 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * The property read info tells how a property can be read. + * + * @author Joel Wurtz + * + * @internal + */ +final class PropertyReadInfo +{ + public const TYPE_METHOD = 'method'; + public const TYPE_PROPERTY = 'property'; + + public const VISIBILITY_PUBLIC = 'public'; + public const VISIBILITY_PROTECTED = 'protected'; + public const VISIBILITY_PRIVATE = 'private'; + + private $type; + + private $name; + + private $visibility; + + private $static; + + private $byRef; + + private function __construct() + { + } + + /** + * Get type of access. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get name of the access, which can be a method name or a property name, depending on the type. + */ + public function getName(): string + { + return $this->name; + } + + public function getVisibility(): string + { + return $this->visibility; + } + + public function isStatic(): bool + { + return $this->static; + } + + /** + * Whether this accessor can be accessed by reference. + */ + public function canBeReference(): bool + { + return $this->byRef; + } + + public static function forProperty(string $propertyName, string $visibility, bool $static, bool $byRef): self + { + $accessor = new self(); + $accessor->type = self::TYPE_PROPERTY; + $accessor->name = $propertyName; + $accessor->visibility = $visibility; + $accessor->static = $static; + $accessor->byRef = $byRef; + + return $accessor; + } + + public static function forMethod(string $methodName, string $visibility, bool $static): self + { + $accessor = new self(); + $accessor->type = self::TYPE_METHOD; + $accessor->name = $methodName; + $accessor->visibility = $visibility; + $accessor->static = $static; + $accessor->byRef = false; + + return $accessor; + } +} diff --git a/src/Symfony/Component/PropertyInfo/PropertyReadInfoExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyReadInfoExtractorInterface.php new file mode 100644 index 0000000000000..2c152c0f78607 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyReadInfoExtractorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Extract read information for the property of a class. + * + * @author Joel Wurtz + * + * @internal + */ +interface PropertyReadInfoExtractorInterface +{ + /** + * Get read information object for a given property of a class. + * + * @internal + */ + public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo; +} diff --git a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php new file mode 100644 index 0000000000000..4a3f8d380d8de --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * The write mutator defines how a property can be written. + * + * @author Joel Wurtz + * + * @internal + */ +final class PropertyWriteInfo +{ + public const TYPE_METHOD = 'method'; + public const TYPE_PROPERTY = 'property'; + public const TYPE_ADDER_AND_REMOVER = 'adder_and_remover'; + public const TYPE_CONSTRUCTOR = 'constructor'; + + public const VISIBILITY_PUBLIC = 'public'; + public const VISIBILITY_PROTECTED = 'protected'; + public const VISIBILITY_PRIVATE = 'private'; + + private $type; + private $name; + private $visibility; + private $static; + private $adderInfo; + private $removerInfo; + + private function __construct() + { + } + + public function getType(): string + { + return $this->type; + } + + public function getName(): string + { + if (null === $this->name) { + throw new \LogicException("Calling getName() when having a mutator of type {$this->type} is not tolerated"); + } + + return $this->name; + } + + public function getAdderInfo(): self + { + if (null === $this->adderInfo) { + throw new \LogicException("Calling getAdderInfo() when having a mutator of type {$this->type} is not tolerated"); + } + + return $this->adderInfo; + } + + public function getRemoverInfo(): self + { + if (null === $this->removerInfo) { + throw new \LogicException("Calling getRemoverInfo() when having a mutator of type {$this->type} is not tolerated"); + } + + return $this->removerInfo; + } + + public function getVisibility(): string + { + if (null === $this->visibility) { + throw new \LogicException("Calling getVisibility() when having a mutator of type {$this->type} is not tolerated"); + } + + return $this->visibility; + } + + public function isStatic(): bool + { + if (null === $this->static) { + throw new \LogicException("Calling isStatic() when having a mutator of type {$this->type} is not tolerated"); + } + + return $this->static; + } + + public static function forMethod(string $methodName, string $visibility, bool $static): self + { + $mutator = new self(); + $mutator->type = self::TYPE_METHOD; + $mutator->name = $methodName; + $mutator->visibility = $visibility; + $mutator->static = $static; + + return $mutator; + } + + public static function forProperty(string $propertyName, string $visibility, bool $static): self + { + $mutator = new self(); + $mutator->type = self::TYPE_PROPERTY; + $mutator->name = $propertyName; + $mutator->visibility = $visibility; + $mutator->static = $static; + + return $mutator; + } + + public static function forAdderAndRemover(self $adder, self $remover): self + { + $mutator = new self(); + $mutator->type = self::TYPE_ADDER_AND_REMOVER; + $mutator->adderInfo = $adder; + $mutator->removerInfo = $remover; + + return $mutator; + } + + public static function forConstructor(string $propertyName): self + { + $mutator = new self(); + $mutator->type = self::TYPE_CONSTRUCTOR; + $mutator->name = $propertyName; + + return $mutator; + } +} diff --git a/src/Symfony/Component/PropertyInfo/PropertyWriteInfoExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyWriteInfoExtractorInterface.php new file mode 100644 index 0000000000000..ed1b1c860bbad --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyWriteInfoExtractorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Extract write information for the property of a class. + * + * @author Joel Wurtz + * + * @internal + */ +interface PropertyWriteInfoExtractorInterface +{ + /** + * Get write information object for a given property of a class. + * + * @internal + */ + public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo; +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index cf26b49b84e55..aa2e6c8405816 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -13,11 +13,14 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyReadInfo; +use Symfony\Component\PropertyInfo\PropertyWriteInfo; use Symfony\Component\PropertyInfo\Tests\Fixtures\AdderRemoverDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended2; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php74Dummy; use Symfony\Component\PropertyInfo\Type; @@ -272,7 +275,7 @@ public function testIsWritable($property, $expected) { $this->assertSame( $expected, - $this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property, []) + $this->extractor->isWritable(Dummy::class, $property, []) ); } @@ -367,6 +370,19 @@ public function constructorTypesProvider(): array ]; } + public function testNullOnPrivateProtectedAccessor() + { + $barAcessor = $this->extractor->getReadInfo(Dummy::class, 'bar'); + $barMutator = $this->extractor->getWriteInfo(Dummy::class, 'bar'); + $bazAcessor = $this->extractor->getReadInfo(Dummy::class, 'baz'); + $bazMutator = $this->extractor->getWriteInfo(Dummy::class, 'baz'); + + $this->assertNull($barAcessor); + $this->assertNull($barMutator); + $this->assertNull($bazAcessor); + $this->assertNull($bazMutator); + } + /** * @requires PHP 7.4 */ @@ -375,4 +391,107 @@ public function testTypedProperties(): void $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], $this->extractor->getTypes(Php74Dummy::class, 'dummy')); $this->assertEquals([new Type(Type::BUILTIN_TYPE_BOOL, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableBoolProp')); } + + /** + * @dataProvider readAccessorProvider + */ + public function testGetReadAccessor($class, $property, $found, $type, $name, $visibility, $static) + { + $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE); + $readAcessor = $extractor->getReadInfo($class, $property); + + if (!$found) { + $this->assertNull($readAcessor); + + return; + } + + $this->assertNotNull($readAcessor); + $this->assertSame($type, $readAcessor->getType()); + $this->assertSame($name, $readAcessor->getName()); + $this->assertSame($visibility, $readAcessor->getVisibility()); + $this->assertSame($static, $readAcessor->isStatic()); + } + + public function readAccessorProvider(): array + { + return [ + [Dummy::class, 'bar', true, PropertyReadInfo::TYPE_PROPERTY, 'bar', PropertyReadInfo::VISIBILITY_PRIVATE, false], + [Dummy::class, 'baz', true, PropertyReadInfo::TYPE_PROPERTY, 'baz', PropertyReadInfo::VISIBILITY_PROTECTED, false], + [Dummy::class, 'bal', true, PropertyReadInfo::TYPE_PROPERTY, 'bal', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Dummy::class, 'parent', true, PropertyReadInfo::TYPE_PROPERTY, 'parent', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Dummy::class, 'static', true, PropertyReadInfo::TYPE_METHOD, 'getStatic', PropertyReadInfo::VISIBILITY_PUBLIC, true], + [Dummy::class, 'foo', true, PropertyReadInfo::TYPE_PROPERTY, 'foo', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Php71Dummy::class, 'foo', true, PropertyReadInfo::TYPE_METHOD, 'getFoo', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Php71Dummy::class, 'buz', true, PropertyReadInfo::TYPE_METHOD, 'getBuz', PropertyReadInfo::VISIBILITY_PUBLIC, false], + ]; + } + + /** + * @dataProvider writeMutatorProvider + */ + public function testGetWriteMutator($class, $property, $allowConstruct, $found, $type, $name, $addName, $removeName, $visibility, $static) + { + $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE); + $writeMutator = $extractor->getWriteInfo($class, $property, [ + 'enable_constructor_extraction' => $allowConstruct, + 'enable_getter_setter_extraction' => true, + ]); + + if (!$found) { + $this->assertNull($writeMutator); + + return; + } + + $this->assertNotNull($writeMutator); + $this->assertSame($type, $writeMutator->getType()); + + if (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $writeMutator->getType()) { + $this->assertNotNull($writeMutator->getAdderInfo()); + $this->assertSame($addName, $writeMutator->getAdderInfo()->getName()); + $this->assertNotNull($writeMutator->getRemoverInfo()); + $this->assertSame($removeName, $writeMutator->getRemoverInfo()->getName()); + } + + if (PropertyWriteInfo::TYPE_CONSTRUCTOR === $writeMutator->getType()) { + $this->assertSame($name, $writeMutator->getName()); + } + + if (PropertyWriteInfo::TYPE_PROPERTY === $writeMutator->getType()) { + $this->assertSame($name, $writeMutator->getName()); + $this->assertSame($visibility, $writeMutator->getVisibility()); + $this->assertSame($static, $writeMutator->isStatic()); + } + + if (PropertyWriteInfo::TYPE_METHOD === $writeMutator->getType()) { + $this->assertSame($name, $writeMutator->getName()); + $this->assertSame($visibility, $writeMutator->getVisibility()); + $this->assertSame($static, $writeMutator->isStatic()); + } + } + + public function writeMutatorProvider(): array + { + return [ + [Dummy::class, 'bar', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'bar', null, null, PropertyWriteInfo::VISIBILITY_PRIVATE, false], + [Dummy::class, 'baz', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'baz', null, null, PropertyWriteInfo::VISIBILITY_PROTECTED, false], + [Dummy::class, 'bal', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'bal', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Dummy::class, 'parent', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'parent', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Dummy::class, 'staticSetter', false, true, PropertyWriteInfo::TYPE_METHOD, 'staticSetter', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, true], + [Dummy::class, 'foo', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'foo', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71Dummy::class, 'bar', false, true, PropertyWriteInfo::TYPE_METHOD, 'setBar', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71Dummy::class, 'string', false, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71Dummy::class, 'string', true, true, PropertyWriteInfo::TYPE_CONSTRUCTOR, 'string', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71Dummy::class, 'baz', false, true, PropertyWriteInfo::TYPE_ADDER_AND_REMOVER, null, 'addBaz', 'removeBaz', PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71DummyExtended::class, 'bar', false, true, PropertyWriteInfo::TYPE_METHOD, 'setBar', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71DummyExtended::class, 'string', false, false, -1, '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71DummyExtended::class, 'string', true, true, PropertyWriteInfo::TYPE_CONSTRUCTOR, 'string', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71DummyExtended::class, 'baz', false, true, PropertyWriteInfo::TYPE_ADDER_AND_REMOVER, null, 'addBaz', 'removeBaz', PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71DummyExtended2::class, 'bar', false, true, PropertyWriteInfo::TYPE_METHOD, 'setBar', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71DummyExtended2::class, 'string', false, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71DummyExtended2::class, 'string', true, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php71DummyExtended2::class, 'baz', false, true, PropertyWriteInfo::TYPE_ADDER_AND_REMOVER, null, 'addBaz', 'removeBaz', PropertyWriteInfo::VISIBILITY_PUBLIC, false], + ]; + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php71Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php71Dummy.php index 80012f968d70f..59d0dbcb80532 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php71Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php71Dummy.php @@ -35,6 +35,10 @@ public function setBar(?int $bar) public function addBaz(string $baz) { } + + public function removeBaz(string $baz) + { + } } class Php71DummyExtended extends Php71Dummy From 0a92dab7532aef0f6cc1fd5f20509f291d8eec55 Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Mon, 6 Jan 2020 21:21:23 +0100 Subject: [PATCH 2/3] Rebase, fix tests, review & update CHANGELOG --- .../Component/PropertyAccess/CHANGELOG.md | 5 + .../PropertyAccess/PropertyAccessor.php | 81 +++++-------- .../Tests/PropertyAccessorCollectionTest.php | 2 +- .../Tests/PropertyAccessorTest.php | 2 +- .../Component/PropertyAccess/composer.json | 2 +- .../Component/PropertyInfo/CHANGELOG.md | 5 + .../Extractor/ReflectionExtractor.php | 112 ++++++++++++------ .../PropertyInfo/PropertyReadInfo.php | 31 +---- .../PropertyReadInfoExtractorInterface.php | 4 - .../PropertyInfo/PropertyWriteInfo.php | 56 ++++----- .../PropertyWriteInfoExtractorInterface.php | 4 - .../Extractor/ReflectionExtractorTest.php | 6 +- 12 files changed, 151 insertions(+), 159 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index d733c4148187c..7a545752b5e96 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.1.0 +----- + +* Linking to PropertyInfo extractor to remove a lot of duplicate code + 4.4.0 ----- diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 9fc955efd309e..a082dde2dbe07 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -76,19 +76,14 @@ class PropertyAccessor implements PropertyAccessorInterface * Should not be used by application code. Use * {@link PropertyAccess::createPropertyAccessor()} instead. */ - public function __construct(bool $magicCall = false, bool $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null, bool $throwExceptionOnInvalidPropertyPath = true) + public function __construct(bool $magicCall = false, bool $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null, bool $throwExceptionOnInvalidPropertyPath = true, PropertyReadInfoExtractorInterface $readInfoExtractor = null, PropertyWriteInfoExtractorInterface $writeInfoExtractor = null) { $this->magicCall = $magicCall; $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex; $this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value $this->ignoreInvalidProperty = !$throwExceptionOnInvalidPropertyPath; - $this->readInfoExtractor = $this->writeInfoExtractor = new ReflectionExtractor( - ['set'], - ['get', 'is', 'has', 'can'], - ['add', 'remove'], - false, - ReflectionExtractor::ALLOW_PUBLIC - ); + $this->readInfoExtractor = $readInfoExtractor ?? new ReflectionExtractor([], null, null, false); + $this->writeInfoExtractor = $writeInfoExtractor ?? new ReflectionExtractor(['set'], null, null, false); } /** @@ -391,34 +386,25 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid $access = $this->getReadInfo($class, $property); if (null !== $access) { - if (PropertyReadInfo::TYPE_METHOD === $access->getType()) { - $result[self::VALUE] = $object->{$access->getName()}(); - } + $name = $access->getName(); + $type = $access->getType(); - if (PropertyReadInfo::TYPE_PROPERTY === $access->getType()) { - $result[self::VALUE] = $object->{$access->getName()}; + if (PropertyReadInfo::TYPE_METHOD === $type) { + $result[self::VALUE] = $object->$name(); + } elseif (PropertyReadInfo::TYPE_PROPERTY === $type) { + $result[self::VALUE] = $object->$name; if (isset($zval[self::REF]) && $access->canBeReference()) { - $result[self::REF] = &$object->{$access->getName()}; + $result[self::REF] = &$object->$name; } } } elseif ($object instanceof \stdClass && property_exists($object, $property)) { - // Needed to support \stdClass instances. We need to explicitly - // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if - // 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; if (isset($zval[self::REF])) { $result[self::REF] = &$object->$property; } } elseif (!$ignoreInvalidProperty) { - throw new NoSuchPropertyException(sprintf( - 'Can get a way to read the property "%s" in class "%s".', - $property, - $class - )); + throw new NoSuchPropertyException(sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class)); } // Objects are always passed around by reference @@ -494,39 +480,29 @@ private function writeProperty(array $zval, string $property, $value) $class = \get_class($object); $mutator = $this->getWriteInfo($class, $property, $value); - if (null !== $mutator) { - if (PropertyWriteInfo::TYPE_METHOD === $mutator->getType()) { - $object->{$mutator->getName()}($value); - } + if (PropertyWriteInfo::TYPE_NONE !== $mutator->getType()) { + $type = $mutator->getType(); - if (PropertyWriteInfo::TYPE_PROPERTY === $mutator->getType()) { + if (PropertyWriteInfo::TYPE_METHOD === $type) { + $object->{$mutator->getName()}($value); + } elseif (PropertyWriteInfo::TYPE_PROPERTY === $type) { $object->{$mutator->getName()} = $value; - } - - if (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $mutator->getType()) { + } elseif (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $type) { $this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo()); } } elseif ($object instanceof \stdClass && property_exists($object, $property)) { - // Needed to support \stdClass instances. We need to explicitly - // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if - // 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; - } else { + } elseif (!$this->ignoreInvalidProperty) { + if ($mutator->hasErrors()) { + throw new NoSuchPropertyException(implode('. ', $mutator->getErrors()).'.'); + } + throw new NoSuchPropertyException(sprintf('Could not determine access type for property "%s" in class "%s".', $property, \get_class($object))); } } /** * 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 iterable $collection The collection to write - * @param PropertyWriteInfo $addMethod The add*() method - * @param PropertyWriteInfo $removeMethod The remove*() method */ private function writeCollection(array $zval, string $property, iterable $collection, PropertyWriteInfo $addMethod, PropertyWriteInfo $removeMethod) { @@ -534,6 +510,9 @@ private function writeCollection(array $zval, string $property, iterable $collec $previousValue = $this->readProperty($zval, $property); $previousValue = $previousValue[self::VALUE]; + $removeMethodName = $removeMethod->getName(); + $addMethodName = $addMethod->getName(); + if ($previousValue instanceof \Traversable) { $previousValue = iterator_to_array($previousValue); } @@ -544,7 +523,7 @@ private function writeCollection(array $zval, string $property, iterable $collec foreach ($previousValue as $key => $item) { if (!\in_array($item, $collection, true)) { unset($previousValue[$key]); - $zval[self::VALUE]->{$removeMethod->getName()}($item); + $zval[self::VALUE]->$removeMethodName($item); } } } else { @@ -553,12 +532,12 @@ private function writeCollection(array $zval, string $property, iterable $collec foreach ($collection as $item) { if (!$previousValue || !\in_array($item, $previousValue, true)) { - $zval[self::VALUE]->{$addMethod->getName()}($item); + $zval[self::VALUE]->$addMethodName($item); } } } - private function getWriteInfo(string $class, string $property, $value): ?PropertyWriteInfo + private function getWriteInfo(string $class, string $property, $value): PropertyWriteInfo { $useAdderAndRemover = \is_array($value) || $value instanceof \Traversable; $key = str_replace('\\', '.', $class).'..'.$property.'..'.(int) $useAdderAndRemover; @@ -601,13 +580,13 @@ private function isPropertyWritable($object, string $property): bool $mutatorForArray = $this->getWriteInfo(\get_class($object), $property, []); - if (null !== $mutatorForArray || ($object instanceof \stdClass && property_exists($object, $property))) { + if (PropertyWriteInfo::TYPE_NONE !== $mutatorForArray->getType() || ($object instanceof \stdClass && property_exists($object, $property))) { return true; } $mutator = $this->getWriteInfo(\get_class($object), $property, ''); - return null !== $mutator || ($object instanceof \stdClass && property_exists($object, $property)); + return PropertyWriteInfo::TYPE_NONE !== $mutator->getType() || ($object instanceof \stdClass && property_exists($object, $property)); } /** diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index 09aebab87b135..18e51f33f275c 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -188,7 +188,7 @@ public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists() public function testSetValueFailsIfAdderAndRemoverExistButValueIsNotTraversable() { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException'); - $this->expectExceptionMessageRegExp('/Could not determine access type for property "axes" in class "Symfony\\\\Component\\\\PropertyAccess\\\\Tests\\\\PropertyAccessorCollectionTest_Car[^"]*": The property "axes" in class "Symfony\\\\Component\\\\PropertyAccess\\\\Tests\\\\PropertyAccessorCollectionTest_Car[^"]*" can be defined with the methods "addAxis\(\)", "removeAxis\(\)" but the new value must be an array or an instance of \\\\Traversable, "string" given./'); + $this->expectExceptionMessageRegExp('/The property "axes" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\PropertyAccessorCollectionTest_Car" can be defined with the methods "addAxis\(\)", "removeAxis\(\)" but the new value must be an array or an instance of \\\Traversable\./'); $car = new PropertyAccessorCollectionTest_Car(); $this->propertyAccessor->setValue($car, 'axes', 'Not an array or Traversable'); diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 218f18730f162..70c3b681b76a0 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -760,7 +760,7 @@ public function testRemoverWithoutAdder() public function testAdderAndRemoveNeedsTheExactParametersDefined() { $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException'); - $this->expectExceptionMessageRegExp('/.*The method "addFoo" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\Fixtures\\\TestAdderRemoverInvalidArgumentLength" requires 0 arguments, but should accept only 1\. The method "removeFoo" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\Fixtures\\\TestAdderRemoverInvalidArgumentLength" requires 2 arguments, but should accept only 1\./'); + $this->expectExceptionMessageRegExp('/.*The method "addFoo" in class "Symfony\\\Component\\\PropertyAccess\\\Tests\\\Fixtures\\\TestAdderRemoverInvalidArgumentLength" requires 0 arguments, but should accept only 1\./'); $object = new TestAdderRemoverInvalidArgumentLength(); $this->propertyAccessor->setValue($object, 'foo', [1, 2]); } diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index a423c79e30f75..411f8121d5fea 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.2.5", "symfony/inflector": "^4.4|^5.0", - "symfony/property-info": "^4.4|^5.0" + "symfony/property-info": "^5.1" }, "require-dev": { "symfony/cache": "^4.4|^5.0" diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index 19120c9f603b3..2925a37a94475 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.1.0 +----- + +* Add support for extracting accessor and mutator via PHP Reflection + 4.3.0 ----- diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 33d77c3ef6ace..29e4327e82f54 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -233,28 +233,28 @@ public function getReadInfo(string $class, string $property, array $context = [] if ($reflClass->hasMethod($methodName) && ($reflClass->getMethod($methodName)->getModifiers() & $this->methodReflectionFlags)) { $method = $reflClass->getMethod($methodName); - return PropertyReadInfo::forMethod($methodName, $this->getReadVisiblityForMethod($method), $method->isStatic()); + return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, $this->getReadVisiblityForMethod($method), $method->isStatic(), false); } } if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($reflClass->getMethod($getsetter)->getModifiers() & $this->methodReflectionFlags)) { $method = $reflClass->getMethod($getsetter); - return PropertyReadInfo::forMethod($getsetter, $this->getReadVisiblityForMethod($method), $method->isStatic()); + return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisiblityForMethod($method), $method->isStatic(), false); } if ($hasProperty && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { $reflProperty = $reflClass->getProperty($property); - return PropertyReadInfo::forProperty($property, $this->getReadVisiblityForProperty($reflProperty), $reflProperty->isStatic(), true); + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($reflProperty), $reflProperty->isStatic(), true); } if ($reflClass->hasMethod('__get') && ($reflClass->getMethod('__get')->getModifiers() & $this->methodReflectionFlags)) { - return PropertyReadInfo::forProperty($property, PropertyReadInfo::VISIBILITY_PUBLIC, false, false); + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, false); } if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) { - return PropertyReadInfo::forMethod('get'.$camelProp, PropertyReadInfo::VISIBILITY_PUBLIC, false); + return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, 'get'.$camelProp, PropertyReadInfo::VISIBILITY_PUBLIC, false, false); } return null; @@ -263,7 +263,7 @@ public function getReadInfo(string $class, string $property, array $context = [] /** * {@inheritdoc} */ - public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo + public function getWriteInfo(string $class, string $property, array $context = []): PropertyWriteInfo { try { $reflClass = new \ReflectionClass($class); @@ -278,64 +278,92 @@ public function getWriteInfo(string $class, string $property, array $context = [ $camelized = $this->camelize($property); $constructor = $reflClass->getConstructor(); + $singulars = (array) Inflector::singularize($camelized); + $errors = []; if (null !== $constructor && $allowConstruct) { foreach ($constructor->getParameters() as $parameter) { if ($parameter->getName() === $property) { - return PropertyWriteInfo::forConstructor($property); + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_CONSTRUCTOR, $property); } } } - if ($allowAdderRemover && null !== $methods = $this->findAdderAndRemover($reflClass, (array) Inflector::singularize($camelized))) { - [$adderAccessName, $removerAccessName] = $methods; - + [$adderAccessName, $removerAccessName, $adderAndRemoverErrors] = $this->findAdderAndRemover($reflClass, $singulars); + if ($allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) { $adderMethod = $reflClass->getMethod($adderAccessName); $removerMethod = $reflClass->getMethod($removerAccessName); - return PropertyWriteInfo::forAdderAndRemover( - PropertyWriteInfo::forMethod($adderAccessName, $this->getWriteVisiblityForMethod($adderMethod), $adderMethod->isStatic()), - PropertyWriteInfo::forMethod($removerAccessName, $this->getWriteVisiblityForMethod($removerMethod), $removerMethod->isStatic()) - ); + $mutator = new PropertyWriteInfo(PropertyWriteInfo::TYPE_ADDER_AND_REMOVER); + $mutator->setAdderInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $adderAccessName, $this->getWriteVisiblityForMethod($adderMethod), $adderMethod->isStatic())); + $mutator->setRemoverInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $removerAccessName, $this->getWriteVisiblityForMethod($removerMethod), $removerMethod->isStatic())); + + return $mutator; + } else { + $errors = array_merge($errors, $adderAndRemoverErrors); } foreach ($this->mutatorPrefixes as $mutatorPrefix) { $methodName = $mutatorPrefix.$camelized; - if (!$this->isMethodAccessible($reflClass, $methodName, 1)) { + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $methodName, 1); + if (!$accessible) { + $errors = array_merge($errors, $methodAccessibleErrors); continue; } $method = $reflClass->getMethod($methodName); if (!\in_array($mutatorPrefix, $this->arrayMutatorPrefixes, true)) { - return PropertyWriteInfo::forMethod($methodName, $this->getWriteVisiblityForMethod($method), $method->isStatic()); + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $methodName, $this->getWriteVisiblityForMethod($method), $method->isStatic()); } } $getsetter = lcfirst($camelized); - if ($allowGetterSetter && $this->isMethodAccessible($reflClass, $getsetter, 1)) { + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $getsetter, 1); + if ($allowGetterSetter && $accessible) { $method = $reflClass->getMethod($getsetter); - return PropertyWriteInfo::forMethod($getsetter, $this->getWriteVisiblityForMethod($method), $method->isStatic()); + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $getsetter, $this->getWriteVisiblityForMethod($method), $method->isStatic()); + } else { + $errors = array_merge($errors, $methodAccessibleErrors); } if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { $reflProperty = $reflClass->getProperty($property); - return PropertyWriteInfo::forProperty($property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic()); + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic()); } - if ($this->isMethodAccessible($reflClass, '__set', 2)) { - return PropertyWriteInfo::forProperty($property, PropertyWriteInfo::VISIBILITY_PUBLIC, false); + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__set', 2); + if ($accessible) { + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false); + } else { + $errors = array_merge($errors, $methodAccessibleErrors); } - if ($allowMagicCall && $this->isMethodAccessible($reflClass, '__call', 2)) { - return PropertyWriteInfo::forMethod('set'.$camelized, PropertyWriteInfo::VISIBILITY_PUBLIC, false); + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__call', 2); + if ($allowMagicCall && $accessible) { + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, 'set'.$camelized, PropertyWriteInfo::VISIBILITY_PUBLIC, false); + } else { + $errors = array_merge($errors, $methodAccessibleErrors); } - return null; + if (!$allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) { + $errors = array_merge($errors, [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', + $property, + $reflClass->getName(), + implode('()", "', [$adderAccessName, $removerAccessName]) + )]); + } + + $noneProperty = new PropertyWriteInfo(); + $noneProperty->setErrors($errors); + + return $noneProperty; } /** @@ -575,45 +603,57 @@ private function getPropertyName(string $methodName, array $reflectionProperties * @param \ReflectionClass $reflClass The reflection class for the given object * @param array $singulars The singular form of the property name or null * - * @return array|null An array containing the adder and remover when found, null otherwise + * @return array An array containing the adder and remover when found and errors */ - private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) + private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars): array { if (!\is_array($this->arrayMutatorPrefixes) && 2 !== \count($this->arrayMutatorPrefixes)) { return null; } [$addPrefix, $removePrefix] = $this->arrayMutatorPrefixes; + $errors = []; foreach ($singulars as $singular) { $addMethod = $addPrefix.$singular; $removeMethod = $removePrefix.$singular; - $addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1); - $removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1); + [$addMethodFound, $addMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $addMethod, 1); + [$removeMethodFound, $removeMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $removeMethod, 1); + $errors = array_merge($errors, $addMethodAccessibleErrors, $removeMethodAccessibleErrors); if ($addMethodFound && $removeMethodFound) { - return [$addMethod, $removeMethod]; + return [$addMethod, $removeMethod, []]; + } elseif ($addMethodFound && !$removeMethodFound) { + $errors[] = sprintf('The add method "%s" in class "%s" was found, but the corresponding remove method "%s" was not found', $addMethod, $reflClass->getName(), $removeMethod); + } elseif (!$addMethodFound && $removeMethodFound) { + $errors[] = sprintf('The remove method "%s" in class "%s" was found, but the corresponding add method "%s" was not found', $removeMethod, $reflClass->getName(), $addMethod); } } + + return [null, null, $errors]; } /** - * Returns whether a method is public and has the number of required parameters. + * Returns whether a method is public and has the number of required parameters and errors. */ - private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): bool + private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): array { + $errors = []; + if ($class->hasMethod($methodName)) { $method = $class->getMethod($methodName); - if (($method->getModifiers() & $this->methodReflectionFlags) - && $method->getNumberOfRequiredParameters() <= $parameters - && $method->getNumberOfParameters() >= $parameters) { - return true; + if (\ReflectionMethod::IS_PUBLIC === $this->methodReflectionFlags && !$method->isPublic()) { + $errors[] = sprintf('The method "%s" in class "%s" was found but does not have public access.', $methodName, $class->getName()); + } elseif ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) { + $errors[] = sprintf('The method "%s" in class "%s" requires %d arguments, but should accept only %d.', $methodName, $class->getName(), $method->getNumberOfRequiredParameters(), $parameters); + } else { + return [true, $errors]; } } - return false; + return [false, $errors]; } /** diff --git a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php index 4ec0f3ef76d22..ae10352444793 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php +++ b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php @@ -37,8 +37,13 @@ final class PropertyReadInfo private $byRef; - private function __construct() + public function __construct(string $type, string $name, string $visibility, bool $static, bool $byRef) { + $this->type = $type; + $this->name = $name; + $this->visibility = $visibility; + $this->static = $static; + $this->byRef = $byRef; } /** @@ -74,28 +79,4 @@ public function canBeReference(): bool { return $this->byRef; } - - public static function forProperty(string $propertyName, string $visibility, bool $static, bool $byRef): self - { - $accessor = new self(); - $accessor->type = self::TYPE_PROPERTY; - $accessor->name = $propertyName; - $accessor->visibility = $visibility; - $accessor->static = $static; - $accessor->byRef = $byRef; - - return $accessor; - } - - public static function forMethod(string $methodName, string $visibility, bool $static): self - { - $accessor = new self(); - $accessor->type = self::TYPE_METHOD; - $accessor->name = $methodName; - $accessor->visibility = $visibility; - $accessor->static = $static; - $accessor->byRef = false; - - return $accessor; - } } diff --git a/src/Symfony/Component/PropertyInfo/PropertyReadInfoExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyReadInfoExtractorInterface.php index 2c152c0f78607..816b2825d58b8 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyReadInfoExtractorInterface.php +++ b/src/Symfony/Component/PropertyInfo/PropertyReadInfoExtractorInterface.php @@ -15,15 +15,11 @@ * Extract read information for the property of a class. * * @author Joel Wurtz - * - * @internal */ interface PropertyReadInfoExtractorInterface { /** * Get read information object for a given property of a class. - * - * @internal */ public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo; } diff --git a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php index 4a3f8d380d8de..207003ea158b7 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php +++ b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php @@ -20,6 +20,7 @@ */ final class PropertyWriteInfo { + public const TYPE_NONE = 'none'; public const TYPE_METHOD = 'method'; public const TYPE_PROPERTY = 'property'; public const TYPE_ADDER_AND_REMOVER = 'adder_and_remover'; @@ -35,9 +36,14 @@ final class PropertyWriteInfo private $static; private $adderInfo; private $removerInfo; + private $errors = []; - private function __construct() + public function __construct(string $type = self::TYPE_NONE, string $name = null, string $visibility = null, bool $static = null) { + $this->type = $type; + $this->name = $name; + $this->visibility = $visibility; + $this->static = $static; } public function getType(): string @@ -54,6 +60,11 @@ public function getName(): string return $this->name; } + public function setAdderInfo(self $adderInfo): void + { + $this->adderInfo = $adderInfo; + } + public function getAdderInfo(): self { if (null === $this->adderInfo) { @@ -63,6 +74,11 @@ public function getAdderInfo(): self return $this->adderInfo; } + public function setRemoverInfo(self $removerInfo): void + { + $this->removerInfo = $removerInfo; + } + public function getRemoverInfo(): self { if (null === $this->removerInfo) { @@ -90,44 +106,18 @@ public function isStatic(): bool return $this->static; } - public static function forMethod(string $methodName, string $visibility, bool $static): self + public function setErrors(array $errors): void { - $mutator = new self(); - $mutator->type = self::TYPE_METHOD; - $mutator->name = $methodName; - $mutator->visibility = $visibility; - $mutator->static = $static; - - return $mutator; + $this->errors = $errors; } - public static function forProperty(string $propertyName, string $visibility, bool $static): self + public function getErrors(): array { - $mutator = new self(); - $mutator->type = self::TYPE_PROPERTY; - $mutator->name = $propertyName; - $mutator->visibility = $visibility; - $mutator->static = $static; - - return $mutator; - } - - public static function forAdderAndRemover(self $adder, self $remover): self - { - $mutator = new self(); - $mutator->type = self::TYPE_ADDER_AND_REMOVER; - $mutator->adderInfo = $adder; - $mutator->removerInfo = $remover; - - return $mutator; + return $this->errors; } - public static function forConstructor(string $propertyName): self + public function hasErrors(): bool { - $mutator = new self(); - $mutator->type = self::TYPE_CONSTRUCTOR; - $mutator->name = $propertyName; - - return $mutator; + return (bool) \count($this->errors); } } diff --git a/src/Symfony/Component/PropertyInfo/PropertyWriteInfoExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyWriteInfoExtractorInterface.php index ed1b1c860bbad..f113463818e60 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyWriteInfoExtractorInterface.php +++ b/src/Symfony/Component/PropertyInfo/PropertyWriteInfoExtractorInterface.php @@ -15,15 +15,11 @@ * Extract write information for the property of a class. * * @author Joel Wurtz - * - * @internal */ interface PropertyWriteInfoExtractorInterface { /** * Get write information object for a given property of a class. - * - * @internal */ public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index aa2e6c8405816..4f01159be28cf 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -378,9 +378,9 @@ public function testNullOnPrivateProtectedAccessor() $bazMutator = $this->extractor->getWriteInfo(Dummy::class, 'baz'); $this->assertNull($barAcessor); - $this->assertNull($barMutator); + $this->assertEquals(PropertyWriteInfo::TYPE_NONE, $barMutator->getType()); $this->assertNull($bazAcessor); - $this->assertNull($bazMutator); + $this->assertEquals(PropertyWriteInfo::TYPE_NONE, $bazMutator->getType()); } /** @@ -439,7 +439,7 @@ public function testGetWriteMutator($class, $property, $allowConstruct, $found, ]); if (!$found) { - $this->assertNull($writeMutator); + $this->assertEquals(PropertyWriteInfo::TYPE_NONE, $writeMutator->getType()); return; } From 328fada4a59e35f81060f535cdf09cb624333a61 Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Tue, 28 Jan 2020 10:19:08 +0100 Subject: [PATCH 3/3] [Serializer] Use PropertyAccessor to access / mutate values in AbstractObjectNormalizer --- .../PropertyAccess/PropertyAccessor.php | 11 +++-- .../PropertyAccessorBuilder.php | 17 +++++++- .../Normalizer/AbstractObjectNormalizer.php | 42 +++++++++++++------ .../Normalizer/GetSetMethodNormalizer.php | 42 ------------------- .../Normalizer/ObjectNormalizer.php | 38 +---------------- .../Normalizer/PropertyNormalizer.php | 42 ------------------- 6 files changed, 54 insertions(+), 138 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index a082dde2dbe07..d9c2b69b87c26 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -48,6 +48,10 @@ class PropertyAccessor implements PropertyAccessorInterface * @var bool */ private $magicCall; + /** + * @var bool + */ + private $staticCall; private $ignoreInvalidIndices; private $ignoreInvalidProperty; @@ -76,9 +80,10 @@ class PropertyAccessor implements PropertyAccessorInterface * Should not be used by application code. Use * {@link PropertyAccess::createPropertyAccessor()} instead. */ - public function __construct(bool $magicCall = false, bool $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null, bool $throwExceptionOnInvalidPropertyPath = true, PropertyReadInfoExtractorInterface $readInfoExtractor = null, PropertyWriteInfoExtractorInterface $writeInfoExtractor = null) + public function __construct(bool $magicCall = false, bool $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null, bool $throwExceptionOnInvalidPropertyPath = true, PropertyReadInfoExtractorInterface $readInfoExtractor = null, PropertyWriteInfoExtractorInterface $writeInfoExtractor = null, bool $staticCall = true) { $this->magicCall = $magicCall; + $this->staticCall = $staticCall; $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex; $this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value $this->ignoreInvalidProperty = !$throwExceptionOnInvalidPropertyPath; @@ -385,7 +390,7 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid $class = \get_class($object); $access = $this->getReadInfo($class, $property); - if (null !== $access) { + if (null !== $access && (!$access->isStatic() || $this->staticCall)) { $name = $access->getName(); $type = $access->getType(); @@ -480,7 +485,7 @@ private function writeProperty(array $zval, string $property, $value) $class = \get_class($object); $mutator = $this->getWriteInfo($class, $property, $value); - if (PropertyWriteInfo::TYPE_NONE !== $mutator->getType()) { + if (PropertyWriteInfo::TYPE_NONE !== $mutator->getType() && (!$mutator->isStatic() || $this->staticCall)) { $type = $mutator->getType(); if (PropertyWriteInfo::TYPE_METHOD === $type) { diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php index 94aa4ecc3535d..5757e9f762542 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php @@ -21,6 +21,7 @@ class PropertyAccessorBuilder { private $magicCall = false; + private $staticCall = true; private $throwExceptionOnInvalidIndex = false; private $throwExceptionOnInvalidPropertyPath = true; @@ -61,6 +62,20 @@ public function isMagicCallEnabled() return $this->magicCall; } + public function enableStaticCall(): self + { + $this->staticCall = true; + + return $this; + } + + public function disableStaticCall(): self + { + $this->staticCall = false; + + return $this; + } + /** * Enables exceptions when reading a non-existing index. * @@ -164,6 +179,6 @@ public function getCacheItemPool() */ public function getPropertyAccessor() { - return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool, $this->throwExceptionOnInvalidPropertyPath); + return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool, $this->throwExceptionOnInvalidPropertyPath, null, null, $this->staticCall); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 38b3498413554..20e9769869298 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -13,6 +13,8 @@ use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Encoder\JsonEncoder; @@ -93,15 +95,17 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer private $propertyTypeExtractor; private $typesCache = []; private $attributesCache = []; + private $discriminatorCache = []; private $objectClassResolver; + private $propertyAccessor; /** * @var ClassDiscriminatorResolverInterface|null */ protected $classDiscriminatorResolver; - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = []) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [], PropertyAccessorInterface $propertyAccessor = null) { parent::__construct($classMetadataFactory, $nameConverter, $defaultContext); @@ -118,6 +122,11 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory } $this->classDiscriminatorResolver = $classDiscriminatorResolver; $this->objectClassResolver = $objectClassResolver; + + $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessorBuilder() + ->disableExceptionOnInvalidPropertyPath() + ->disableStaticCall() + ->getPropertyAccessor(); } /** @@ -208,6 +217,25 @@ public function normalize($object, string $format = null, array $context = []) return $data; } + protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []) + { + $cacheKey = \get_class($object); + if (!\array_key_exists($cacheKey, $this->discriminatorCache)) { + $this->discriminatorCache[$cacheKey] = null; + if (null !== $this->classDiscriminatorResolver) { + $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object); + $this->discriminatorCache[$cacheKey] = null === $mapping ? null : $mapping->getTypeProperty(); + } + } + + return $attribute === $this->discriminatorCache[$cacheKey] ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) : $this->propertyAccessor->getValue($object, $attribute); + } + + protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []) + { + $this->propertyAccessor->setValue($object, $attribute, $value); + } + /** * {@inheritdoc} */ @@ -276,13 +304,6 @@ protected function getAttributes($object, string $format = null, array $context) */ abstract protected function extractAttributes(object $object, string $format = null, array $context = []); - /** - * Gets the attribute value. - * - * @return mixed - */ - abstract protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []); - /** * {@inheritdoc} */ @@ -343,11 +364,6 @@ public function denormalize($data, string $type, string $format = null, array $c return $object; } - /** - * Sets attribute value. - */ - abstract protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []); - /** * Validates the submitted data and denormalizes it. * diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index 676034278cf6e..fd324dfcb761b 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -117,46 +117,4 @@ protected function extractAttributes(object $object, string $format = null, arra return $attributes; } - - /** - * {@inheritdoc} - */ - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []) - { - $ucfirsted = ucfirst($attribute); - - $getter = 'get'.$ucfirsted; - if (\is_callable([$object, $getter])) { - return $object->$getter(); - } - - $isser = 'is'.$ucfirsted; - if (\is_callable([$object, $isser])) { - return $object->$isser(); - } - - $haser = 'has'.$ucfirsted; - if (\is_callable([$object, $haser])) { - return $object->$haser(); - } - - return null; - } - - /** - * {@inheritdoc} - */ - protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []) - { - $setter = 'set'.ucfirst($attribute); - $key = \get_class($object).':'.$setter; - - if (!isset(self::$setterAccessibleCache[$key])) { - self::$setterAccessibleCache[$key] = \is_callable([$object, $setter]) && !(new \ReflectionMethod($object, $setter))->isStatic(); - } - - if (self::$setterAccessibleCache[$key]) { - $object->$setter($value); - } - } } diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 81fe9c1166797..ad580a4ff088f 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; @@ -28,10 +27,6 @@ */ class ObjectNormalizer extends AbstractObjectNormalizer { - protected $propertyAccessor; - - private $discriminatorCache = []; - private $objectClassResolver; public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = []) @@ -40,9 +35,7 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.'); } - parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext); - - $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext, $propertyAccessor); $this->objectClassResolver = $objectClassResolver ?? function ($class) { return \is_object($class) ? \get_class($class) : $class; @@ -121,35 +114,6 @@ protected function extractAttributes(object $object, string $format = null, arra return array_keys($attributes); } - /** - * {@inheritdoc} - */ - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []) - { - $cacheKey = \get_class($object); - if (!\array_key_exists($cacheKey, $this->discriminatorCache)) { - $this->discriminatorCache[$cacheKey] = null; - if (null !== $this->classDiscriminatorResolver) { - $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object); - $this->discriminatorCache[$cacheKey] = null === $mapping ? null : $mapping->getTypeProperty(); - } - } - - return $attribute === $this->discriminatorCache[$cacheKey] ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) : $this->propertyAccessor->getValue($object, $attribute); - } - - /** - * {@inheritdoc} - */ - protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []) - { - try { - $this->propertyAccessor->setValue($object, $attribute, $value); - } catch (NoSuchPropertyException $exception) { - // Properties not found are ignored - } - } - /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 2d12b5ec5125b..5111a0df1e1bc 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -126,48 +126,6 @@ protected function extractAttributes(object $object, string $format = null, arra return $attributes; } - /** - * {@inheritdoc} - */ - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []) - { - try { - $reflectionProperty = $this->getReflectionProperty($object, $attribute); - } catch (\ReflectionException $reflectionException) { - return null; - } - - // Override visibility - if (!$reflectionProperty->isPublic()) { - $reflectionProperty->setAccessible(true); - } - - return $reflectionProperty->getValue($object); - } - - /** - * {@inheritdoc} - */ - protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []) - { - try { - $reflectionProperty = $this->getReflectionProperty($object, $attribute); - } catch (\ReflectionException $reflectionException) { - return; - } - - if ($reflectionProperty->isStatic()) { - return; - } - - // Override visibility - if (!$reflectionProperty->isPublic()) { - $reflectionProperty->setAccessible(true); - } - - $reflectionProperty->setValue($object, $value); - } - /** * @param string|object $classOrObject * pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy