Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
<?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\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Adds extractors to the property_info.constructor_extractor service.
*
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
*/
final class PropertyInfoConstructorPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('property_info.constructor_extractor')) {
return;
}
$definition = $container->getDefinition('property_info.constructor_extractor');
$listExtractors = $this->findAndSortTaggedServices('property_info.constructor_extractor', $container);
$definition->replaceArgument(0, new IteratorArgument($listExtractors));
}
}

View File

@@ -0,0 +1,54 @@
<?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\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Adds extractors to the property_info service.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertyInfoPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;
/**
* @return void
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('property_info')) {
return;
}
$definition = $container->getDefinition('property_info');
$listExtractors = $this->findAndSortTaggedServices('property_info.list_extractor', $container);
$definition->replaceArgument(0, new IteratorArgument($listExtractors));
$typeExtractors = $this->findAndSortTaggedServices('property_info.type_extractor', $container);
$definition->replaceArgument(1, new IteratorArgument($typeExtractors));
$descriptionExtractors = $this->findAndSortTaggedServices('property_info.description_extractor', $container);
$definition->replaceArgument(2, new IteratorArgument($descriptionExtractors));
$accessExtractors = $this->findAndSortTaggedServices('property_info.access_extractor', $container);
$definition->replaceArgument(3, new IteratorArgument($accessExtractors));
$initializableExtractors = $this->findAndSortTaggedServices('property_info.initializable_extractor', $container);
$definition->setArgument(4, new IteratorArgument($initializableExtractors));
}
}

View File

@@ -0,0 +1,33 @@
<?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\Type;
/**
* Infers the constructor argument type.
*
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
*
* @internal
*/
interface ConstructorArgumentTypeExtractorInterface
{
/**
* Gets types of an argument from constructor.
*
* @return Type[]|null
*
* @internal
*/
public function getTypesFromConstructor(string $class, string $property): ?array;
}

View File

@@ -0,0 +1,42 @@
<?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\PropertyTypeExtractorInterface;
/**
* Extracts the constructor argument type using ConstructorArgumentTypeExtractorInterface implementations.
*
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
*/
final class ConstructorExtractor implements PropertyTypeExtractorInterface
{
/**
* @param iterable<int, ConstructorArgumentTypeExtractorInterface> $extractors
*/
public function __construct(
private readonly iterable $extractors = [],
) {
}
public function getTypes(string $class, string $property, array $context = []): ?array
{
foreach ($this->extractors as $extractor) {
$value = $extractor->getTypesFromConstructor($class, $property);
if (null !== $value) {
return $value;
}
}
return null;
}
}

View File

@@ -0,0 +1,348 @@
<?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 phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\DocBlockFactoryInterface;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\ContextFactory;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper;
/**
* Extracts data using a PHPDoc parser.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final
*/
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
{
public const PROPERTY = 0;
public const ACCESSOR = 1;
public const MUTATOR = 2;
/**
* @var array<string, array{DocBlock|null, int|null, string|null}>
*/
private array $docBlocks = [];
/**
* @var Context[]
*/
private array $contexts = [];
private DocBlockFactoryInterface $docBlockFactory;
private ContextFactory $contextFactory;
private PhpDocTypeHelper $phpDocTypeHelper;
private array $mutatorPrefixes;
private array $accessorPrefixes;
private array $arrayMutatorPrefixes;
/**
* @param string[]|null $mutatorPrefixes
* @param string[]|null $accessorPrefixes
* @param string[]|null $arrayMutatorPrefixes
*/
public function __construct(?DocBlockFactoryInterface $docBlockFactory = null, ?array $mutatorPrefixes = null, ?array $accessorPrefixes = null, ?array $arrayMutatorPrefixes = null)
{
if (!class_exists(DocBlockFactory::class)) {
throw new \LogicException(\sprintf('Unable to use the "%s" class as the "phpdocumentor/reflection-docblock" package is not installed. Try running composer require "phpdocumentor/reflection-docblock".', __CLASS__));
}
$this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance();
$this->contextFactory = new ContextFactory();
$this->phpDocTypeHelper = new PhpDocTypeHelper();
$this->mutatorPrefixes = $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes;
$this->accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes;
$this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes;
}
public function getShortDescription(string $class, string $property, array $context = []): ?string
{
/** @var DocBlock $docBlock */
[$docBlock] = $this->getDocBlock($class, $property);
if (!$docBlock) {
return null;
}
$shortDescription = $docBlock->getSummary();
if (!empty($shortDescription)) {
return $shortDescription;
}
foreach ($docBlock->getTagsByName('var') as $var) {
if ($var && !$var instanceof InvalidTag) {
$varDescription = $var->getDescription()->render();
if (!empty($varDescription)) {
return $varDescription;
}
}
}
return null;
}
public function getLongDescription(string $class, string $property, array $context = []): ?string
{
/** @var DocBlock $docBlock */
[$docBlock] = $this->getDocBlock($class, $property);
if (!$docBlock) {
return null;
}
$contents = $docBlock->getDescription()->render();
return '' === $contents ? null : $contents;
}
public function getTypes(string $class, string $property, array $context = []): ?array
{
/** @var DocBlock $docBlock */
[$docBlock, $source, $prefix] = $this->getDocBlock($class, $property);
if (!$docBlock) {
return null;
}
$tag = match ($source) {
self::PROPERTY => 'var',
self::ACCESSOR => 'return',
self::MUTATOR => 'param',
};
$parentClass = null;
$types = [];
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
foreach ($docBlock->getTagsByName($tag) as $tag) {
if ($tag && !$tag instanceof InvalidTag && null !== $tag->getType()) {
foreach ($this->phpDocTypeHelper->getTypes($tag->getType()) as $type) {
switch ($type->getClassName()) {
case 'self':
case 'static':
$resolvedClass = $class;
break;
case 'parent':
if (false !== $resolvedClass = $parentClass ??= get_parent_class($class)) {
break;
}
// no break
default:
$types[] = $type;
continue 2;
}
$types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
}
}
}
if (!isset($types[0])) {
return null;
}
if (!\in_array($prefix, $this->arrayMutatorPrefixes)) {
return $types;
}
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
}
public function getTypesFromConstructor(string $class, string $property): ?array
{
$docBlock = $this->getDocBlockFromConstructor($class, $property);
if (!$docBlock) {
return null;
}
$types = [];
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
foreach ($docBlock->getTagsByName('param') as $tag) {
if ($tag && null !== $tag->getType()) {
$types[] = $this->phpDocTypeHelper->getTypes($tag->getType());
}
}
if (!isset($types[0]) || [] === $types[0]) {
return null;
}
return array_merge([], ...$types);
}
private function getDocBlockFromConstructor(string $class, string $property): ?DocBlock
{
try {
$reflectionClass = new \ReflectionClass($class);
} catch (\ReflectionException) {
return null;
}
$reflectionConstructor = $reflectionClass->getConstructor();
if (!$reflectionConstructor) {
return null;
}
try {
$docBlock = $this->docBlockFactory->create($reflectionConstructor, $this->contextFactory->createFromReflector($reflectionConstructor));
return $this->filterDocBlockParams($docBlock, $property);
} catch (\InvalidArgumentException) {
return null;
}
}
private function filterDocBlockParams(DocBlock $docBlock, string $allowedParam): DocBlock
{
$tags = array_values(array_filter($docBlock->getTagsByName('param'), fn ($tag) => $tag instanceof DocBlock\Tags\Param && $allowedParam === $tag->getVariableName()));
return new DocBlock($docBlock->getSummary(), $docBlock->getDescription(), $tags, $docBlock->getContext(),
$docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd());
}
/**
* @return array{DocBlock|null, int|null, string|null}
*/
private function getDocBlock(string $class, string $property): array
{
$propertyHash = \sprintf('%s::%s', $class, $property);
if (isset($this->docBlocks[$propertyHash])) {
return $this->docBlocks[$propertyHash];
}
try {
$reflectionProperty = new \ReflectionProperty($class, $property);
} catch (\ReflectionException) {
$reflectionProperty = null;
}
$ucFirstProperty = ucfirst($property);
switch (true) {
case $reflectionProperty?->isPromoted() && $docBlock = $this->getDocBlockFromConstructor($class, $property):
$data = [$docBlock, self::MUTATOR, null];
break;
case $docBlock = $this->getDocBlockFromProperty($class, $property):
$data = [$docBlock, self::PROPERTY, null];
break;
case [$docBlock] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR):
$data = [$docBlock, self::ACCESSOR, null];
break;
case [$docBlock, $prefix] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR):
$data = [$docBlock, self::MUTATOR, $prefix];
break;
default:
$data = [null, null, null];
}
return $this->docBlocks[$propertyHash] = $data;
}
private function getDocBlockFromProperty(string $class, string $property): ?DocBlock
{
// Use a ReflectionProperty instead of $class to get the parent class if applicable
try {
$reflectionProperty = new \ReflectionProperty($class, $property);
} catch (\ReflectionException) {
return null;
}
$reflector = $reflectionProperty->getDeclaringClass();
foreach ($reflector->getTraits() as $trait) {
if ($trait->hasProperty($property)) {
return $this->getDocBlockFromProperty($trait->getName(), $property);
}
}
try {
return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflector));
} catch (\InvalidArgumentException|\RuntimeException) {
return null;
}
}
/**
* @return array{DocBlock, string}|null
*/
private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
{
$prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
$prefix = null;
foreach ($prefixes as $prefix) {
$methodName = $prefix.$ucFirstProperty;
try {
$reflectionMethod = new \ReflectionMethod($class, $methodName);
if ($reflectionMethod->isStatic()) {
continue;
}
if (
(self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters())
|| (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
) {
break;
}
} catch (\ReflectionException) {
// Try the next prefix if the method doesn't exist
}
}
if (!isset($reflectionMethod)) {
return null;
}
$reflector = $reflectionMethod->getDeclaringClass();
foreach ($reflector->getTraits() as $trait) {
if ($trait->hasMethod($methodName)) {
return $this->getDocBlockFromMethod($trait->getName(), $ucFirstProperty, $type);
}
}
try {
return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflector)), $prefix];
} catch (\InvalidArgumentException|\RuntimeException) {
return null;
}
}
/**
* Prevents a lot of redundant calls to ContextFactory::createForNamespace().
*/
private function createFromReflector(\ReflectionClass $reflector): Context
{
$cacheKey = $reflector->getNamespaceName().':'.$reflector->getFileName();
if (isset($this->contexts[$cacheKey])) {
return $this->contexts[$cacheKey];
}
$this->contexts[$cacheKey] = $this->contextFactory->createFromReflector($reflector);
return $this->contexts[$cacheKey];
}
}

View File

@@ -0,0 +1,323 @@
<?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 phpDocumentor\Reflection\Types\ContextFactory;
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\ParserConfig;
use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
/**
* Extracts data using PHPStan parser.
*
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*/
final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
{
private const PROPERTY = 0;
private const ACCESSOR = 1;
private const MUTATOR = 2;
private PhpDocParser $phpDocParser;
private Lexer $lexer;
private NameScopeFactory $nameScopeFactory;
/** @var array<string, array{PhpDocNode|null, int|null, string|null, string|null}> */
private array $docBlocks = [];
private PhpStanTypeHelper $phpStanTypeHelper;
private array $mutatorPrefixes;
private array $accessorPrefixes;
private array $arrayMutatorPrefixes;
/**
* @param list<string>|null $mutatorPrefixes
* @param list<string>|null $accessorPrefixes
* @param list<string>|null $arrayMutatorPrefixes
*/
public function __construct(?array $mutatorPrefixes = null, ?array $accessorPrefixes = null, ?array $arrayMutatorPrefixes = null)
{
if (!class_exists(ContextFactory::class)) {
throw new \LogicException(\sprintf('Unable to use the "%s" class as the "phpdocumentor/type-resolver" package is not installed. Try running composer require "phpdocumentor/type-resolver".', __CLASS__));
}
if (!class_exists(PhpDocParser::class)) {
throw new \LogicException(\sprintf('Unable to use the "%s" class as the "phpstan/phpdoc-parser" package is not installed. Try running composer require "phpstan/phpdoc-parser".', __CLASS__));
}
$this->phpStanTypeHelper = new PhpStanTypeHelper();
$this->mutatorPrefixes = $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes;
$this->accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes;
$this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? ReflectionExtractor::$defaultArrayMutatorPrefixes;
if (class_exists(ParserConfig::class)) {
$parserConfig = new ParserConfig([]);
$this->phpDocParser = new PhpDocParser($parserConfig, new TypeParser($parserConfig, new ConstExprParser($parserConfig)), new ConstExprParser($parserConfig));
$this->lexer = new Lexer($parserConfig);
} else {
$this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
$this->lexer = new Lexer();
}
$this->nameScopeFactory = new NameScopeFactory();
}
public function getTypes(string $class, string $property, array $context = []): ?array
{
/** @var PhpDocNode|null $docNode */
[$docNode, $source, $prefix, $declaringClass] = $this->getDocBlock($class, $property);
$nameScope = $this->nameScopeFactory->create($class, $declaringClass);
if (null === $docNode) {
return null;
}
switch ($source) {
case self::PROPERTY:
$tag = '@var';
break;
case self::ACCESSOR:
$tag = '@return';
break;
case self::MUTATOR:
$tag = '@param';
break;
}
$parentClass = null;
$types = [];
foreach ($docNode->getTagsByName($tag) as $tagDocNode) {
if ($tagDocNode->value instanceof InvalidTagValueNode) {
continue;
}
if (
$tagDocNode->value instanceof ParamTagValueNode
&& null === $prefix
&& $tagDocNode->value->parameterName !== '$'.$property
) {
continue;
}
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value, $nameScope) as $type) {
switch ($type->getClassName()) {
case 'self':
case 'static':
$resolvedClass = $class;
break;
case 'parent':
if (false !== $resolvedClass = $parentClass ??= get_parent_class($class)) {
break;
}
// no break
default:
$types[] = $type;
continue 2;
}
$types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
}
}
if (!isset($types[0])) {
return null;
}
if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) {
return $types;
}
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
}
public function getTypesFromConstructor(string $class, string $property): ?array
{
if (null === $tagDocNode = $this->getDocBlockFromConstructor($class, $property)) {
return null;
}
$types = [];
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode, $this->nameScopeFactory->create($class)) as $type) {
$types[] = $type;
}
if (!isset($types[0])) {
return null;
}
return $types;
}
private function getDocBlockFromConstructor(string $class, string $property): ?ParamTagValueNode
{
try {
$reflectionClass = new \ReflectionClass($class);
} catch (\ReflectionException) {
return null;
}
if (null === $reflectionConstructor = $reflectionClass->getConstructor()) {
return null;
}
if (!$rawDocNode = $reflectionConstructor->getDocComment()) {
return null;
}
$phpDocNode = $this->getPhpDocNode($rawDocNode);
return $this->filterDocBlockParams($phpDocNode, $property);
}
private function filterDocBlockParams(PhpDocNode $docNode, string $allowedParam): ?ParamTagValueNode
{
$tags = array_values(array_filter($docNode->getTagsByName('@param'), fn ($tagNode) => $tagNode instanceof PhpDocTagNode && ('$'.$allowedParam) === $tagNode->value->parameterName));
if (!$tags) {
return null;
}
return $tags[0]->value;
}
/**
* @return array{PhpDocNode|null, int|null, string|null, string|null}
*/
private function getDocBlock(string $class, string $property): array
{
$propertyHash = $class.'::'.$property;
if (isset($this->docBlocks[$propertyHash])) {
return $this->docBlocks[$propertyHash];
}
$ucFirstProperty = ucfirst($property);
if ([$docBlock, $source, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
$data = [$docBlock, $source, null, $declaringClass];
} elseif ([$docBlock, $_, $declaringClass] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR)) {
$data = [$docBlock, self::ACCESSOR, null, $declaringClass];
} elseif ([$docBlock, $prefix, $declaringClass] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR)) {
$data = [$docBlock, self::MUTATOR, $prefix, $declaringClass];
} else {
$data = [null, null, null, null];
}
return $this->docBlocks[$propertyHash] = $data;
}
/**
* @return array{PhpDocNode, int, string}|null
*/
private function getDocBlockFromProperty(string $class, string $property): ?array
{
// Use a ReflectionProperty instead of $class to get the parent class if applicable
try {
$reflectionProperty = new \ReflectionProperty($class, $property);
} catch (\ReflectionException) {
return null;
}
$reflector = $reflectionProperty->getDeclaringClass();
foreach ($reflector->getTraits() as $trait) {
if ($trait->hasProperty($property)) {
return $this->getDocBlockFromProperty($trait->getName(), $property);
}
}
// Type can be inside property docblock as `@var`
$rawDocNode = $reflectionProperty->getDocComment();
$phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null;
$source = self::PROPERTY;
if (!$phpDocNode?->getTagsByName('@var')) {
$phpDocNode = null;
}
// or in the constructor as `@param` for promoted properties
if (!$phpDocNode && $reflectionProperty->isPromoted()) {
$constructor = new \ReflectionMethod($class, '__construct');
$rawDocNode = $constructor->getDocComment();
$phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null;
$source = self::MUTATOR;
}
if (!$phpDocNode) {
return null;
}
return [$phpDocNode, $source, $reflectionProperty->class];
}
/**
* @return array{PhpDocNode, string, string}|null
*/
private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
{
$prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
$prefix = null;
foreach ($prefixes as $prefix) {
$methodName = $prefix.$ucFirstProperty;
try {
$reflectionMethod = new \ReflectionMethod($class, $methodName);
if ($reflectionMethod->isStatic()) {
continue;
}
if (
(self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters())
|| (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
) {
break;
}
} catch (\ReflectionException) {
// Try the next prefix if the method doesn't exist
}
}
if (!isset($reflectionMethod)) {
return null;
}
if (null === $rawDocNode = $reflectionMethod->getDocComment() ?: null) {
return null;
}
$phpDocNode = $this->getPhpDocNode($rawDocNode);
return [$phpDocNode, $prefix, $reflectionMethod->class];
}
private function getPhpDocNode(string $rawDocNode): PhpDocNode
{
$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
$phpDocNode = $this->phpDocParser->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_END);
return $phpDocNode;
}
}

View File

@@ -0,0 +1,872 @@
<?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\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyReadInfo;
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\String\Inflector\EnglishInflector;
use Symfony\Component\String\Inflector\InflectorInterface;
/**
* Extracts data using the reflection API.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final
*/
class ReflectionExtractor implements PropertyListExtractorInterface, 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' => Type::BUILTIN_TYPE_INT,
'boolean' => Type::BUILTIN_TYPE_BOOL,
'double' => Type::BUILTIN_TYPE_FLOAT,
];
private array $mutatorPrefixes;
private array $accessorPrefixes;
private array $arrayMutatorPrefixes;
private bool $enableConstructorExtraction;
private int $methodReflectionFlags;
private int $magicMethodsFlags;
private int $propertyReflectionFlags;
private InflectorInterface $inflector;
private array $arrayMutatorPrefixesFirst;
private array $arrayMutatorPrefixesLast;
/**
* @param string[]|null $mutatorPrefixes
* @param string[]|null $accessorPrefixes
* @param string[]|null $arrayMutatorPrefixes
*/
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)
{
$this->mutatorPrefixes = $mutatorPrefixes ?? self::$defaultMutatorPrefixes;
$this->accessorPrefixes = $accessorPrefixes ?? self::$defaultAccessorPrefixes;
$this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? self::$defaultArrayMutatorPrefixes;
$this->enableConstructorExtraction = $enableConstructorExtraction;
$this->methodReflectionFlags = $this->getMethodsFlags($accessFlags);
$this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags);
$this->magicMethodsFlags = $magicMethodsFlags;
$this->inflector = $inflector ?? new EnglishInflector();
$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 = $reflectionClass->getProperties();
$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->getPropertyName($reflectionMethod->name, $reflectionProperties);
if (!$propertyName || isset($properties[$propertyName])) {
continue;
}
if ($reflectionClass->hasProperty($lowerCasedPropertyName = lcfirst($propertyName)) || (!$reflectionClass->hasProperty($propertyName) && !preg_match('/^[A-Z]{2,}/', $propertyName))) {
$propertyName = $lowerCasedPropertyName;
}
$properties[$propertyName] = $propertyName;
}
return $properties ? array_values($properties) : null;
}
public function getTypes(string $class, string $property, array $context = []): ?array
{
if ($fromMutator = $this->extractFromMutator($class, $property)) {
return $fromMutator;
}
if ($fromAccessor = $this->extractFromAccessor($class, $property)) {
return $fromAccessor;
}
if (
($context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction)
&& $fromConstructor = $this->extractFromConstructor($class, $property)
) {
return $fromConstructor;
}
if ($fromPropertyDeclaration = $this->extractFromPropertyDeclaration($class, $property)) {
return $fromPropertyDeclaration;
}
return null;
}
public function getTypesFromConstructor(string $class, string $property): ?array
{
try {
$reflection = new \ReflectionClass($class);
} catch (\ReflectionException) {
return null;
}
if (!$reflectionConstructor = $reflection->getConstructor()) {
return null;
}
if (!$reflectionParameter = $this->getReflectionParameterFromConstructor($property, $reflectionConstructor)) {
return null;
}
if (!$reflectionType = $reflectionParameter->getType()) {
return null;
}
if (!$types = $this->extractFromReflectionType($reflectionType, $reflectionConstructor->getDeclaringClass())) {
return null;
}
return $types;
}
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;
}
// First test with the camelized property name
[$reflectionMethod] = $this->getMutatorMethod($class, $this->camelize($property));
if (null !== $reflectionMethod) {
return true;
}
// Otherwise check for the old way
[$reflectionMethod] = $this->getMutatorMethod($class, $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;
}
$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) && $reflClass->getMethod($methodName)->getModifiers() & $this->methodReflectionFlags && !$reflClass->getMethod($methodName)->getNumberOfRequiredParameters()) {
$method = $reflClass->getMethod($methodName);
return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, $this->getReadVisiblityForMethod($method), $method->isStatic(), false);
}
}
if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($reflClass->getMethod($getsetter)->getModifiers() & $this->methodReflectionFlags)) {
$method = $reflClass->getMethod($getsetter);
return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisiblityForMethod($method), $method->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->getReadVisiblityForProperty($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;
}
$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;
$allowAdderRemover = $context['enable_adder_remover_extraction'] ?? true;
$camelized = $this->camelize($property);
$constructor = $reflClass->getConstructor();
$singulars = $this->inflector->singularize($camelized);
$errors = [];
if (null !== $constructor && $allowConstruct) {
foreach ($constructor->getParameters() as $parameter) {
if ($parameter->getName() === $property) {
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_CONSTRUCTOR, $property);
}
}
}
[$adderAccessName, $removerAccessName, $adderAndRemoverErrors] = $this->findAdderAndRemover($reflClass, $singulars);
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->getWriteVisiblityForMethod($adderMethod), $adderMethod->isStatic()));
$mutator->setRemoverInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $removerAccessName, $this->getWriteVisiblityForMethod($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->getWriteVisiblityForMethod($method), $method->isStatic());
}
}
$getsetter = lcfirst($camelized);
if ($allowGetterSetter) {
[$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $getsetter, 1);
if ($accessible) {
$method = $reflClass->getMethod($getsetter);
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $getsetter, $this->getWriteVisiblityForMethod($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->getWriteVisiblityForProperty($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;
}
/**
* @return Type[]|null
*/
private function extractFromMutator(string $class, string $property): ?array
{
[$reflectionMethod, $prefix] = $this->getMutatorMethod($class, $property);
if (null === $reflectionMethod) {
return null;
}
$reflectionParameters = $reflectionMethod->getParameters();
$reflectionParameter = $reflectionParameters[0];
if (!$reflectionType = $reflectionParameter->getType()) {
return null;
}
$type = $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass());
if (1 === \count($type) && \in_array($prefix, $this->arrayMutatorPrefixes)) {
$type = [new Type(Type::BUILTIN_TYPE_ARRAY, $this->isNullableProperty($class, $property), null, true, new Type(Type::BUILTIN_TYPE_INT), $type[0])];
}
return $type;
}
/**
* Tries to extract type information from accessors.
*
* @return Type[]|null
*/
private function extractFromAccessor(string $class, string $property): ?array
{
[$reflectionMethod, $prefix] = $this->getAccessorMethod($class, $property);
if (null === $reflectionMethod) {
return null;
}
if ($reflectionType = $reflectionMethod->getReturnType()) {
return $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass());
}
if (\in_array($prefix, ['is', 'can', 'has'])) {
return [new Type(Type::BUILTIN_TYPE_BOOL)];
}
return null;
}
/**
* Tries to extract type information from constructor.
*
* @return Type[]|null
*/
private function extractFromConstructor(string $class, string $property): ?array
{
try {
$reflectionClass = new \ReflectionClass($class);
} catch (\ReflectionException) {
return null;
}
$constructor = $reflectionClass->getConstructor();
if (!$constructor) {
return null;
}
foreach ($constructor->getParameters() as $parameter) {
if ($property !== $parameter->name) {
continue;
}
$reflectionType = $parameter->getType();
return $reflectionType ? $this->extractFromReflectionType($reflectionType, $constructor->getDeclaringClass()) : null;
}
if ($parentClass = $reflectionClass->getParentClass()) {
return $this->extractFromConstructor($parentClass->getName(), $property);
}
return null;
}
private function extractFromPropertyDeclaration(string $class, string $property): ?array
{
try {
$reflectionClass = new \ReflectionClass($class);
$reflectionProperty = $reflectionClass->getProperty($property);
$reflectionPropertyType = $reflectionProperty->getType();
if (null !== $reflectionPropertyType && $types = $this->extractFromReflectionType($reflectionPropertyType, $reflectionProperty->getDeclaringClass())) {
return $types;
}
} catch (\ReflectionException) {
return null;
}
$defaultValue = $reflectionClass->getDefaultProperties()[$property] ?? null;
if (null === $defaultValue) {
return null;
}
$type = \gettype($defaultValue);
$type = static::MAP_TYPES[$type] ?? $type;
return [new Type($type, $this->isNullableProperty($class, $property), null, Type::BUILTIN_TYPE_ARRAY === $type)];
}
private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionClass $declaringClass): array
{
$types = [];
$nullable = $reflectionType->allowsNull();
foreach (($reflectionType instanceof \ReflectionUnionType || $reflectionType instanceof \ReflectionIntersectionType) ? $reflectionType->getTypes() : [$reflectionType] as $type) {
if (!$type instanceof \ReflectionNamedType) {
// Nested composite types are not supported yet.
return [];
}
$phpTypeOrClass = $type->getName();
if ('null' === $phpTypeOrClass || 'mixed' === $phpTypeOrClass || 'never' === $phpTypeOrClass) {
continue;
}
if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) {
$types[] = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true);
} elseif ('void' === $phpTypeOrClass) {
$types[] = new Type(Type::BUILTIN_TYPE_NULL, $nullable);
} elseif ($type->isBuiltin()) {
$types[] = new Type($phpTypeOrClass, $nullable);
} else {
$types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $declaringClass));
}
}
return $types;
}
private function resolveTypeName(string $name, \ReflectionClass $declaringClass): string
{
if ('self' === $lcName = strtolower($name)) {
return $declaringClass->name;
}
if ('parent' === $lcName && $parent = $declaringClass->getParentClass()) {
return $parent->name;
}
return $name;
}
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 (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isProtectedSet()) {
return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PROTECTED);
}
if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isPrivateSet()) {
return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PRIVATE);
}
if (\PHP_VERSION_ID >= 80400 && $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 a the instance of \ReflectionMethod as first key
* and the prefix of the method as second or null if not found.
*/
private function getAccessorMethod(string $class, string $property): ?array
{
$ucProperty = ucfirst($property);
foreach ($this->accessorPrefixes as $prefix) {
try {
$reflectionMethod = new \ReflectionMethod($class, $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 a the instance of \ReflectionMethod as first key
* and the prefix of the method as second or null if not found.
*/
private function getMutatorMethod(string $class, 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)) {
$names = array_merge($names, $ucSingulars);
}
foreach ($names as $name) {
try {
$reflectionMethod = new \ReflectionMethod($class, $prefix.$name);
if ($reflectionMethod->isStatic()) {
continue;
}
// Parameter can be optional to allow things like: method(?array $foo = null)
if ($reflectionMethod->getNumberOfParameters() >= 1) {
return [$reflectionMethod, $prefix];
}
} catch (\ReflectionException) {
// Try the next prefix if the method doesn't exist
}
}
}
return null;
}
private function getPropertyName(string $methodName, array $reflectionProperties): ?string
{
$pattern = implode('|', array_merge($this->accessorPrefixes, $this->mutatorPrefixes));
if ('' !== $pattern && preg_match('/^('.$pattern.')(.+)$/i', $methodName, $matches)) {
if (!\in_array($matches[1], $this->arrayMutatorPrefixes)) {
return $matches[2];
}
foreach ($reflectionProperties as $reflectionProperty) {
foreach ($this->inflector->singularize($reflectionProperty->name) as $name) {
if (strtolower($name) === strtolower($matches[2])) {
return $reflectionProperty->name;
}
}
}
return $matches[2];
}
return null;
}
/**
* Searches for add and remove methods.
*
* @param \ReflectionClass $reflClass The reflection class for the given object
* @param array $singulars The singular form of the property name or null
*
* @return array An array containing the adder and remover when found and errors
*/
private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars): array
{
if (!\is_array($this->arrayMutatorPrefixes) && 2 !== \count($this->arrayMutatorPrefixes)) {
return [null, null, []];
}
[$addPrefix, $removePrefix] = $this->arrayMutatorPrefixes;
$errors = [];
foreach ($singulars 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
{
return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
}
/**
* Return allowed reflection method flags.
*/
private function getMethodsFlags(int $accessFlags): int
{
$methodFlags = 0;
if ($accessFlags & self::ALLOW_PUBLIC) {
$methodFlags |= \ReflectionMethod::IS_PUBLIC;
}
if ($accessFlags & self::ALLOW_PRIVATE) {
$methodFlags |= \ReflectionMethod::IS_PRIVATE;
}
if ($accessFlags & self::ALLOW_PROTECTED) {
$methodFlags |= \ReflectionMethod::IS_PROTECTED;
}
return $methodFlags;
}
/**
* Return allowed reflection property flags.
*/
private function getPropertyFlags(int $accessFlags): int
{
$propertyFlags = 0;
if ($accessFlags & self::ALLOW_PUBLIC) {
$propertyFlags |= \ReflectionProperty::IS_PUBLIC;
}
if ($accessFlags & self::ALLOW_PRIVATE) {
$propertyFlags |= \ReflectionProperty::IS_PRIVATE;
}
if ($accessFlags & self::ALLOW_PROTECTED) {
$propertyFlags |= \ReflectionProperty::IS_PROTECTED;
}
return $propertyFlags;
}
private function getReadVisiblityForProperty(\ReflectionProperty $reflectionProperty): string
{
if ($reflectionProperty->isPrivate()) {
return PropertyReadInfo::VISIBILITY_PRIVATE;
}
if ($reflectionProperty->isProtected()) {
return PropertyReadInfo::VISIBILITY_PROTECTED;
}
return PropertyReadInfo::VISIBILITY_PUBLIC;
}
private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod): string
{
if ($reflectionMethod->isPrivate()) {
return PropertyReadInfo::VISIBILITY_PRIVATE;
}
if ($reflectionMethod->isProtected()) {
return PropertyReadInfo::VISIBILITY_PROTECTED;
}
return PropertyReadInfo::VISIBILITY_PUBLIC;
}
private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string
{
if (\PHP_VERSION_ID >= 80400) {
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 getWriteVisiblityForMethod(\ReflectionMethod $reflectionMethod): string
{
if ($reflectionMethod->isPrivate()) {
return PropertyWriteInfo::VISIBILITY_PRIVATE;
}
if ($reflectionMethod->isProtected()) {
return PropertyWriteInfo::VISIBILITY_PROTECTED;
}
return PropertyWriteInfo::VISIBILITY_PUBLIC;
}
}

View File

@@ -0,0 +1,52 @@
<?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\PropertyListExtractorInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
/**
* Lists available properties using Symfony Serializer Component metadata.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final
*/
class SerializerExtractor implements PropertyListExtractorInterface
{
public function __construct(
private readonly ClassMetadataFactoryInterface $classMetadataFactory,
) {
}
public function getProperties(string $class, array $context = []): ?array
{
if (!\array_key_exists('serializer_groups', $context) || (null !== $context['serializer_groups'] && !\is_array($context['serializer_groups']))) {
return null;
}
if (!$this->classMetadataFactory->hasMetadataFor($class)) {
return null;
}
$properties = [];
$serializerClassMetadata = $this->classMetadataFactory->getMetadataFor($class);
foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
if (!$serializerAttributeMetadata->isIgnored() && (null === $context['serializer_groups'] || array_intersect($context['serializer_groups'], $serializerAttributeMetadata->getGroups()))) {
$properties[] = $serializerAttributeMetadata->getName();
}
}
return $properties;
}
}

19
vendor/symfony/property-info/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2015-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,65 @@
<?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\PhpStan;
/**
* NameScope class adapted from PHPStan code.
*
* @copyright Copyright (c) 2016, PHPStan https://github.com/phpstan/phpstan-src
* @copyright Copyright (c) 2016, Ondřej Mirtes
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*
* @internal
*/
final class NameScope
{
private string $calledClassName;
private string $namespace;
/** @var array<string, string> alias(string) => fullName(string) */
private array $uses;
public function __construct(string $calledClassName, string $namespace, array $uses = [])
{
$this->calledClassName = $calledClassName;
$this->namespace = $namespace;
$this->uses = $uses;
}
public function resolveStringName(string $name): string
{
if (str_starts_with($name, '\\')) {
return ltrim($name, '\\');
}
$nameParts = explode('\\', $name);
$firstNamePart = $nameParts[0];
if (isset($this->uses[$firstNamePart])) {
if (1 === \count($nameParts)) {
return $this->uses[$firstNamePart];
}
array_shift($nameParts);
return \sprintf('%s\\%s', $this->uses[$firstNamePart], implode('\\', $nameParts));
}
if (null !== $this->namespace) {
return \sprintf('%s\\%s', $this->namespace, $name);
}
return $name;
}
public function resolveRootClass(): string
{
return $this->resolveStringName($this->calledClassName);
}
}

View File

@@ -0,0 +1,70 @@
<?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\PhpStan;
use phpDocumentor\Reflection\Types\ContextFactory;
/**
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*
* @internal
*/
final class NameScopeFactory
{
public function create(string $calledClassName, ?string $declaringClassName = null): NameScope
{
$declaringClassName ??= $calledClassName;
$path = explode('\\', $calledClassName);
$calledClassName = array_pop($path);
$declaringReflection = new \ReflectionClass($declaringClassName);
[$declaringNamespace, $declaringUses] = $this->extractFromFullClassName($declaringReflection);
$declaringUses = array_merge($declaringUses, $this->collectUses($declaringReflection));
return new NameScope($calledClassName, $declaringNamespace, $declaringUses);
}
private function collectUses(\ReflectionClass $reflection): array
{
$uses = [$this->extractFromFullClassName($reflection)[1]];
foreach ($reflection->getTraits() as $traitReflection) {
$uses[] = $this->extractFromFullClassName($traitReflection)[1];
}
if (false !== $parentClass = $reflection->getParentClass()) {
$uses[] = $this->collectUses($parentClass);
}
return $uses ? array_merge(...$uses) : [];
}
private function extractFromFullClassName(\ReflectionClass $reflection): array
{
$namespace = trim($reflection->getNamespaceName(), '\\');
$fileName = $reflection->getFileName();
if (\is_string($fileName) && is_file($fileName)) {
if (false === $contents = file_get_contents($fileName)) {
throw new \RuntimeException(\sprintf('Unable to read file "%s".', $fileName));
}
$factory = new ContextFactory();
$context = $factory->createForNamespace($namespace, $contents);
return [$namespace, $context->getNamespaceAliases()];
}
return [$namespace, []];
}
}

View File

@@ -0,0 +1,34 @@
<?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;
/**
* Guesses if the property can be accessed or mutated.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface PropertyAccessExtractorInterface
{
/**
* Is the property readable?
*
* @return bool|null
*/
public function isReadable(string $class, string $property, array $context = []);
/**
* Is the property writable?
*
* @return bool|null
*/
public function isWritable(string $class, string $property, array $context = []);
}

View File

@@ -0,0 +1,30 @@
<?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;
/**
* Guesses the property's human readable description.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface PropertyDescriptionExtractorInterface
{
/**
* Gets the short description of the property.
*/
public function getShortDescription(string $class, string $property, array $context = []): ?string;
/**
* Gets the long description of the property.
*/
public function getLongDescription(string $class, string $property, array $context = []): ?string;
}

View File

@@ -0,0 +1,99 @@
<?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;
use Psr\Cache\CacheItemPoolInterface;
/**
* Adds a PSR-6 cache layer on top of an extractor.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final
*/
class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface
{
private array $arrayCache = [];
public function __construct(
private readonly PropertyInfoExtractorInterface $propertyInfoExtractor,
private readonly CacheItemPoolInterface $cacheItemPool,
) {
}
public function isReadable(string $class, string $property, array $context = []): ?bool
{
return $this->extract('isReadable', [$class, $property, $context]);
}
public function isWritable(string $class, string $property, array $context = []): ?bool
{
return $this->extract('isWritable', [$class, $property, $context]);
}
public function getShortDescription(string $class, string $property, array $context = []): ?string
{
return $this->extract('getShortDescription', [$class, $property, $context]);
}
public function getLongDescription(string $class, string $property, array $context = []): ?string
{
return $this->extract('getLongDescription', [$class, $property, $context]);
}
public function getProperties(string $class, array $context = []): ?array
{
return $this->extract('getProperties', [$class, $context]);
}
public function getTypes(string $class, string $property, array $context = []): ?array
{
return $this->extract('getTypes', [$class, $property, $context]);
}
public function isInitializable(string $class, string $property, array $context = []): ?bool
{
return $this->extract('isInitializable', [$class, $property, $context]);
}
/**
* Retrieves the cached data if applicable or delegates to the decorated extractor.
*/
private function extract(string $method, array $arguments): mixed
{
try {
$serializedArguments = serialize($arguments);
} catch (\Exception) {
// If arguments are not serializable, skip the cache
return $this->propertyInfoExtractor->{$method}(...$arguments);
}
// Calling rawurlencode escapes special characters not allowed in PSR-6's keys
$key = rawurlencode($method.'.'.$serializedArguments);
if (\array_key_exists($key, $this->arrayCache)) {
return $this->arrayCache[$key];
}
$item = $this->cacheItemPool->getItem($key);
if ($item->isHit()) {
return $this->arrayCache[$key] = $item->get();
}
$value = $this->propertyInfoExtractor->{$method}(...$arguments);
$item->set($value);
$this->cacheItemPool->save($item);
return $this->arrayCache[$key] = $value;
}
}

View File

@@ -0,0 +1,90 @@
<?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;
/**
* Default {@see PropertyInfoExtractorInterface} implementation.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final
*/
class PropertyInfoExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface
{
/**
* @param iterable<mixed, PropertyListExtractorInterface> $listExtractors
* @param iterable<mixed, PropertyTypeExtractorInterface> $typeExtractors
* @param iterable<mixed, PropertyDescriptionExtractorInterface> $descriptionExtractors
* @param iterable<mixed, PropertyAccessExtractorInterface> $accessExtractors
* @param iterable<mixed, PropertyInitializableExtractorInterface> $initializableExtractors
*/
public function __construct(
private readonly iterable $listExtractors = [],
private readonly iterable $typeExtractors = [],
private readonly iterable $descriptionExtractors = [],
private readonly iterable $accessExtractors = [],
private readonly iterable $initializableExtractors = [],
) {
}
public function getProperties(string $class, array $context = []): ?array
{
return $this->extract($this->listExtractors, 'getProperties', [$class, $context]);
}
public function getShortDescription(string $class, string $property, array $context = []): ?string
{
return $this->extract($this->descriptionExtractors, 'getShortDescription', [$class, $property, $context]);
}
public function getLongDescription(string $class, string $property, array $context = []): ?string
{
return $this->extract($this->descriptionExtractors, 'getLongDescription', [$class, $property, $context]);
}
public function getTypes(string $class, string $property, array $context = []): ?array
{
return $this->extract($this->typeExtractors, 'getTypes', [$class, $property, $context]);
}
public function isReadable(string $class, string $property, array $context = []): ?bool
{
return $this->extract($this->accessExtractors, 'isReadable', [$class, $property, $context]);
}
public function isWritable(string $class, string $property, array $context = []): ?bool
{
return $this->extract($this->accessExtractors, 'isWritable', [$class, $property, $context]);
}
public function isInitializable(string $class, string $property, array $context = []): ?bool
{
return $this->extract($this->initializableExtractors, 'isInitializable', [$class, $property, $context]);
}
/**
* Iterates over registered extractors and return the first value found.
*
* @param iterable<mixed, object> $extractors
* @param list<mixed> $arguments
*/
private function extract(iterable $extractors, string $method, array $arguments): mixed
{
foreach ($extractors as $extractor) {
if (null !== $value = $extractor->{$method}(...$arguments)) {
return $value;
}
}
return null;
}
}

View File

@@ -0,0 +1,23 @@
<?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;
/**
* Gets info about PHP class properties.
*
* A convenient interface inheriting all specific info interfaces.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface PropertyInfoExtractorInterface extends PropertyTypeExtractorInterface, PropertyDescriptionExtractorInterface, PropertyAccessExtractorInterface, PropertyListExtractorInterface
{
}

View File

@@ -0,0 +1,25 @@
<?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;
/**
* Guesses if the property can be initialized through the constructor.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface PropertyInitializableExtractorInterface
{
/**
* Is the property initializable? Returns true if a constructor's parameter matches the given property name.
*/
public function isInitializable(string $class, string $property, array $context = []): ?bool;
}

View File

@@ -0,0 +1,27 @@
<?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;
/**
* Extracts the list of properties available for the given class.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface PropertyListExtractorInterface
{
/**
* Gets the list of properties available for the given class.
*
* @return string[]|null
*/
public function getProperties(string $class, array $context = []);
}

View File

@@ -0,0 +1,70 @@
<?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;
/**
* The property read info tells how a property can be read.
*
* @author Joel Wurtz <jwurtz@jolicode.com>
*/
final class PropertyReadInfo
{
public const TYPE_METHOD = 'method';
public const TYPE_PROPERTY = 'property';
public const VISIBILITY_PUBLIC = 'public';
public const VISIBILITY_PROTECTED = 'protected';
public const VISIBILITY_PRIVATE = 'private';
public function __construct(
private readonly string $type,
private readonly string $name,
private readonly string $visibility,
private readonly bool $static,
private readonly bool $byRef,
) {
}
/**
* Get type of access.
*/
public function getType(): string
{
return $this->type;
}
/**
* Get name of the access, which can be a method name or a property name, depending on the type.
*/
public function getName(): string
{
return $this->name;
}
public function getVisibility(): string
{
return $this->visibility;
}
public function isStatic(): bool
{
return $this->static;
}
/**
* Whether this accessor can be accessed by reference.
*/
public function canBeReference(): bool
{
return $this->byRef;
}
}

View File

@@ -0,0 +1,25 @@
<?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;
/**
* Extract read information for the property of a class.
*
* @author Joel Wurtz <jwurtz@jolicode.com>
*/
interface PropertyReadInfoExtractorInterface
{
/**
* Get read information object for a given property of a class.
*/
public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo;
}

View File

@@ -0,0 +1,27 @@
<?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;
/**
* Type Extractor Interface.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface PropertyTypeExtractorInterface
{
/**
* Gets types of a property.
*
* @return Type[]|null
*/
public function getTypes(string $class, string $property, array $context = []);
}

View File

@@ -0,0 +1,117 @@
<?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;
/**
* The write mutator defines how a property can be written.
*
* @author Joel Wurtz <jwurtz@jolicode.com>
*/
final class PropertyWriteInfo
{
public const TYPE_NONE = 'none';
public const TYPE_METHOD = 'method';
public const TYPE_PROPERTY = 'property';
public const TYPE_ADDER_AND_REMOVER = 'adder_and_remover';
public const TYPE_CONSTRUCTOR = 'constructor';
public const VISIBILITY_PUBLIC = 'public';
public const VISIBILITY_PROTECTED = 'protected';
public const VISIBILITY_PRIVATE = 'private';
private ?self $adderInfo = null;
private ?self $removerInfo = null;
private array $errors = [];
public function __construct(
private readonly string $type = self::TYPE_NONE,
private readonly ?string $name = null,
private readonly ?string $visibility = null,
private readonly ?bool $static = null,
) {
}
public function getType(): string
{
return $this->type;
}
public function getName(): string
{
if (null === $this->name) {
throw new \LogicException("Calling getName() when having a mutator of type {$this->type} is not tolerated.");
}
return $this->name;
}
public function setAdderInfo(self $adderInfo): void
{
$this->adderInfo = $adderInfo;
}
public function getAdderInfo(): self
{
if (null === $this->adderInfo) {
throw new \LogicException("Calling getAdderInfo() when having a mutator of type {$this->type} is not tolerated.");
}
return $this->adderInfo;
}
public function setRemoverInfo(self $removerInfo): void
{
$this->removerInfo = $removerInfo;
}
public function getRemoverInfo(): self
{
if (null === $this->removerInfo) {
throw new \LogicException("Calling getRemoverInfo() when having a mutator of type {$this->type} is not tolerated.");
}
return $this->removerInfo;
}
public function getVisibility(): string
{
if (null === $this->visibility) {
throw new \LogicException("Calling getVisibility() when having a mutator of type {$this->type} is not tolerated.");
}
return $this->visibility;
}
public function isStatic(): bool
{
if (null === $this->static) {
throw new \LogicException("Calling isStatic() when having a mutator of type {$this->type} is not tolerated.");
}
return $this->static;
}
public function setErrors(array $errors): void
{
$this->errors = $errors;
}
public function getErrors(): array
{
return $this->errors;
}
public function hasErrors(): bool
{
return (bool) \count($this->errors);
}
}

View File

@@ -0,0 +1,25 @@
<?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;
/**
* Extract write information for the property of a class.
*
* @author Joel Wurtz <jwurtz@jolicode.com>
*/
interface PropertyWriteInfoExtractorInterface
{
/**
* Get write information object for a given property of a class.
*/
public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo;
}

165
vendor/symfony/property-info/Type.php vendored Normal file
View File

@@ -0,0 +1,165 @@
<?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;
/**
* Type value object (immutable).
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final
*/
class Type
{
public const BUILTIN_TYPE_INT = 'int';
public const BUILTIN_TYPE_FLOAT = 'float';
public const BUILTIN_TYPE_STRING = 'string';
public const BUILTIN_TYPE_BOOL = 'bool';
public const BUILTIN_TYPE_RESOURCE = 'resource';
public const BUILTIN_TYPE_OBJECT = 'object';
public const BUILTIN_TYPE_ARRAY = 'array';
public const BUILTIN_TYPE_NULL = 'null';
public const BUILTIN_TYPE_FALSE = 'false';
public const BUILTIN_TYPE_TRUE = 'true';
public const BUILTIN_TYPE_CALLABLE = 'callable';
public const BUILTIN_TYPE_ITERABLE = 'iterable';
/**
* List of PHP builtin types.
*
* @var string[]
*/
public static $builtinTypes = [
self::BUILTIN_TYPE_INT,
self::BUILTIN_TYPE_FLOAT,
self::BUILTIN_TYPE_STRING,
self::BUILTIN_TYPE_BOOL,
self::BUILTIN_TYPE_RESOURCE,
self::BUILTIN_TYPE_OBJECT,
self::BUILTIN_TYPE_ARRAY,
self::BUILTIN_TYPE_CALLABLE,
self::BUILTIN_TYPE_FALSE,
self::BUILTIN_TYPE_TRUE,
self::BUILTIN_TYPE_NULL,
self::BUILTIN_TYPE_ITERABLE,
];
/**
* List of PHP builtin collection types.
*
* @var string[]
*/
public static $builtinCollectionTypes = [
self::BUILTIN_TYPE_ARRAY,
self::BUILTIN_TYPE_ITERABLE,
];
private string $builtinType;
private bool $nullable;
private ?string $class;
private bool $collection;
private array $collectionKeyType;
private array $collectionValueType;
/**
* @param Type[]|Type|null $collectionKeyType
* @param Type[]|Type|null $collectionValueType
*
* @throws \InvalidArgumentException
*/
public function __construct(string $builtinType, bool $nullable = false, ?string $class = null, bool $collection = false, array|self|null $collectionKeyType = null, array|self|null $collectionValueType = null)
{
if (!\in_array($builtinType, self::$builtinTypes)) {
throw new \InvalidArgumentException(\sprintf('"%s" is not a valid PHP type.', $builtinType));
}
$this->builtinType = $builtinType;
$this->nullable = $nullable;
$this->class = $class;
$this->collection = $collection;
$this->collectionKeyType = $this->validateCollectionArgument($collectionKeyType, 5, '$collectionKeyType') ?? [];
$this->collectionValueType = $this->validateCollectionArgument($collectionValueType, 6, '$collectionValueType') ?? [];
}
private function validateCollectionArgument(array|self|null $collectionArgument, int $argumentIndex, string $argumentName): ?array
{
if (null === $collectionArgument) {
return null;
}
if (\is_array($collectionArgument)) {
foreach ($collectionArgument as $type) {
if (!$type instanceof self) {
throw new \TypeError(\sprintf('"%s()": Argument #%d (%s) must be of type "%s[]", "%s" or "null", array value "%s" given.', __METHOD__, $argumentIndex, $argumentName, self::class, self::class, get_debug_type($collectionArgument)));
}
}
return $collectionArgument;
}
return [$collectionArgument];
}
/**
* Gets built-in type.
*
* Can be bool, int, float, string, array, object, resource, null, callback or iterable.
*/
public function getBuiltinType(): string
{
return $this->builtinType;
}
public function isNullable(): bool
{
return $this->nullable;
}
/**
* Gets the class name.
*
* Only applicable if the built-in type is object.
*/
public function getClassName(): ?string
{
return $this->class;
}
public function isCollection(): bool
{
return $this->collection;
}
/**
* Gets collection key types.
*
* Only applicable for a collection type.
*
* @return Type[]
*/
public function getCollectionKeyTypes(): array
{
return $this->collectionKeyType;
}
/**
* Gets collection value types.
*
* Only applicable for a collection type.
*
* @return Type[]
*/
public function getCollectionValueTypes(): array
{
return $this->collectionValueType;
}
}

View File

@@ -0,0 +1,198 @@
<?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\Util;
use phpDocumentor\Reflection\PseudoType;
use phpDocumentor\Reflection\PseudoTypes\ConstExpression;
use phpDocumentor\Reflection\PseudoTypes\List_;
use phpDocumentor\Reflection\Type as DocType;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Collection;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\String_;
use Symfony\Component\PropertyInfo\Type;
// Workaround for phpdocumentor/type-resolver < 1.6
// We trigger the autoloader here, so we don't need to trigger it inside the loop later.
class_exists(List_::class);
/**
* Transforms a php doc type to a {@link Type} instance.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Guilhem N. <egetick@gmail.com>
*/
final class PhpDocTypeHelper
{
/**
* Creates a {@see Type} from a PHPDoc type.
*
* @return Type[]
*/
public function getTypes(DocType $varType): array
{
if ($varType instanceof ConstExpression) {
// It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment
return [];
}
$types = [];
$nullable = false;
if ($varType instanceof Nullable) {
$nullable = true;
$varType = $varType->getActualType();
}
if (!$varType instanceof Compound) {
if ($varType instanceof Null_) {
$nullable = true;
}
$type = $this->createType($varType, $nullable);
if (null !== $type) {
$types[] = $type;
}
return $types;
}
$varTypes = [];
for ($typeIndex = 0; $varType->has($typeIndex); ++$typeIndex) {
$type = $varType->get($typeIndex);
if ($type instanceof ConstExpression) {
// It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment
return [];
}
// If null is present, all types are nullable
if ($type instanceof Null_) {
$nullable = true;
continue;
}
if ($type instanceof Nullable) {
$nullable = true;
$type = $type->getActualType();
}
$varTypes[] = $type;
}
foreach ($varTypes as $varType) {
$type = $this->createType($varType, $nullable);
if (null !== $type) {
$types[] = $type;
}
}
return $types;
}
/**
* Creates a {@see Type} from a PHPDoc type.
*/
private function createType(DocType $type, bool $nullable): ?Type
{
$docType = (string) $type;
if ($type instanceof Collection) {
$fqsen = $type->getFqsen();
if ($fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) {
// Workaround for phpdocumentor/type-resolver < 1.6
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $this->getTypes($type->getValueType()));
}
[$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen);
$collection = is_a($class, \Traversable::class, true) || is_a($class, \ArrayAccess::class, true);
// it's safer to fall back to other extractors if the generic type is too abstract
if (!$collection && !class_exists($class)) {
return null;
}
$keys = $this->getTypes($type->getKeyType());
$values = $this->getTypes($type->getValueType());
return new Type($phpType, $nullable, $class, $collection, $keys, $values);
}
// Cannot guess
if (!$docType || 'mixed' === $docType) {
return null;
}
if (str_ends_with($docType, '[]') && $type instanceof Array_) {
$collectionKeyTypes = new Type(Type::BUILTIN_TYPE_INT);
$collectionValueTypes = $this->getTypes($type->getValueType());
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes);
}
if ((str_starts_with($docType, 'list<') || str_starts_with($docType, 'array<')) && $type instanceof Array_) {
// array<value> is converted to x[] which is handled above
// so it's only necessary to handle array<key, value> here
$collectionKeyTypes = $this->getTypes($type->getKeyType());
$collectionValueTypes = $this->getTypes($type->getValueType());
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes);
}
if ($type instanceof PseudoType) {
if ($type->underlyingType() instanceof Integer) {
return new Type(Type::BUILTIN_TYPE_INT, $nullable, null);
} elseif ($type->underlyingType() instanceof String_) {
return new Type(Type::BUILTIN_TYPE_STRING, $nullable, null);
}
}
$docType = $this->normalizeType($docType);
[$phpType, $class] = $this->getPhpTypeAndClass($docType);
if ('array' === $docType) {
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, null, null);
}
return new Type($phpType, $nullable, $class);
}
private function normalizeType(string $docType): string
{
return match ($docType) {
'integer' => 'int',
'boolean' => 'bool',
// real is not part of the PHPDoc standard, so we ignore it
'double' => 'float',
'callback' => 'callable',
'void' => 'null',
default => $docType,
};
}
private function getPhpTypeAndClass(string $docType): array
{
if (\in_array($docType, Type::$builtinTypes)) {
return [$docType, null];
}
if (\in_array($docType, ['parent', 'self', 'static'], true)) {
return ['object', $docType];
}
return ['object', ltrim($docType, '\\')];
}
}

View File

@@ -0,0 +1,211 @@
<?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\Util;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use Symfony\Component\PropertyInfo\PhpStan\NameScope;
use Symfony\Component\PropertyInfo\Type;
/**
* Transforms a php doc tag value to a {@link Type} instance.
*
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*
* @internal
*/
final class PhpStanTypeHelper
{
/**
* Creates a {@see Type} from a PhpDocTagValueNode type.
*
* @return Type[]
*/
public function getTypes(PhpDocTagValueNode $node, NameScope $nameScope): array
{
if ($node instanceof ParamTagValueNode || $node instanceof ReturnTagValueNode || $node instanceof VarTagValueNode) {
return $this->compressNullableType($this->extractTypes($node->type, $nameScope));
}
return [];
}
/**
* Because PhpStan extract null as a separated type when Symfony / PHP compress it in the first available type we
* need this method to mimic how Symfony want null types.
*
* @param Type[] $types
*
* @return Type[]
*/
private function compressNullableType(array $types): array
{
$firstTypeIndex = null;
$nullableTypeIndex = null;
foreach ($types as $k => $type) {
if (null === $firstTypeIndex && Type::BUILTIN_TYPE_NULL !== $type->getBuiltinType() && !$type->isNullable()) {
$firstTypeIndex = $k;
}
if (null === $nullableTypeIndex && Type::BUILTIN_TYPE_NULL === $type->getBuiltinType()) {
$nullableTypeIndex = $k;
}
if (null !== $firstTypeIndex && null !== $nullableTypeIndex) {
break;
}
}
if (null !== $firstTypeIndex && null !== $nullableTypeIndex) {
$firstType = $types[$firstTypeIndex];
$types[$firstTypeIndex] = new Type(
$firstType->getBuiltinType(),
true,
$firstType->getClassName(),
$firstType->isCollection(),
$firstType->getCollectionKeyTypes(),
$firstType->getCollectionValueTypes()
);
unset($types[$nullableTypeIndex]);
}
return array_values($types);
}
/**
* @return Type[]
*/
private function extractTypes(TypeNode $node, NameScope $nameScope): array
{
if ($node instanceof UnionTypeNode) {
$types = [];
foreach ($node->types as $type) {
if ($type instanceof ConstTypeNode) {
// It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment
return [];
}
foreach ($this->extractTypes($type, $nameScope) as $subType) {
$types[] = $subType;
}
}
return $this->compressNullableType($types);
}
if ($node instanceof GenericTypeNode) {
if ('class-string' === $node->type->name) {
return [new Type(Type::BUILTIN_TYPE_STRING)];
}
[$mainType] = $this->extractTypes($node->type, $nameScope);
if (Type::BUILTIN_TYPE_INT === $mainType->getBuiltinType()) {
return [$mainType];
}
$collection = $mainType->isCollection() || is_a($mainType->getClassName(), \Traversable::class, true) || is_a($mainType->getClassName(), \ArrayAccess::class, true);
// it's safer to fall back to other extractors if the generic type is too abstract
if (!$collection && !class_exists($mainType->getClassName()) && !interface_exists($mainType->getClassName(), false)) {
return [];
}
$collectionKeyTypes = $mainType->getCollectionKeyTypes();
$collectionKeyValues = [];
if (1 === \count($node->genericTypes)) {
foreach ($this->extractTypes($node->genericTypes[0], $nameScope) as $subType) {
$collectionKeyValues[] = $subType;
}
} elseif (2 === \count($node->genericTypes)) {
foreach ($this->extractTypes($node->genericTypes[0], $nameScope) as $keySubType) {
$collectionKeyTypes[] = $keySubType;
}
foreach ($this->extractTypes($node->genericTypes[1], $nameScope) as $valueSubType) {
$collectionKeyValues[] = $valueSubType;
}
}
return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), $collection, $collectionKeyTypes, $collectionKeyValues)];
}
if ($node instanceof ArrayShapeNode) {
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)];
}
if ($node instanceof ArrayTypeNode) {
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], $this->extractTypes($node->type, $nameScope))];
}
if ($node instanceof CallableTypeNode || $node instanceof CallableTypeParameterNode) {
return [new Type(Type::BUILTIN_TYPE_CALLABLE)];
}
if ($node instanceof NullableTypeNode) {
$subTypes = $this->extractTypes($node->type, $nameScope);
if (\count($subTypes) > 1) {
$subTypes[] = new Type(Type::BUILTIN_TYPE_NULL);
return $subTypes;
}
return [new Type($subTypes[0]->getBuiltinType(), true, $subTypes[0]->getClassName(), $subTypes[0]->isCollection(), $subTypes[0]->getCollectionKeyTypes(), $subTypes[0]->getCollectionValueTypes())];
}
if ($node instanceof ThisTypeNode) {
return [new Type(Type::BUILTIN_TYPE_OBJECT, false, $nameScope->resolveRootClass())];
}
if ($node instanceof IdentifierTypeNode) {
if (\in_array($node->name, Type::$builtinTypes)) {
return [new Type($node->name, false, null, \in_array($node->name, Type::$builtinCollectionTypes))];
}
return match ($node->name) {
'integer',
'positive-int',
'negative-int' => [new Type(Type::BUILTIN_TYPE_INT)],
'double' => [new Type(Type::BUILTIN_TYPE_FLOAT)],
'list',
'non-empty-list' => [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT))],
'non-empty-array' => [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)],
'mixed' => [], // mixed seems to be ignored in all other extractors
'parent' => [new Type(Type::BUILTIN_TYPE_OBJECT, false, $node->name)],
'static',
'self' => [new Type(Type::BUILTIN_TYPE_OBJECT, false, $nameScope->resolveRootClass())],
'class-string',
'html-escaped-string',
'lowercase-string',
'non-empty-lowercase-string',
'non-empty-string',
'numeric-string',
'trait-string',
'interface-string',
'literal-string' => [new Type(Type::BUILTIN_TYPE_STRING)],
'void' => [new Type(Type::BUILTIN_TYPE_NULL)],
'scalar' => [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT), new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_BOOL)],
'number' => [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT)],
'numeric' => [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT), new Type(Type::BUILTIN_TYPE_STRING)],
'array-key' => [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)],
default => [new Type(Type::BUILTIN_TYPE_OBJECT, false, $nameScope->resolveStringName($node->name))],
};
}
return [];
}
}