/
githubmirror
/
symfony
Обзор
Документация
Войти
/
githubmirror
/
symfony
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
8.2
src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php
1 057 строк
42 KB
HypeMC
[PropertyInfo][Serializer] Enable using `#[WithAccessors]` with the serializer
11 авг 2026, 16:52
11 авг 2026, 16:52
3fca390
Код
Авторство
О чём код?
<?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\PropertyInfo\Extractor; use Symfony\Component\PropertyInfo\Attribute\WithAccessors; use Symfony\Component\PropertyInfo\Exception\MappingException; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyNameExtractorInterface; 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\String\Inflector\EnglishInflector; use Symfony\Component\String\Inflector\InflectorInterface; use Symfony\Component\TypeInfo\Exception\UnsupportedException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; use Symfony\Component\TypeInfo\TypeIdentifier; use Symfony\Component\TypeInfo\TypeResolver\ReflectionParameterTypeResolver; use Symfony\Component\TypeInfo\TypeResolver\ReflectionPropertyTypeResolver; use Symfony\Component\TypeInfo\TypeResolver\ReflectionReturnTypeResolver; use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver; use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface; /** * Extracts data using the reflection API. * * @author Kévin Dunglas <dunglas@gmail.com> * * @final */ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyNameExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface { /** * @internal */ public static array $defaultMutatorPrefixes = ['add', 'remove', 'set']; /** * @internal */ public static array $defaultAccessorPrefixes = ['get', 'is', 'has', 'can']; /** * @internal */ public static array $defaultArrayMutatorPrefixes = ['add', 'remove']; public const ALLOW_PRIVATE = 1; public const ALLOW_PROTECTED = 2; public const ALLOW_PUBLIC = 4; /** @var int Allow none of the magic methods */ public const DISALLOW_MAGIC_METHODS = 0; /** @var int Allow magic __get methods */ public const ALLOW_MAGIC_GET = 1 << 0; /** @var int Allow magic __set methods */ public const ALLOW_MAGIC_SET = 1 << 1; /** @var int Allow magic __call methods */ public const ALLOW_MAGIC_CALL = 1 << 2; private const MAP_TYPES = [ 'integer' => TypeIdentifier::INT->value, 'boolean' => TypeIdentifier::BOOL->value, 'double' => TypeIdentifier::FLOAT->value, ]; private array $mutatorPrefixes; private array $accessorPrefixes; private array $arrayMutatorPrefixes; private int $methodReflectionFlags; private int $propertyReflectionFlags; private InflectorInterface $inflector; private array $arrayMutatorPrefixesFirst; private array $arrayMutatorPrefixesLast; private TypeResolverInterface $typeResolver; /** @var array<string, WithAccessors|null> */ private array $accessorsAttributes = []; /** @var array<string, array<string, string>> */ private array $accessorMethodToPropertyMap = []; /** * @param string[]|null $mutatorPrefixes * @param string[]|null $accessorPrefixes * @param string[]|null $arrayMutatorPrefixes */ public function __construct( ?array $mutatorPrefixes = null, ?array $accessorPrefixes = null, ?array $arrayMutatorPrefixes = null, private bool $enableConstructorExtraction = true, int $accessFlags = self::ALLOW_PUBLIC, ?InflectorInterface $inflector = null, private int $magicMethodsFlags = self::ALLOW_MAGIC_GET | self::ALLOW_MAGIC_SET, ) { $this->mutatorPrefixes = $mutatorPrefixes ?? self::$defaultMutatorPrefixes; $this->accessorPrefixes = $accessorPrefixes ?? self::$defaultAccessorPrefixes; $this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? self::$defaultArrayMutatorPrefixes; $this->methodReflectionFlags = $this->getMethodsFlags($accessFlags); $this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags); $this->inflector = $inflector ?? new EnglishInflector(); $typeContextFactory = new TypeContextFactory(); $this->typeResolver = TypeResolver::create([ \ReflectionType::class => $reflectionTypeResolver = new ReflectionTypeResolver(), \ReflectionParameter::class => new ReflectionParameterTypeResolver($reflectionTypeResolver, $typeContextFactory), \ReflectionProperty::class => new ReflectionPropertyTypeResolver($reflectionTypeResolver, $typeContextFactory), \ReflectionFunctionAbstract::class => new ReflectionReturnTypeResolver($reflectionTypeResolver, $typeContextFactory), ]); $this->arrayMutatorPrefixesFirst = array_merge($this->arrayMutatorPrefixes, array_diff($this->mutatorPrefixes, $this->arrayMutatorPrefixes)); $this->arrayMutatorPrefixesLast = array_reverse($this->arrayMutatorPrefixesFirst); } public function getProperties(string $class, array $context = []): ?array { try { $reflectionClass = new \ReflectionClass($class); } catch (\ReflectionException) { return null; } $reflectionProperties = array_column($reflectionClass->getProperties(), null, 'name'); $properties = []; foreach ($reflectionProperties as $reflectionProperty) { if ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags) { $properties[$reflectionProperty->name] = $reflectionProperty->name; } } foreach ($reflectionClass->getMethods($this->methodReflectionFlags) as $reflectionMethod) { if ($reflectionMethod->isStatic()) { continue; } $propertyName = $this->extractPropertyNameFromMethod($reflectionClass, $reflectionMethod->name, $reflectionProperties); if (!$propertyName || isset($properties[$propertyName])) { continue; } $properties[$propertyName] = $propertyName; } return $properties ? array_values($properties) : null; } public function getPropertyName(string $class, string $method, array $context = []): ?string { try { $reflectionMethod = new \ReflectionMethod($class, $method); } catch (\ReflectionException) { return null; } if ($reflectionMethod->isStatic()) { return null; } $reflectionClass = $reflectionMethod->getDeclaringClass(); $reflectionProperties = array_column($reflectionClass->getProperties(), null, 'name'); return $this->extractPropertyNameFromMethod($reflectionClass, $method, $reflectionProperties); } public function getType(string $class, string $property, array $context = []): ?Type { try { $refClass = new \ReflectionClass($class); } catch (\ReflectionException) { return null; } if (null !== $accessors = $this->getAccessorsAttribute($refClass, $property)) { return $this->extractTypeFromAccessors($refClass, $class, $property, $accessors); } [$mutatorReflection, $prefix] = $this->getMutatorMethod($refClass, $property); if ($mutatorReflection) { try { $type = $this->typeResolver->resolve($mutatorReflection->getParameters()[0]); if (!$type instanceof CollectionType && \in_array($prefix, $this->arrayMutatorPrefixes, true)) { $type = $this->isNullableProperty($class, $property) ? Type::nullable(Type::list($type)) : Type::list($type); } return $type; } catch (UnsupportedException) { } } [$accessorReflection, $prefix] = $this->getAccessorMethod($refClass, $property); $allowedPrefixes = array_diff($this->accessorPrefixes, ['is', 'can', 'has']); if ($accessorReflection && (\in_array($prefix, $allowedPrefixes, true) || !property_exists($class, $property))) { try { return $this->typeResolver->resolve($accessorReflection); } catch (UnsupportedException) { } } if ($context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction) { if ($type = $this->extractTypeFromConstructor($refClass, $property)) { return $type; } } try { $reflectionProperty = $refClass->getProperty($property); } catch (\ReflectionException) { return null; } if ($reflectionProperty->hasHook(\PropertyHookType::Set) && $setHookParams = $reflectionProperty->getHook(\PropertyHookType::Set)->getParameters()) { try { return $this->typeResolver->resolve($setHookParams[0]); } catch (UnsupportedException) { } } try { return $this->typeResolver->resolve($reflectionProperty); } catch (UnsupportedException) { } $allowedPrefixes = array_diff($this->accessorPrefixes, $allowedPrefixes); [$accessorReflection, $prefix] = $this->getAccessorMethod($refClass, $property); if ($accessorReflection && \in_array($prefix, $allowedPrefixes, true)) { try { return $this->typeResolver->resolve($accessorReflection); } catch (UnsupportedException) { } } if (null === $defaultValue = ($refClass->getDefaultProperties()[$property] ?? null)) { return null; } $typeIdentifier = TypeIdentifier::from(static::MAP_TYPES[\gettype($defaultValue)] ?? \gettype($defaultValue)); $type = 'array' === $typeIdentifier->value ? Type::array() : Type::builtin($typeIdentifier); if ($this->isNullableProperty($class, $property)) { $type = Type::nullable($type); } return $type; } public function getTypeFromConstructor(string $class, string $property): ?Type { try { $reflection = new \ReflectionClass($class); } catch (\ReflectionException) { return null; } if (!$reflectionConstructor = $reflection->getConstructor()) { return null; } if (!$reflectionParameter = $this->getReflectionParameterFromConstructor($property, $reflectionConstructor)) { return null; } try { return $this->typeResolver->resolve($reflectionParameter); } catch (UnsupportedException) { return null; } } private function getReflectionParameterFromConstructor(string $property, \ReflectionMethod $reflectionConstructor): ?\ReflectionParameter { foreach ($reflectionConstructor->getParameters() as $reflectionParameter) { if ($reflectionParameter->getName() === $property) { return $reflectionParameter; } } return null; } public function isReadable(string $class, string $property, array $context = []): ?bool { if ($this->isAllowedProperty($class, $property)) { return true; } return null !== $this->getReadInfo($class, $property, $context); } public function isWritable(string $class, string $property, array $context = []): ?bool { if ($this->isAllowedProperty($class, $property, true)) { return true; } try { $refClass = new \ReflectionClass($class); } catch (\ReflectionException) { return null; } if (null !== $accessors = $this->getAccessorsAttribute($refClass, $property)) { return null !== $accessors->setter || null !== $accessors->adder; } // First test with the camelized property name [$reflectionMethod] = $this->getMutatorMethod($refClass, $this->camelize($property)); if (null !== $reflectionMethod) { return true; } // Otherwise check for the old way [$reflectionMethod] = $this->getMutatorMethod($refClass, $property); return null !== $reflectionMethod; } public function isInitializable(string $class, string $property, array $context = []): ?bool { try { $reflectionClass = new \ReflectionClass($class); } catch (\ReflectionException) { return null; } if (!$reflectionClass->isInstantiable()) { return false; } if ($constructor = $reflectionClass->getConstructor()) { foreach ($constructor->getParameters() as $parameter) { if ($property === $parameter->name) { return true; } } } elseif ($parentClass = $reflectionClass->getParentClass()) { return $this->isInitializable($parentClass->getName(), $property); } return false; } public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo { try { $reflClass = new \ReflectionClass($class); } catch (\ReflectionException) { return null; } if (null !== $methodName = $this->getAccessorsAttribute($reflClass, $property)?->getter) { $method = $reflClass->getMethod($methodName); return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, $this->getReadVisibilityForMethod($method), $method->isStatic(), false); } $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false; $magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags; $allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL); $allowMagicGet = (bool) ($magicMethods & self::ALLOW_MAGIC_GET); $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) && ($m = $reflClass->getMethod($methodName))->getModifiers() & $this->methodReflectionFlags && !$m->getNumberOfRequiredParameters() && !\in_array((string) $m->getReturnType(), ['void', 'never'], true)) { return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, $this->getReadVisibilityForMethod($m), $m->isStatic(), false); } } if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($m = $reflClass->getMethod($getsetter))->getModifiers() & $this->methodReflectionFlags && !$m->getNumberOfRequiredParameters() && !\in_array((string) $m->getReturnType(), ['void', 'never'], true)) { return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisibilityForMethod($m), $m->isStatic(), false); } if ($allowMagicGet && $reflClass->hasMethod('__get') && (($r = $reflClass->getMethod('__get'))->getModifiers() & $this->methodReflectionFlags)) { return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, $r->returnsReference()); } if ($hasProperty && (($r = $reflClass->getProperty($property))->getModifiers() & $this->propertyReflectionFlags)) { return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisibilityForProperty($r), $r->isStatic(), true); } if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) { return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, 'get'.$camelProp, PropertyReadInfo::VISIBILITY_PUBLIC, false, false); } return null; } public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo { try { $reflClass = new \ReflectionClass($class); } catch (\ReflectionException) { return null; } $allowAdderRemover = $context['enable_adder_remover_extraction'] ?? true; $accessorsAttribute = $this->getAccessorsAttribute($reflClass, $property); $adderAccessName = $accessorsAttribute?->adder; $removerAccessName = $accessorsAttribute?->remover; if ($allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) { $adderMethod = $reflClass->getMethod($adderAccessName); $removerMethod = $reflClass->getMethod($removerAccessName); $mutator = new PropertyWriteInfo(PropertyWriteInfo::TYPE_ADDER_AND_REMOVER); $mutator->setAdderInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $adderAccessName, $this->getWriteVisibilityForMethod($adderMethod), $adderMethod->isStatic())); $mutator->setRemoverInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $removerAccessName, $this->getWriteVisibilityForMethod($removerMethod), $removerMethod->isStatic())); return $mutator; } if (null !== $methodName = $accessorsAttribute?->setter) { $method = $reflClass->getMethod($methodName); return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $methodName, $this->getWriteVisibilityForMethod($method), $method->isStatic()); } $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false; $magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags; $allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL); $allowMagicSet = (bool) ($magicMethods & self::ALLOW_MAGIC_SET); $allowConstruct = $context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction; $constructor = $reflClass->getConstructor(); $errors = []; if (null !== $constructor && $allowConstruct) { foreach ($constructor->getParameters() as $parameter) { if ($parameter->getName() === $property) { return new PropertyWriteInfo(PropertyWriteInfo::TYPE_CONSTRUCTOR, $property); } } } $camelized = $this->camelize($property); $nonCamelized = ucfirst($property); if (null === $adderAccessName || null === $removerAccessName) { [$adderAccessName, $removerAccessName, $adderAndRemoverErrors] = $this->findAdderAndRemover($reflClass, $camelized); if ($allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) { $adderMethod = $reflClass->getMethod($adderAccessName); $removerMethod = $reflClass->getMethod($removerAccessName); $mutator = new PropertyWriteInfo(PropertyWriteInfo::TYPE_ADDER_AND_REMOVER); $mutator->setAdderInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $adderAccessName, $this->getWriteVisibilityForMethod($adderMethod), $adderMethod->isStatic())); $mutator->setRemoverInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $removerAccessName, $this->getWriteVisibilityForMethod($removerMethod), $removerMethod->isStatic())); return $mutator; } $errors[] = $adderAndRemoverErrors; } foreach ($this->mutatorPrefixes as $mutatorPrefix) { $methodName = $mutatorPrefix.$camelized; [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $methodName, 1); if (!$accessible) { $errors[] = $methodAccessibleErrors; continue; } $method = $reflClass->getMethod($methodName); if (!\in_array($mutatorPrefix, $this->arrayMutatorPrefixes, true)) { return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $methodName, $this->getWriteVisibilityForMethod($method), $method->isStatic()); } } if ($camelized !== $nonCamelized) { foreach ($this->mutatorPrefixes as $mutatorPrefix) { $methodName = $mutatorPrefix.$nonCamelized; [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $methodName, 1); if (!$accessible) { $errors[] = $methodAccessibleErrors; continue; } $method = $reflClass->getMethod($methodName); if (!\in_array($mutatorPrefix, $this->arrayMutatorPrefixes, true)) { return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $methodName, $this->getWriteVisibilityForMethod($method), $method->isStatic()); } } } $getsetter = lcfirst($camelized); $getsetterNonCamelized = lcfirst($nonCamelized); if ($allowGetterSetter) { [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $getsetter, 1); if ($accessible) { $method = $reflClass->getMethod($getsetter); return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $getsetter, $this->getWriteVisibilityForMethod($method), $method->isStatic()); } $errors[] = $methodAccessibleErrors; if ($getsetter !== $getsetterNonCamelized) { [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $getsetterNonCamelized, 1); if ($accessible) { $method = $reflClass->getMethod($getsetterNonCamelized); return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $getsetterNonCamelized, $this->getWriteVisibilityForMethod($method), $method->isStatic()); } $errors[] = $methodAccessibleErrors; } } if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { $reflProperty = $reflClass->getProperty($property); if (!$reflProperty->isReadOnly()) { return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisibilityForProperty($reflProperty), $reflProperty->isStatic()); } $errors[] = [\sprintf('The property "%s" in class "%s" is a promoted readonly property.', $property, $reflClass->getName())]; $allowMagicSet = $allowMagicCall = false; } if ($allowMagicSet) { [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__set', 2); if ($accessible) { return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false); } $errors[] = $methodAccessibleErrors; } if ($allowMagicCall) { [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__call', 2); if ($accessible) { return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, 'set'.$camelized, PropertyWriteInfo::VISIBILITY_PUBLIC, false); } $errors[] = $methodAccessibleErrors; } if (!$allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) { $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(array_merge([], ...$errors)); return $noneProperty; } private function extractTypeFromConstructor(\ReflectionClass $reflectionClass, string $property): ?Type { if (!$constructor = $reflectionClass->getConstructor()) { return null; } foreach ($constructor->getParameters() as $parameter) { if ($property !== $parameter->name) { continue; } try { return $this->typeResolver->resolve($parameter); } catch (UnsupportedException) { } } if ($parentClass = $reflectionClass->getParentClass()) { return $this->extractTypeFromConstructor($parentClass, $property); } return null; } private function isNullableProperty(string $class, string $property): bool { try { $reflectionProperty = new \ReflectionProperty($class, $property); $reflectionPropertyType = $reflectionProperty->getType(); return null !== $reflectionPropertyType && $reflectionPropertyType->allowsNull(); } catch (\ReflectionException) { // Return false if the property doesn't exist } return false; } private function isAllowedProperty(string $class, string $property, bool $writeAccessRequired = false): bool { try { $reflectionProperty = new \ReflectionProperty($class, $property); if ($writeAccessRequired) { if ($reflectionProperty->isReadOnly()) { return false; } if ($reflectionProperty->isProtectedSet()) { return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PROTECTED); } if ($reflectionProperty->isPrivateSet()) { return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PRIVATE); } if ($reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { return false; } } return (bool) ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags); } catch (\ReflectionException) { // Return false if the property doesn't exist } return false; } /** * Gets the accessor method. * * Returns an array with an instance of \ReflectionMethod as the first key * and the prefix of the method as the second, or null if not found. */ private function getAccessorMethod(\ReflectionClass $refClass, string $property): ?array { $ucProperty = ucfirst($property); foreach ($this->accessorPrefixes as $prefix) { try { $reflectionMethod = $refClass->getMethod($prefix.$ucProperty); if ($reflectionMethod->isStatic()) { continue; } if (0 === $reflectionMethod->getNumberOfRequiredParameters()) { return [$reflectionMethod, $prefix]; } } catch (\ReflectionException) { // Return null if the property doesn't exist } } return null; } /** * Returns an array with an instance of \ReflectionMethod as the first key * and the prefix of the method as the second, or null if not found. */ private function getMutatorMethod(\ReflectionClass $refClass, string $property): ?array { $ucProperty = ucfirst($property); $ucSingulars = $this->inflector->singularize($ucProperty); $mutatorPrefixes = \in_array($ucProperty, $ucSingulars, true) ? $this->arrayMutatorPrefixesLast : $this->arrayMutatorPrefixesFirst; foreach ($mutatorPrefixes as $prefix) { $names = [$ucProperty]; if (\in_array($prefix, $this->arrayMutatorPrefixes, true)) { $names = array_merge($names, $ucSingulars); } foreach ($names as $name) { try { $reflectionMethod = $refClass->getMethod($prefix.$name); if ($reflectionMethod->isStatic() || !($reflectionMethod->getModifiers() & $this->methodReflectionFlags)) { continue; } // Parameter can be optional to allow things like: method(?array $foo = null), // but at most one parameter may be required, matching the write-access rules if ($reflectionMethod->getNumberOfParameters() >= 1 && $reflectionMethod->getNumberOfRequiredParameters() <= 1) { return [$reflectionMethod, $prefix]; } } catch (\ReflectionException) { // Try the next prefix if the method doesn't exist } } } return null; } /** * @param array<string, \ReflectionProperty> $reflectionProperties */ private function extractPropertyNameFromMethod(\ReflectionClass $refClass, string $methodName, array $reflectionProperties): ?string { if (null !== $propertyName = $this->getAccessorMethodFromAttribute($refClass, $reflectionProperties, $methodName)) { return $propertyName; } $pattern = implode('|', array_merge($this->accessorPrefixes, $this->mutatorPrefixes)); $propertyName = null; if ('' !== $pattern && preg_match('/^('.$pattern.')(.+)$/i', $methodName, $matches)) { // a lowercase first letter means the prefix is part of a longer word rather than a // prefix, e.g. "hash" or "cancel", so the method is not an accessor at all if (ctype_lower($matches[2][0])) { return null; } $propertyName = $matches[2]; if (\in_array($matches[1], $this->arrayMutatorPrefixes, true)) { foreach ($reflectionProperties as $reflectionProperty) { foreach ($this->inflector->singularize($reflectionProperty->name) as $name) { if (strtolower($name) === strtolower($matches[2])) { return $reflectionProperty->name; } } } } } if (!$propertyName) { return null; } if (isset($reflectionProperties[$propertyName])) { return $propertyName; } if ($refClass->hasProperty($lowerCasedPropertyName = lcfirst($propertyName)) || (!$refClass->hasProperty($propertyName) && !preg_match('/^[A-Z]{2,}/', $propertyName))) { $propertyName = $lowerCasedPropertyName; } return $propertyName; } /** * Searches for add and remove methods. * * @param \ReflectionClass $reflClass The reflection class for the given object * @param string $property The camelized property name to singularize and probe * * @return array{?string, ?string, list<string>} The adder method, the remover method, and any errors collected along the way */ private function findAdderAndRemover(\ReflectionClass $reflClass, string $property): array { if (2 !== \count($this->arrayMutatorPrefixes)) { return [null, null, []]; } [$addPrefix, $removePrefix] = $this->arrayMutatorPrefixes; $errors = []; foreach ($this->inflector->singularize($property) as $singular) { $addMethod = $addPrefix.$singular; $removeMethod = $removePrefix.$singular; [$addMethodFound, $addMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $addMethod, 1); [$removeMethodFound, $removeMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $removeMethod, 1); $errors[] = $addMethodAccessibleErrors; $errors[] = $removeMethodAccessibleErrors; if ($addMethodFound && $removeMethodFound) { return [$addMethod, $removeMethod, []]; } if ($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, array_merge([], ...$errors)]; } /** * Returns whether a method is public and has the number of required parameters and errors. */ private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): array { $errors = []; if ($class->hasMethod($methodName)) { $method = $class->getMethod($methodName); 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, $errors]; } /** * Camelizes a given string. */ private function camelize(string $string): string { if ('' === ltrim($string, '_')) { return $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 getReadVisibilityForProperty(\ReflectionProperty $reflectionProperty): string { if ($reflectionProperty->isPrivate()) { return PropertyReadInfo::VISIBILITY_PRIVATE; } if ($reflectionProperty->isProtected()) { return PropertyReadInfo::VISIBILITY_PROTECTED; } return PropertyReadInfo::VISIBILITY_PUBLIC; } private function getReadVisibilityForMethod(\ReflectionMethod $reflectionMethod): string { if ($reflectionMethod->isPrivate()) { return PropertyReadInfo::VISIBILITY_PRIVATE; } if ($reflectionMethod->isProtected()) { return PropertyReadInfo::VISIBILITY_PROTECTED; } return PropertyReadInfo::VISIBILITY_PUBLIC; } private function getWriteVisibilityForProperty(\ReflectionProperty $reflectionProperty): string { if ($reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { return PropertyWriteInfo::VISIBILITY_PRIVATE; } if ($reflectionProperty->isPrivateSet()) { return PropertyWriteInfo::VISIBILITY_PRIVATE; } if ($reflectionProperty->isProtectedSet()) { return PropertyWriteInfo::VISIBILITY_PROTECTED; } if ($reflectionProperty->isPrivate()) { return PropertyWriteInfo::VISIBILITY_PRIVATE; } if ($reflectionProperty->isProtected()) { return PropertyWriteInfo::VISIBILITY_PROTECTED; } return PropertyWriteInfo::VISIBILITY_PUBLIC; } private function getWriteVisibilityForMethod(\ReflectionMethod $reflectionMethod): string { if ($reflectionMethod->isPrivate()) { return PropertyWriteInfo::VISIBILITY_PRIVATE; } if ($reflectionMethod->isProtected()) { return PropertyWriteInfo::VISIBILITY_PROTECTED; } return PropertyWriteInfo::VISIBILITY_PUBLIC; } /** * Resolves the type from the methods named by the attribute. * * A named method is used as is: the prefix gating and the singular/plural guessing that drive * discovery must not second-guess a choice the application made explicit. */ private function extractTypeFromAccessors(\ReflectionClass $refClass, string $class, string $property, WithAccessors $accessors): ?Type { if (null !== $accessors->adder) { try { $type = $this->typeResolver->resolve($refClass->getMethod($accessors->adder)->getParameters()[0]); if (!$type instanceof CollectionType) { $type = $this->isNullableProperty($class, $property) ? Type::nullable(Type::list($type)) : Type::list($type); } return $type; } catch (UnsupportedException) { } } if (null !== $accessors->setter) { try { return $this->typeResolver->resolve($refClass->getMethod($accessors->setter)->getParameters()[0]); } catch (UnsupportedException) { } } if (null !== $accessors->getter) { try { return $this->typeResolver->resolve($refClass->getMethod($accessors->getter)); } catch (UnsupportedException) { } } try { return $this->typeResolver->resolve($refClass->getProperty($property)); } catch (\ReflectionException|UnsupportedException) { } return null; } private function getAccessorsAttribute(\ReflectionClass $refClass, string $property): ?WithAccessors { $propertyHash = $refClass->name.'::'.$property; if (\array_key_exists($propertyHash, $this->accessorsAttributes)) { return $this->accessorsAttributes[$propertyHash]; } if (!$refClass->hasProperty($property)) { if ($parentClass = $refClass->getParentClass()) { return $this->accessorsAttributes[$propertyHash] = $this->getAccessorsAttribute($parentClass, $property); } return $this->accessorsAttributes[$propertyHash] = null; } $refProperty = $refClass->getProperty($property); /** @var \ReflectionAttribute<WithAccessors> $refAttribute */ if (null === $refAttribute = $refProperty->getAttributes(WithAccessors::class)[0] ?? null) { return $this->accessorsAttributes[$propertyHash] = null; } $accessorsAttribute = $refAttribute->newInstance(); $invalidAccessors = []; foreach ([$accessorsAttribute->getter, $accessorsAttribute->setter, $accessorsAttribute->adder, $accessorsAttribute->remover] as $accessor) { if (null !== $accessor && !$refClass->hasMethod($accessor)) { $invalidAccessors[] = $accessor; } } if ($invalidAccessors) { throw new MappingException(\sprintf('Invalid #[WithAccessors] mapping on property "%s" of class "%s". The following methods are missing: "%s".', $refProperty->name, $refClass->name, implode('", "', $invalidAccessors)), $refClass->name, $invalidAccessors); } return $this->accessorsAttributes[$propertyHash] = $accessorsAttribute; } /** * @param \ReflectionProperty[] $reflectionProperties */ private function getAccessorMethodFromAttribute(\ReflectionClass $refClass, array $reflectionProperties, string $method): ?string { $className = $refClass->name; if (!\array_key_exists($className, $this->accessorMethodToPropertyMap)) { $map = []; foreach ($reflectionProperties as $refProperty) { if (null === $accessorsAttribute = $this->getAccessorsAttribute($refClass, $refProperty->name)) { continue; } foreach ([$accessorsAttribute->getter, $accessorsAttribute->setter, $accessorsAttribute->adder, $accessorsAttribute->remover] as $accessor) { if (null !== $accessor) { $map[$accessor] = $refProperty->name; } } } $this->accessorMethodToPropertyMap[$className] = $map; } if (isset($this->accessorMethodToPropertyMap[$className][$method])) { return $this->accessorMethodToPropertyMap[$className][$method]; } if ($parentClass = $refClass->getParentClass()) { return $this->getAccessorMethodFromAttribute($parentClass, $parentClass->getProperties(), $method); } return null; } }