From fc250863a8825f8f47a77ded13eff812bbf79f2c Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 26 Mar 2019 12:55:18 +0100 Subject: [PATCH 1/2] [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 e4626aa72fd0..9fc955efd309 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 91c41273a708..a423c79e30f7 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 b62dd25a75d0..33d77c3ef6ac 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 000000000000..4ec0f3ef76d2 --- /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 000000000000..2c152c0f7860 --- /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 000000000000..4a3f8d380d8d --- /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 000000000000..ed1b1c860bba --- /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 cf26b49b84e5..aa2e6c840581 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 80012f968d70..59d0dbcb8053 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/2] 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 d733c4148187..7a545752b5e9 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 9fc955efd309..a082dde2dbe0 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 09aebab87b13..18e51f33f275 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 218f18730f16..70c3b681b76a 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 a423c79e30f7..411f8121d5fe 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 19120c9f603b..2925a37a9447 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 33d77c3ef6ac..29e4327e82f5 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 4ec0f3ef76d2..ae1035244479 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 2c152c0f7860..816b2825d58b 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 4a3f8d380d8d..207003ea158b 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 ed1b1c860bba..f113463818e6 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 aa2e6c840581..4f01159be28c 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; }