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

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