vendor/symfony/property-info/Extractor/ReflectionExtractor.php line 86

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\PropertyInfo\Extractor;
  11. use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
  12. use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
  13. use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
  14. use Symfony\Component\PropertyInfo\PropertyReadInfo;
  15. use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
  16. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  17. use Symfony\Component\PropertyInfo\PropertyWriteInfo;
  18. use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
  19. use Symfony\Component\PropertyInfo\Type;
  20. use Symfony\Component\String\Inflector\EnglishInflector;
  21. use Symfony\Component\String\Inflector\InflectorInterface;
  22. /**
  23.  * Extracts data using the reflection API.
  24.  *
  25.  * @author Kévin Dunglas <dunglas@gmail.com>
  26.  *
  27.  * @final
  28.  */
  29. class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface
  30. {
  31.     /**
  32.      * @internal
  33.      */
  34.     public static array $defaultMutatorPrefixes = ['add', 'remove', 'set'];
  35.     /**
  36.      * @internal
  37.      */
  38.     public static array $defaultAccessorPrefixes = ['get', 'is', 'has', 'can'];
  39.     /**
  40.      * @internal
  41.      */
  42.     public static array $defaultArrayMutatorPrefixes = ['add', 'remove'];
  43.     public const ALLOW_PRIVATE = 1;
  44.     public const ALLOW_PROTECTED = 2;
  45.     public const ALLOW_PUBLIC = 4;
  46.     /** @var int Allow none of the magic methods */
  47.     public const DISALLOW_MAGIC_METHODS = 0;
  48.     /** @var int Allow magic __get methods */
  49.     public const ALLOW_MAGIC_GET = 1 << 0;
  50.     /** @var int Allow magic __set methods */
  51.     public const ALLOW_MAGIC_SET = 1 << 1;
  52.     /** @var int Allow magic __call methods */
  53.     public const ALLOW_MAGIC_CALL = 1 << 2;
  54.     private const MAP_TYPES = [
  55.         'integer' => Type::BUILTIN_TYPE_INT,
  56.         'boolean' => Type::BUILTIN_TYPE_BOOL,
  57.         'double' => Type::BUILTIN_TYPE_FLOAT,
  58.     ];
  59.     private $mutatorPrefixes;
  60.     private $accessorPrefixes;
  61.     private $arrayMutatorPrefixes;
  62.     private $enableConstructorExtraction;
  63.     private $methodReflectionFlags;
  64.     private $magicMethodsFlags;
  65.     private $propertyReflectionFlags;
  66.     private $inflector;
  67.     private $arrayMutatorPrefixesFirst;
  68.     private $arrayMutatorPrefixesLast;
  69.     /**
  70.      * @param string[]|null $mutatorPrefixes
  71.      * @param string[]|null $accessorPrefixes
  72.      * @param string[]|null $arrayMutatorPrefixes
  73.      */
  74.     public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null, bool $enableConstructorExtraction = true, int $accessFlags = self::ALLOW_PUBLIC, InflectorInterface $inflector = null, int $magicMethodsFlags = self::ALLOW_MAGIC_GET | self::ALLOW_MAGIC_SET)
  75.     {
  76.         $this->mutatorPrefixes = $mutatorPrefixes ?? self::$defaultMutatorPrefixes;
  77.         $this->accessorPrefixes = $accessorPrefixes ?? self::$defaultAccessorPrefixes;
  78.         $this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? self::$defaultArrayMutatorPrefixes;
  79.         $this->enableConstructorExtraction = $enableConstructorExtraction;
  80.         $this->methodReflectionFlags = $this->getMethodsFlags($accessFlags);
  81.         $this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags);
  82.         $this->magicMethodsFlags = $magicMethodsFlags;
  83.         $this->inflector = $inflector ?? new EnglishInflector();
  84.         $this->arrayMutatorPrefixesFirst = array_merge($this->arrayMutatorPrefixes, array_diff($this->mutatorPrefixes, $this->arrayMutatorPrefixes));
  85.         $this->arrayMutatorPrefixesLast = array_reverse($this->arrayMutatorPrefixesFirst);
  86.     }
  87.     public function getProperties(string $class, array $context = []): ?array
  88.     {
  89.         try {
  90.             $reflectionClass = new \ReflectionClass($class);
  91.         } catch (\ReflectionException) {
  92.             return null;
  93.         }
  94.         $reflectionProperties = $reflectionClass->getProperties();
  95.         $properties = [];
  96.         foreach ($reflectionProperties as $reflectionProperty) {
  97.             if ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags) {
  98.                 $properties[$reflectionProperty->name] = $reflectionProperty->name;
  99.             }
  100.         }
  101.         foreach ($reflectionClass->getMethods($this->methodReflectionFlags) as $reflectionMethod) {
  102.             if ($reflectionMethod->isStatic()) {
  103.                 continue;
  104.             }
  105.             $propertyName = $this->getPropertyName($reflectionMethod->name, $reflectionProperties);
  106.             if (!$propertyName || isset($properties[$propertyName])) {
  107.                 continue;
  108.             }
  109.             if ($reflectionClass->hasProperty($lowerCasedPropertyName = lcfirst($propertyName)) || (!$reflectionClass->hasProperty($propertyName) && !preg_match('/^[A-Z]{2,}/', $propertyName))) {
  110.                 $propertyName = $lowerCasedPropertyName;
  111.             }
  112.             $properties[$propertyName] = $propertyName;
  113.         }
  114.         return $properties ? array_values($properties) : null;
  115.     }
  116.     public function getTypes(string $class, string $property, array $context = []): ?array
  117.     {
  118.         if ($fromMutator = $this->extractFromMutator($class, $property)) {
  119.             return $fromMutator;
  120.         }
  121.         if ($fromAccessor = $this->extractFromAccessor($class, $property)) {
  122.             return $fromAccessor;
  123.         }
  124.         if (
  125.             ($context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction) &&
  126.             $fromConstructor = $this->extractFromConstructor($class, $property)
  127.         ) {
  128.             return $fromConstructor;
  129.         }
  130.         if ($fromPropertyDeclaration = $this->extractFromPropertyDeclaration($class, $property)) {
  131.             return $fromPropertyDeclaration;
  132.         }
  133.         return null;
  134.     }
  135.     public function getTypesFromConstructor(string $class, string $property): ?array
  136.     {
  137.         try {
  138.             $reflection = new \ReflectionClass($class);
  139.         } catch (\ReflectionException) {
  140.             return null;
  141.         }
  142.         if (!$reflectionConstructor = $reflection->getConstructor()) {
  143.             return null;
  144.         }
  145.         if (!$reflectionParameter = $this->getReflectionParameterFromConstructor($property, $reflectionConstructor)) {
  146.             return null;
  147.         }
  148.         if (!$reflectionType = $reflectionParameter->getType()) {
  149.             return null;
  150.         }
  151.         if (!$types = $this->extractFromReflectionType($reflectionType, $reflectionConstructor->getDeclaringClass())) {
  152.             return null;
  153.         }
  154.         return $types;
  155.     }
  156.     private function getReflectionParameterFromConstructor(string $property, \ReflectionMethod $reflectionConstructor): ?\ReflectionParameter
  157.     {
  158.         $reflectionParameter = null;
  159.         foreach ($reflectionConstructor->getParameters() as $reflectionParameter) {
  160.             if ($reflectionParameter->getName() === $property) {
  161.                 return $reflectionParameter;
  162.             }
  163.         }
  164.         return null;
  165.     }
  166.     public function isReadable(string $class, string $property, array $context = []): ?bool
  167.     {
  168.         if ($this->isAllowedProperty($class, $property)) {
  169.             return true;
  170.         }
  171.         return null !== $this->getReadInfo($class, $property, $context);
  172.     }
  173.     public function isWritable(string $class, string $property, array $context = []): ?bool
  174.     {
  175.         if ($this->isAllowedProperty($class, $property, true)) {
  176.             return true;
  177.         }
  178.         [$reflectionMethod] = $this->getMutatorMethod($class, $property);
  179.         return null !== $reflectionMethod;
  180.     }
  181.     public function isInitializable(string $class, string $property, array $context = []): ?bool
  182.     {
  183.         try {
  184.             $reflectionClass = new \ReflectionClass($class);
  185.         } catch (\ReflectionException) {
  186.             return null;
  187.         }
  188.         if (!$reflectionClass->isInstantiable()) {
  189.             return false;
  190.         }
  191.         if ($constructor = $reflectionClass->getConstructor()) {
  192.             foreach ($constructor->getParameters() as $parameter) {
  193.                 if ($property === $parameter->name) {
  194.                     return true;
  195.                 }
  196.             }
  197.         } elseif ($parentClass = $reflectionClass->getParentClass()) {
  198.             return $this->isInitializable($parentClass->getName(), $property);
  199.         }
  200.         return false;
  201.     }
  202.     public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo
  203.     {
  204.         try {
  205.             $reflClass = new \ReflectionClass($class);
  206.         } catch (\ReflectionException) {
  207.             return null;
  208.         }
  209.         $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false;
  210.         $magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags;
  211.         $allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL);
  212.         $allowMagicGet = (bool) ($magicMethods & self::ALLOW_MAGIC_GET);
  213.         $hasProperty = $reflClass->hasProperty($property);
  214.         $camelProp = $this->camelize($property);
  215.         $getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
  216.         foreach ($this->accessorPrefixes as $prefix) {
  217.             $methodName = $prefix.$camelProp;
  218.             if ($reflClass->hasMethod($methodName) && $reflClass->getMethod($methodName)->getModifiers() & $this->methodReflectionFlags && !$reflClass->getMethod($methodName)->getNumberOfRequiredParameters()) {
  219.                 $method = $reflClass->getMethod($methodName);
  220.                 return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, $this->getReadVisiblityForMethod($method), $method->isStatic(), false);
  221.             }
  222.         }
  223.         if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($reflClass->getMethod($getsetter)->getModifiers() & $this->methodReflectionFlags)) {
  224.             $method = $reflClass->getMethod($getsetter);
  225.             return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisiblityForMethod($method), $method->isStatic(), false);
  226.         }
  227.         if ($allowMagicGet && $reflClass->hasMethod('__get') && ($reflClass->getMethod('__get')->getModifiers() & $this->methodReflectionFlags)) {
  228.             return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, false);
  229.         }
  230.         if ($hasProperty && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) {
  231.             $reflProperty = $reflClass->getProperty($property);
  232.             return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($reflProperty), $reflProperty->isStatic(), true);
  233.         }
  234.         if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) {
  235.             return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, 'get'.$camelProp, PropertyReadInfo::VISIBILITY_PUBLIC, false, false);
  236.         }
  237.         return null;
  238.     }
  239.     public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo
  240.     {
  241.         try {
  242.             $reflClass = new \ReflectionClass($class);
  243.         } catch (\ReflectionException) {
  244.             return null;
  245.         }
  246.         $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false;
  247.         $magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags;
  248.         $allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL);
  249.         $allowMagicSet = (bool) ($magicMethods & self::ALLOW_MAGIC_SET);
  250.         $allowConstruct = $context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction;
  251.         $allowAdderRemover = $context['enable_adder_remover_extraction'] ?? true;
  252.         $camelized = $this->camelize($property);
  253.         $constructor = $reflClass->getConstructor();
  254.         $singulars = $this->inflector->singularize($camelized);
  255.         $errors = [];
  256.         if (null !== $constructor && $allowConstruct) {
  257.             foreach ($constructor->getParameters() as $parameter) {
  258.                 if ($parameter->getName() === $property) {
  259.                     return new PropertyWriteInfo(PropertyWriteInfo::TYPE_CONSTRUCTOR, $property);
  260.                 }
  261.             }
  262.         }
  263.         [$adderAccessName, $removerAccessName, $adderAndRemoverErrors] = $this->findAdderAndRemover($reflClass, $singulars);
  264.         if ($allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) {
  265.             $adderMethod = $reflClass->getMethod($adderAccessName);
  266.             $removerMethod = $reflClass->getMethod($removerAccessName);
  267.             $mutator = new PropertyWriteInfo(PropertyWriteInfo::TYPE_ADDER_AND_REMOVER);
  268.             $mutator->setAdderInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $adderAccessName, $this->getWriteVisiblityForMethod($adderMethod), $adderMethod->isStatic()));
  269.             $mutator->setRemoverInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $removerAccessName, $this->getWriteVisiblityForMethod($removerMethod), $removerMethod->isStatic()));
  270.             return $mutator;
  271.         }
  272.         $errors[] = $adderAndRemoverErrors;
  273.         foreach ($this->mutatorPrefixes as $mutatorPrefix) {
  274.             $methodName = $mutatorPrefix.$camelized;
  275.             [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $methodName, 1);
  276.             if (!$accessible) {
  277.                 $errors[] = $methodAccessibleErrors;
  278.                 continue;
  279.             }
  280.             $method = $reflClass->getMethod($methodName);
  281.             if (!\in_array($mutatorPrefix, $this->arrayMutatorPrefixes, true)) {
  282.                 return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $methodName, $this->getWriteVisiblityForMethod($method), $method->isStatic());
  283.             }
  284.         }
  285.         $getsetter = lcfirst($camelized);
  286.         if ($allowGetterSetter) {
  287.             [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $getsetter, 1);
  288.             if ($accessible) {
  289.                 $method = $reflClass->getMethod($getsetter);
  290.                 return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $getsetter, $this->getWriteVisiblityForMethod($method), $method->isStatic());
  291.             }
  292.             $errors[] = $methodAccessibleErrors;
  293.         }
  294.         if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) {
  295.             $reflProperty = $reflClass->getProperty($property);
  296.             return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic());
  297.         }
  298.         if ($allowMagicSet) {
  299.             [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__set', 2);
  300.             if ($accessible) {
  301.                 return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false);
  302.             }
  303.             $errors[] = $methodAccessibleErrors;
  304.         }
  305.         if ($allowMagicCall) {
  306.             [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__call', 2);
  307.             if ($accessible) {
  308.                 return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, 'set'.$camelized, PropertyWriteInfo::VISIBILITY_PUBLIC, false);
  309.             }
  310.             $errors[] = $methodAccessibleErrors;
  311.         }
  312.         if (!$allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) {
  313.             $errors[] = [sprintf(
  314.                 'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
  315.                 'the new value must be an array or an instance of \Traversable',
  316.                 $property,
  317.                 $reflClass->getName(),
  318.                 implode('()", "', [$adderAccessName, $removerAccessName])
  319.             )];
  320.         }
  321.         $noneProperty = new PropertyWriteInfo();
  322.         $noneProperty->setErrors(array_merge([], ...$errors));
  323.         return $noneProperty;
  324.     }
  325.     /**
  326.      * @return Type[]|null
  327.      */
  328.     private function extractFromMutator(string $class, string $property): ?array
  329.     {
  330.         [$reflectionMethod, $prefix] = $this->getMutatorMethod($class, $property);
  331.         if (null === $reflectionMethod) {
  332.             return null;
  333.         }
  334.         $reflectionParameters = $reflectionMethod->getParameters();
  335.         $reflectionParameter = $reflectionParameters[0];
  336.         if (!$reflectionType = $reflectionParameter->getType()) {
  337.             return null;
  338.         }
  339.         $type = $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass());
  340.         if (1 === \count($type) && \in_array($prefix, $this->arrayMutatorPrefixes)) {
  341.             $type = [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type[0])];
  342.         }
  343.         return $type;
  344.     }
  345.     /**
  346.      * Tries to extract type information from accessors.
  347.      *
  348.      * @return Type[]|null
  349.      */
  350.     private function extractFromAccessor(string $class, string $property): ?array
  351.     {
  352.         [$reflectionMethod, $prefix] = $this->getAccessorMethod($class, $property);
  353.         if (null === $reflectionMethod) {
  354.             return null;
  355.         }
  356.         if ($reflectionType = $reflectionMethod->getReturnType()) {
  357.             return $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass());
  358.         }
  359.         if (\in_array($prefix, ['is', 'can', 'has'])) {
  360.             return [new Type(Type::BUILTIN_TYPE_BOOL)];
  361.         }
  362.         return null;
  363.     }
  364.     /**
  365.      * Tries to extract type information from constructor.
  366.      *
  367.      * @return Type[]|null
  368.      */
  369.     private function extractFromConstructor(string $class, string $property): ?array
  370.     {
  371.         try {
  372.             $reflectionClass = new \ReflectionClass($class);
  373.         } catch (\ReflectionException) {
  374.             return null;
  375.         }
  376.         $constructor = $reflectionClass->getConstructor();
  377.         if (!$constructor) {
  378.             return null;
  379.         }
  380.         foreach ($constructor->getParameters() as $parameter) {
  381.             if ($property !== $parameter->name) {
  382.                 continue;
  383.             }
  384.             $reflectionType = $parameter->getType();
  385.             return $reflectionType ? $this->extractFromReflectionType($reflectionType, $constructor->getDeclaringClass()) : null;
  386.         }
  387.         if ($parentClass = $reflectionClass->getParentClass()) {
  388.             return $this->extractFromConstructor($parentClass->getName(), $property);
  389.         }
  390.         return null;
  391.     }
  392.     private function extractFromPropertyDeclaration(string $class, string $property): ?array
  393.     {
  394.         try {
  395.             $reflectionClass = new \ReflectionClass($class);
  396.             $reflectionProperty = $reflectionClass->getProperty($property);
  397.             $reflectionPropertyType = $reflectionProperty->getType();
  398.             if (null !== $reflectionPropertyType && $types = $this->extractFromReflectionType($reflectionPropertyType, $reflectionProperty->getDeclaringClass())) {
  399.                 return $types;
  400.             }
  401.         } catch (\ReflectionException) {
  402.             return null;
  403.         }
  404.         $defaultValue = $reflectionClass->getDefaultProperties()[$property] ?? null;
  405.         if (null === $defaultValue) {
  406.             return null;
  407.         }
  408.         $type = \gettype($defaultValue);
  409.         $type = static::MAP_TYPES[$type] ?? $type;
  410.         return [new Type($type, $this->isNullableProperty($class, $property), null, Type::BUILTIN_TYPE_ARRAY === $type)];
  411.     }
  412.     private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionClass $declaringClass): array
  413.     {
  414.         $types = [];
  415.         $nullable = $reflectionType->allowsNull();
  416.         foreach (($reflectionType instanceof \ReflectionUnionType || $reflectionType instanceof \ReflectionIntersectionType) ? $reflectionType->getTypes() : [$reflectionType] as $type) {
  417.             if (!$type instanceof \ReflectionNamedType) {
  418.                 // Nested composite types are not supported yet.
  419.                 return [];
  420.             }
  421.             $phpTypeOrClass = $type->getName();
  422.             if ('null' === $phpTypeOrClass || 'mixed' === $phpTypeOrClass || 'never' === $phpTypeOrClass) {
  423.                 continue;
  424.             }
  425.             if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) {
  426.                 $types[] = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true);
  427.             } elseif ('void' === $phpTypeOrClass) {
  428.                 $types[] = new Type(Type::BUILTIN_TYPE_NULL, $nullable);
  429.             } elseif ($type->isBuiltin()) {
  430.                 $types[] = new Type($phpTypeOrClass, $nullable);
  431.             } else {
  432.                 $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $declaringClass));
  433.             }
  434.         }
  435.         return $types;
  436.     }
  437.     private function resolveTypeName(string $name, \ReflectionClass $declaringClass): string
  438.     {
  439.         if ('self' === $lcName = strtolower($name)) {
  440.             return $declaringClass->name;
  441.         }
  442.         if ('parent' === $lcName && $parent = $declaringClass->getParentClass()) {
  443.             return $parent->name;
  444.         }
  445.         return $name;
  446.     }
  447.     private function isNullableProperty(string $class, string $property): bool
  448.     {
  449.         try {
  450.             $reflectionProperty = new \ReflectionProperty($class, $property);
  451.             $reflectionPropertyType = $reflectionProperty->getType();
  452.             return null !== $reflectionPropertyType && $reflectionPropertyType->allowsNull();
  453.         } catch (\ReflectionException) {
  454.             // Return false if the property doesn't exist
  455.         }
  456.         return false;
  457.     }
  458.     private function isAllowedProperty(string $class, string $property, bool $writeAccessRequired = false): bool
  459.     {
  460.         try {
  461.             $reflectionProperty = new \ReflectionProperty($class, $property);
  462.             if (\PHP_VERSION_ID >= 80100 && $writeAccessRequired && $reflectionProperty->isReadOnly()) {
  463.                 return false;
  464.             }
  465.             return (bool) ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags);
  466.         } catch (\ReflectionException) {
  467.             // Return false if the property doesn't exist
  468.         }
  469.         return false;
  470.     }
  471.     /**
  472.      * Gets the accessor method.
  473.      *
  474.      * Returns an array with a the instance of \ReflectionMethod as first key
  475.      * and the prefix of the method as second or null if not found.
  476.      */
  477.     private function getAccessorMethod(string $class, string $property): ?array
  478.     {
  479.         $ucProperty = ucfirst($property);
  480.         foreach ($this->accessorPrefixes as $prefix) {
  481.             try {
  482.                 $reflectionMethod = new \ReflectionMethod($class, $prefix.$ucProperty);
  483.                 if ($reflectionMethod->isStatic()) {
  484.                     continue;
  485.                 }
  486.                 if (0 === $reflectionMethod->getNumberOfRequiredParameters()) {
  487.                     return [$reflectionMethod, $prefix];
  488.                 }
  489.             } catch (\ReflectionException) {
  490.                 // Return null if the property doesn't exist
  491.             }
  492.         }
  493.         return null;
  494.     }
  495.     /**
  496.      * Returns an array with a the instance of \ReflectionMethod as first key
  497.      * and the prefix of the method as second or null if not found.
  498.      */
  499.     private function getMutatorMethod(string $class, string $property): ?array
  500.     {
  501.         $ucProperty = ucfirst($property);
  502.         $ucSingulars = $this->inflector->singularize($ucProperty);
  503.         $mutatorPrefixes = \in_array($ucProperty, $ucSingulars, true) ? $this->arrayMutatorPrefixesLast : $this->arrayMutatorPrefixesFirst;
  504.         foreach ($mutatorPrefixes as $prefix) {
  505.             $names = [$ucProperty];
  506.             if (\in_array($prefix, $this->arrayMutatorPrefixes)) {
  507.                 $names = array_merge($names, $ucSingulars);
  508.             }
  509.             foreach ($names as $name) {
  510.                 try {
  511.                     $reflectionMethod = new \ReflectionMethod($class, $prefix.$name);
  512.                     if ($reflectionMethod->isStatic()) {
  513.                         continue;
  514.                     }
  515.                     // Parameter can be optional to allow things like: method(array $foo = null)
  516.                     if ($reflectionMethod->getNumberOfParameters() >= 1) {
  517.                         return [$reflectionMethod, $prefix];
  518.                     }
  519.                 } catch (\ReflectionException) {
  520.                     // Try the next prefix if the method doesn't exist
  521.                 }
  522.             }
  523.         }
  524.         return null;
  525.     }
  526.     private function getPropertyName(string $methodName, array $reflectionProperties): ?string
  527.     {
  528.         $pattern = implode('|', array_merge($this->accessorPrefixes, $this->mutatorPrefixes));
  529.         if ('' !== $pattern && preg_match('/^('.$pattern.')(.+)$/i', $methodName, $matches)) {
  530.             if (!\in_array($matches[1], $this->arrayMutatorPrefixes)) {
  531.                 return $matches[2];
  532.             }
  533.             foreach ($reflectionProperties as $reflectionProperty) {
  534.                 foreach ($this->inflector->singularize($reflectionProperty->name) as $name) {
  535.                     if (strtolower($name) === strtolower($matches[2])) {
  536.                         return $reflectionProperty->name;
  537.                     }
  538.                 }
  539.             }
  540.             return $matches[2];
  541.         }
  542.         return null;
  543.     }
  544.     /**
  545.      * Searches for add and remove methods.
  546.      *
  547.      * @param \ReflectionClass $reflClass The reflection class for the given object
  548.      * @param array            $singulars The singular form of the property name or null
  549.      *
  550.      * @return array An array containing the adder and remover when found and errors
  551.      */
  552.     private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars): array
  553.     {
  554.         if (!\is_array($this->arrayMutatorPrefixes) && 2 !== \count($this->arrayMutatorPrefixes)) {
  555.             return [null, null, []];
  556.         }
  557.         [$addPrefix, $removePrefix] = $this->arrayMutatorPrefixes;
  558.         $errors = [];
  559.         foreach ($singulars as $singular) {
  560.             $addMethod = $addPrefix.$singular;
  561.             $removeMethod = $removePrefix.$singular;
  562.             [$addMethodFound, $addMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $addMethod, 1);
  563.             [$removeMethodFound, $removeMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $removeMethod, 1);
  564.             $errors[] = $addMethodAccessibleErrors;
  565.             $errors[] = $removeMethodAccessibleErrors;
  566.             if ($addMethodFound && $removeMethodFound) {
  567.                 return [$addMethod, $removeMethod, []];
  568.             }
  569.             if ($addMethodFound && !$removeMethodFound) {
  570.                 $errors[] = [sprintf('The add method "%s" in class "%s" was found, but the corresponding remove method "%s" was not found', $addMethod, $reflClass->getName(), $removeMethod)];
  571.             } elseif (!$addMethodFound && $removeMethodFound) {
  572.                 $errors[] = [sprintf('The remove method "%s" in class "%s" was found, but the corresponding add method "%s" was not found', $removeMethod, $reflClass->getName(), $addMethod)];
  573.             }
  574.         }
  575.         return [null, null, array_merge([], ...$errors)];
  576.     }
  577.     /**
  578.      * Returns whether a method is public and has the number of required parameters and errors.
  579.      */
  580.     private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): array
  581.     {
  582.         $errors = [];
  583.         if ($class->hasMethod($methodName)) {
  584.             $method = $class->getMethod($methodName);
  585.             if (\ReflectionMethod::IS_PUBLIC === $this->methodReflectionFlags && !$method->isPublic()) {
  586.                 $errors[] = sprintf('The method "%s" in class "%s" was found but does not have public access.', $methodName, $class->getName());
  587.             } elseif ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) {
  588.                 $errors[] = sprintf('The method "%s" in class "%s" requires %d arguments, but should accept only %d.', $methodName, $class->getName(), $method->getNumberOfRequiredParameters(), $parameters);
  589.             } else {
  590.                 return [true, $errors];
  591.             }
  592.         }
  593.         return [false, $errors];
  594.     }
  595.     /**
  596.      * Camelizes a given string.
  597.      */
  598.     private function camelize(string $string): string
  599.     {
  600.         return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
  601.     }
  602.     /**
  603.      * Return allowed reflection method flags.
  604.      */
  605.     private function getMethodsFlags(int $accessFlags): int
  606.     {
  607.         $methodFlags = 0;
  608.         if ($accessFlags & self::ALLOW_PUBLIC) {
  609.             $methodFlags |= \ReflectionMethod::IS_PUBLIC;
  610.         }
  611.         if ($accessFlags & self::ALLOW_PRIVATE) {
  612.             $methodFlags |= \ReflectionMethod::IS_PRIVATE;
  613.         }
  614.         if ($accessFlags & self::ALLOW_PROTECTED) {
  615.             $methodFlags |= \ReflectionMethod::IS_PROTECTED;
  616.         }
  617.         return $methodFlags;
  618.     }
  619.     /**
  620.      * Return allowed reflection property flags.
  621.      */
  622.     private function getPropertyFlags(int $accessFlags): int
  623.     {
  624.         $propertyFlags = 0;
  625.         if ($accessFlags & self::ALLOW_PUBLIC) {
  626.             $propertyFlags |= \ReflectionProperty::IS_PUBLIC;
  627.         }
  628.         if ($accessFlags & self::ALLOW_PRIVATE) {
  629.             $propertyFlags |= \ReflectionProperty::IS_PRIVATE;
  630.         }
  631.         if ($accessFlags & self::ALLOW_PROTECTED) {
  632.             $propertyFlags |= \ReflectionProperty::IS_PROTECTED;
  633.         }
  634.         return $propertyFlags;
  635.     }
  636.     private function getReadVisiblityForProperty(\ReflectionProperty $reflectionProperty): string
  637.     {
  638.         if ($reflectionProperty->isPrivate()) {
  639.             return PropertyReadInfo::VISIBILITY_PRIVATE;
  640.         }
  641.         if ($reflectionProperty->isProtected()) {
  642.             return PropertyReadInfo::VISIBILITY_PROTECTED;
  643.         }
  644.         return PropertyReadInfo::VISIBILITY_PUBLIC;
  645.     }
  646.     private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod): string
  647.     {
  648.         if ($reflectionMethod->isPrivate()) {
  649.             return PropertyReadInfo::VISIBILITY_PRIVATE;
  650.         }
  651.         if ($reflectionMethod->isProtected()) {
  652.             return PropertyReadInfo::VISIBILITY_PROTECTED;
  653.         }
  654.         return PropertyReadInfo::VISIBILITY_PUBLIC;
  655.     }
  656.     private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string
  657.     {
  658.         if ($reflectionProperty->isPrivate()) {
  659.             return PropertyWriteInfo::VISIBILITY_PRIVATE;
  660.         }
  661.         if ($reflectionProperty->isProtected()) {
  662.             return PropertyWriteInfo::VISIBILITY_PROTECTED;
  663.         }
  664.         return PropertyWriteInfo::VISIBILITY_PUBLIC;
  665.     }
  666.     private function getWriteVisiblityForMethod(\ReflectionMethod $reflectionMethod): string
  667.     {
  668.         if ($reflectionMethod->isPrivate()) {
  669.             return PropertyWriteInfo::VISIBILITY_PRIVATE;
  670.         }
  671.         if ($reflectionMethod->isProtected()) {
  672.             return PropertyWriteInfo::VISIBILITY_PROTECTED;
  673.         }
  674.         return PropertyWriteInfo::VISIBILITY_PUBLIC;
  675.     }
  676. }