This commit is contained in:
Marek
2026-03-24 00:04:55 +01:00
commit c5229e48ed
4225 changed files with 511461 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
CHANGELOG
=========
7.3
---
* Add support for `non-positive-int`, `non-negative-int` and `non-zero-int` PHPStan types to `PhpStanExtractor`
* Add `PropertyDescriptionExtractorInterface` to `PhpStanExtractor`
* Deprecate the `Type` class, use `Symfony\Component\TypeInfo\Type` class from `symfony/type-info` instead
* Deprecate the `PropertyTypeExtractorInterface::getTypes()` method, use `PropertyTypeExtractorInterface::getType()` instead
* Deprecate the `ConstructorArgumentTypeExtractorInterface::getTypesFromConstructor()` method, use `ConstructorArgumentTypeExtractorInterface::getTypeFromConstructor()` instead
7.1
---
* Introduce `PropertyDocBlockExtractorInterface` to extract a property's doc block
* Restrict access to `PhpStanExtractor` based on visibility
* Add `PropertyTypeExtractorInterface::getType()` as experimental
6.4
---
* Make properties writable when a setter in camelCase exists, similar to the camelCase getter
6.1
---
* Add support for phpDocumentor and PHPStan pseudo-types
* Add PHP 8.0 promoted properties `@param` mutation support to `PhpDocExtractor`
* Add PHP 8.0 promoted properties `@param` mutation support to `PhpStanExtractor`
6.0
---
* Remove the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead
* Remove the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()` in favor of `enable_magic_methods_extraction`
5.4
---
* Add PhpStanExtractor
5.3
---
* Add support for multiple types for collection keys & values
* Deprecate the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead
5.2.0
-----
* deprecated the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()` in favor of `enable_magic_methods_extraction`
5.1.0
-----
* Add support for extracting accessor and mutator via PHP Reflection
4.3.0
-----
* Added the ability to extract private and protected properties and methods on `ReflectionExtractor`
* Added the ability to extract property type based on its initial value
4.2.0
-----
* added `PropertyInitializableExtractorInterface` to test if a property can be initialized through the constructor (implemented by `ReflectionExtractor`)
3.3.0
-----
* Added `PropertyInfoPass`

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,51 @@
<?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;
public function process(ContainerBuilder $container): void
{
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,39 @@
<?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 as LegacyType;
use Symfony\Component\TypeInfo\Type;
/**
* Infers the constructor argument type.
*
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
*/
interface ConstructorArgumentTypeExtractorInterface
{
/**
* Gets types of an argument from constructor.
*
* @deprecated since Symfony 7.3, use "getTypeFromConstructor" instead
*
* @return LegacyType[]|null
*/
public function getTypesFromConstructor(string $class, string $property): ?array;
/**
* Gets type of an argument from constructor.
*
* @param class-string $class
*/
public function getTypeFromConstructor(string $class, string $property): ?Type;
}

View File

@@ -0,0 +1,59 @@
<?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;
use Symfony\Component\TypeInfo\Type;
/**
* 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 getType(string $class, string $property, array $context = []): ?Type
{
foreach ($this->extractors as $extractor) {
if (null !== $value = $extractor->getTypeFromConstructor($class, $property)) {
return $value;
}
}
return null;
}
/**
* @deprecated since Symfony 7.3, use "getType" instead
*/
public function getTypes(string $class, string $property, array $context = []): ?array
{
trigger_deprecation('symfony/property-info', '7.3', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class);
foreach ($this->extractors as $extractor) {
$value = $extractor->getTypesFromConstructor($class, $property);
if (null !== $value) {
return $value;
}
}
return null;
}
}

View File

@@ -0,0 +1,541 @@
<?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\PropertyDocBlockExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper;
use Symfony\Component\TypeInfo\Exception\LogicException;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
/**
* Extracts data using a PHPDoc parser.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final
*/
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface, PropertyDocBlockExtractorInterface
{
public const PROPERTY = 0;
public const ACCESSOR = 1;
public const MUTATOR = 2;
/**
* @var array<string, array{DocBlock|null, int|null, string|null, string|null}>
*/
private array $docBlocks = [];
/**
* @var array<string, array{DocBlock, string}|false>
*/
private array $promotedPropertyDocBlocks = [];
/**
* @var Context[]
*/
private array $contexts = [];
private DocBlockFactoryInterface $docBlockFactory;
private ContextFactory $contextFactory;
private TypeContextFactory $typeContextFactory;
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->typeContextFactory = new TypeContextFactory();
$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
{
$docBlockData = $this->getPromotedPropertyDocBlockData($class, $property);
if ($docBlockData && $shortDescription = $this->getShortDescriptionFromDocBlock($docBlockData[0])) {
return $shortDescription;
}
[$docBlock] = $this->findDocBlock($class, $property);
if (!$docBlock) {
return null;
}
return $this->getShortDescriptionFromDocBlock($docBlock);
}
public function getLongDescription(string $class, string $property, array $context = []): ?string
{
$docBlockData = $this->getPromotedPropertyDocBlockData($class, $property);
if ($docBlockData && '' !== $contents = $docBlockData[0]->getDescription()->render()) {
return $contents;
}
[$docBlock] = $this->findDocBlock($class, $property);
if (!$docBlock) {
return null;
}
$contents = $docBlock->getDescription()->render();
return '' === $contents ? null : $contents;
}
/**
* @deprecated since Symfony 7.3, use "getType" instead
*/
public function getTypes(string $class, string $property, array $context = []): ?array
{
trigger_deprecation('symfony/property-info', '7.3', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class);
/** @var DocBlock $docBlock */
[$docBlock, $source, $prefix, $declaringClass] = $this->findDocBlock($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':
$resolvedClass = $declaringClass ?? $class;
break;
case 'static':
$resolvedClass = $class;
break;
case 'parent':
if (false !== $resolvedClass = $parentClass ??= get_parent_class($declaringClass ?? $class)) {
break;
}
// no break
default:
$types[] = $type;
continue 2;
}
$types[] = new LegacyType(LegacyType::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 LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $types[0])];
}
/**
* @deprecated since Symfony 7.3, use "getTypeFromConstructor" instead
*/
public function getTypesFromConstructor(string $class, string $property): ?array
{
trigger_deprecation('symfony/property-info', '7.3', 'The "%s()" method is deprecated, use "%s::getTypeFromConstructor()" instead.', __METHOD__, self::class);
$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);
}
public function getType(string $class, string $property, array $context = []): ?Type
{
if ([$propertyDocBlock, $propertyDeclaringClass] = $this->getPromotedPropertyDocBlockData($class, $property)) {
if ($type = $this->getTypeFromDocBlock($propertyDocBlock, self::PROPERTY, $class, $propertyDeclaringClass, null)) {
return $type;
}
}
[$docBlock, $source, $prefix, $declaringClass] = $this->findDocBlock($class, $property);
if (!$docBlock) {
return null;
}
return $this->getTypeFromDocBlock($docBlock, $source, $class, $declaringClass, $prefix);
}
public function getTypeFromConstructor(string $class, string $property): ?Type
{
if (!$docBlock = $this->getDocBlockFromConstructor($class, $property)) {
return null;
}
$types = [];
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
foreach ($docBlock->getTagsByName('param') as $tag) {
if ($tag instanceof InvalidTag || !$tagType = $tag->getType()) {
continue;
}
$types[] = $this->phpDocTypeHelper->getType($tagType);
}
return $types[0] ?? null;
}
public function getDocBlock(string $class, string $property): ?DocBlock
{
return $this->findDocBlock($class, $property)[0];
}
private function getDocBlockFromConstructor(string $class, string $property): ?DocBlock
{
try {
$reflectionClass = new \ReflectionClass($class);
} catch (\ReflectionException) {
return null;
}
if (!$reflectionConstructor = $reflectionClass->getConstructor()) {
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'), static 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, string|null}
*/
private function findDocBlock(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);
if ($reflectionProperty?->isPromoted() && $docBlock = $this->getDocBlockFromConstructor($reflectionProperty->class, $property)) {
$data = [$docBlock, self::MUTATOR, null, $reflectionProperty->class];
} elseif ([$docBlock, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
$data = [$docBlock, self::PROPERTY, null, $declaringClass];
} else {
$data = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR)
?? $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR)
?? [null, null, null, null];
}
return $this->docBlocks[$propertyHash] = $data;
}
/**
* @return array{DocBlock, string}|null
*/
private function getDocBlockFromProperty(string $class, string $property, ?string $originalClass = null): ?array
{
$originalClass ??= $class;
// 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, $reflector->isTrait() ? $originalClass : $reflector->getName());
}
}
$context = $this->createFromReflector($reflector);
try {
$declaringClass = $reflector->isTrait() ? $originalClass : $reflector->getName();
return [$this->docBlockFactory->create($reflectionProperty, $context), $declaringClass];
} catch (\InvalidArgumentException) {
return null;
} catch (\RuntimeException) {
// Workaround for phpdocumentor/reflection-docblock < 6 not supporting ?Type<...> syntax
if (($rawDoc = $reflectionProperty->getDocComment()) && $docBlock = $this->getNullableGenericDocBlock($rawDoc, $context)) {
return [$docBlock, $declaringClass ?? ($reflector->isTrait() ? $originalClass : $reflector->getName())];
}
return null;
}
}
/**
* @return array{DocBlock, int, ?string, string}|null
*/
private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type, ?string $originalClass = null): ?array
{
$originalClass ??= $class;
$prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
$prefix = null;
$method = null;
foreach ($prefixes as $prefix) {
$methodName = $prefix.$ucFirstProperty;
try {
$method = new \ReflectionMethod($class, $methodName);
if ($method->isStatic()) {
continue;
}
if (self::ACCESSOR === $type && \in_array((string) $method->getReturnType(), ['void', 'never'], true)) {
continue;
}
if (
(self::ACCESSOR === $type && !$method->getNumberOfRequiredParameters())
|| (self::MUTATOR === $type && $method->getNumberOfParameters() >= 1)
) {
break;
}
} catch (\ReflectionException) {
// Try the next prefix if the method doesn't exist
}
}
if (!$method) {
return null;
}
$reflector = $method->getDeclaringClass();
foreach ($reflector->getTraits() as $trait) {
if ($trait->hasMethod($methodName)) {
return $this->getDocBlockFromMethod($trait->getName(), $ucFirstProperty, $type, $reflector->isTrait() ? $originalClass : $reflector->getName());
}
}
$context = $this->createFromReflector($reflector);
$prefix = self::ACCESSOR === $type ? null : $prefix;
try {
$declaringClass = $reflector->isTrait() ? $originalClass : $reflector->getName();
return [$this->docBlockFactory->create($method, $context), $type, $prefix, $declaringClass];
} catch (\InvalidArgumentException) {
return null;
} catch (\RuntimeException) {
// Workaround for phpdocumentor/reflection-docblock < 6 not supporting ?Type<...> syntax
if (($rawDoc = $method->getDocComment()) && $docBlock = $this->getNullableGenericDocBlock($rawDoc, $context)) {
return [$docBlock, $type, $prefix, $declaringClass ?? ($reflector->isTrait() ? $originalClass : $reflector->getName())];
}
return null;
}
}
private function getNullableGenericDocBlock(string $rawDoc, Context $context): ?DocBlock
{
// Converts "?Type<...>" to "Type<...>|null"
if ($rawDoc === $processedDoc = preg_replace('/@(var|param|return)\s+\?(\S+)/', '@$1 $2|null', $rawDoc)) {
return null;
}
try {
return $this->docBlockFactory->create($processedDoc, $context);
} 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();
return $this->contexts[$cacheKey] ??= $this->contextFactory->createFromReflector($reflector);
}
/**
* @return array{DocBlock, string}|null
*/
private function getPromotedPropertyDocBlockData(string $class, string $property): ?array
{
$propertyHash = $class.'::'.$property;
if (isset($this->promotedPropertyDocBlocks[$propertyHash])) {
return false === $this->promotedPropertyDocBlocks[$propertyHash] ? null : $this->promotedPropertyDocBlocks[$propertyHash];
}
try {
$reflectionProperty = new \ReflectionProperty($class, $property);
} catch (\ReflectionException) {
$this->promotedPropertyDocBlocks[$propertyHash] = false;
return null;
}
if (!$reflectionProperty->isPromoted() || !$data = $this->getDocBlockFromProperty($class, $property)) {
$this->promotedPropertyDocBlocks[$propertyHash] = false;
return null;
}
return $this->promotedPropertyDocBlocks[$propertyHash] = $data;
}
private function getTypeFromDocBlock(DocBlock $docBlock, int $source, string $class, ?string $declaringClass, ?string $prefix): ?Type
{
$tag = match ($source) {
self::PROPERTY => 'var',
self::ACCESSOR => 'return',
self::MUTATOR => 'param',
};
$types = [];
$typeContext = $this->typeContextFactory->createFromClassName($class, $declaringClass ?? $class);
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
foreach ($docBlock->getTagsByName($tag) as $tag) {
if ($tag instanceof InvalidTag || !$tagType = $tag->getType()) {
continue;
}
$type = $this->phpDocTypeHelper->getType($tagType);
if (!$type instanceof ObjectType) {
$types[] = $type;
continue;
}
$normalizedClassName = match ($type->getClassName()) {
'self' => $typeContext->getDeclaringClass(),
'static' => $typeContext->getCalledClass(),
default => $type->getClassName(),
};
if ('parent' === $normalizedClassName) {
try {
$normalizedClassName = $typeContext->getParentClass();
} catch (LogicException) {
// if there is no parent for the current class, we keep the "parent" raw string
}
}
$types[] = $type->isNullable() ? Type::nullable(Type::object($normalizedClassName)) : Type::object($normalizedClassName);
}
if (!$type = $types[0] ?? null) {
return null;
}
if (self::MUTATOR !== $source || !\in_array($prefix, $this->arrayMutatorPrefixes, true)) {
return $type;
}
return Type::list($type);
}
private function getShortDescriptionFromDocBlock(DocBlock $docBlock): ?string
{
if ($shortDescription = $docBlock->getSummary()) {
return $shortDescription;
}
foreach ($docBlock->getTagsByName('var') as $var) {
if ($var && !$var instanceof InvalidTag && $varDescription = $var->getDescription()->render()) {
return $varDescription;
}
}
foreach ($docBlock->getTagsByName('param') as $param) {
if (!$param instanceof DocBlock\Tags\Param) {
continue;
}
if ($paramDescription = $param->getDescription()?->render()) {
return $paramDescription;
}
}
return null;
}
}

View File

@@ -0,0 +1,585 @@
<?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\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
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\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeContext\TypeContext;
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
/**
* Extracts data using PHPStan parser.
*
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*/
final class PhpStanExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
{
private const PROPERTY = 0;
private const ACCESSOR = 1;
private const MUTATOR = 2;
private PhpDocParser $phpDocParser;
private Lexer $lexer;
private StringTypeResolver $stringTypeResolver;
private TypeContextFactory $typeContextFactory;
/** @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;
/** @var array<string, TypeContext> */
private array $contexts = [];
/**
* @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, private bool $allowPrivateAccess = true)
{
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->stringTypeResolver = new StringTypeResolver();
$this->typeContextFactory = new TypeContextFactory($this->stringTypeResolver);
}
/**
* @deprecated since Symfony 7.3, use "getType" instead
*/
public function getTypes(string $class, string $property, array $context = []): ?array
{
trigger_deprecation('symfony/property-info', '7.3', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class);
/** @var PhpDocNode|null $docNode */
[$docNode, $source, $prefix, $declaringClass] = $this->getDocBlock($class, $property);
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;
}
$typeContext = $this->contexts[$class.'/'.$declaringClass] ??= $this->typeContextFactory->createFromClassName($class, $declaringClass);
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode->value, $typeContext) 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 LegacyType(LegacyType::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 LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $types[0])];
}
/**
* @deprecated since Symfony 7.3, use "getTypeFromConstructor" instead
*
* @return LegacyType[]|null
*/
public function getTypesFromConstructor(string $class, string $property): ?array
{
trigger_deprecation('symfony/property-info', '7.3', 'The "%s()" method is deprecated, use "%s::getTypeFromConstructor()" instead.', __METHOD__, self::class);
if (null === $tagDocNode = $this->getDocBlockFromConstructor($class, $property)) {
return $this->getTypes($class, $property);
}
$types = [];
foreach ($this->phpStanTypeHelper->getTypes($tagDocNode, $this->typeContextFactory->createFromClassName($class)) as $type) {
$types[] = $type;
}
if (!isset($types[0])) {
return null;
}
return $types;
}
public function getType(string $class, string $property, array $context = []): ?Type
{
/** @var PhpDocNode|null $docNode */
[$docNode, $source, $prefix, $declaringClass] = $this->getDocBlock($class, $property);
if (null === $docNode) {
return null;
}
$typeContext = $this->typeContextFactory->createFromClassName($class, $declaringClass);
$tag = match ($source) {
self::PROPERTY => '@var',
self::ACCESSOR => '@return',
self::MUTATOR => '@param',
default => 'invalid',
};
$types = [];
foreach ($docNode->getTagsByName($tag) as $tagDocNode) {
if (!$tagDocNode->value instanceof ParamTagValueNode && !$tagDocNode->value instanceof ReturnTagValueNode && !$tagDocNode->value instanceof VarTagValueNode) {
continue;
}
if ($tagDocNode->value instanceof ParamTagValueNode && null === $prefix && $tagDocNode->value->parameterName !== '$'.$property) {
continue;
}
try {
$types[] = $this->stringTypeResolver->resolve((string) $tagDocNode->value->type, $typeContext);
} catch (UnsupportedException) {
}
}
if (!$type = $types[0] ?? null) {
return null;
}
if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) {
return $type;
}
return Type::list($type);
}
public function getTypeFromConstructor(string $class, string $property): ?Type
{
$declaringClass = $class;
if (!$tagDocNode = $this->getDocBlockFromConstructor($declaringClass, $property)) {
return $this->getType($class, $property);
}
$typeContext = $this->typeContextFactory->createFromClassName($class, $declaringClass);
return $this->stringTypeResolver->resolve((string) $tagDocNode->type, $typeContext);
}
public function getShortDescription(string $class, string $property, array $context = []): ?string
{
/** @var PhpDocNode|null $docNode */
[$docNode, $constructorDocNode] = $this->getDocBlockFromProperty($class, $property);
if (null === $docNode && null === $constructorDocNode) {
return null;
}
if ($docNode && $shortDescription = $this->getShortDescriptionFromDocNode($docNode, $property)) {
return $shortDescription;
}
if ($constructorDocNode) {
return $this->getShortDescriptionFromDocNode($constructorDocNode, $property);
}
return null;
}
public function getLongDescription(string $class, string $property, array $context = []): ?string
{
/** @var PhpDocNode|null $docNode */
[$docNode, $constructorDocNode] = $this->getDocBlockFromProperty($class, $property);
if (null === $docNode && null === $constructorDocNode) {
return null;
}
if ($docNode && $longDescription = $this->getDescriptionsFromDocNode($docNode)[1]) {
return $longDescription;
}
return $constructorDocNode ? $this->getDescriptionsFromDocNode($constructorDocNode)[1] : null;
}
/**
* A docblock is split into a template marker, a short description, an optional long description and a tags section.
*
* - The template marker is either empty, #@+ or #@-.
* - The short description is started from a non-tag character, and until one or multiple newlines.
* - The long description (optional) is started from a non-tag character, and until a new line is encountered followed by a tag.
* - Tags, and the remaining characters
*
* This method returns the short and the long descriptions.
*
* @return array{0: ?string, 1: ?string}
*/
private function getDescriptionsFromDocNode(PhpDocNode $docNode): array
{
$isTemplateMarker = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && ('#@+' === $node->text || '#@-' === $node->text);
$shortDescription = '';
$longDescription = '';
$shortDescriptionCompleted = false;
// BC layer for phpstan/phpdoc-parser < 2.0
if (!class_exists(ParserConfig::class)) {
$isNewLine = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && '' === $node->text;
foreach ($docNode->children as $child) {
if (!$child instanceof PhpDocTextNode) {
break;
}
if ($isTemplateMarker($child)) {
continue;
}
if ($isNewLine($child) && !$shortDescriptionCompleted) {
if ($shortDescription) {
$shortDescriptionCompleted = true;
}
continue;
}
if (!$shortDescriptionCompleted) {
$shortDescription = \sprintf("%s\n%s", $shortDescription, $child->text);
continue;
}
$longDescription = \sprintf("%s\n%s", $longDescription, $child->text);
}
} else {
foreach ($docNode->children as $child) {
if (!$child instanceof PhpDocTextNode) {
break;
}
if ($isTemplateMarker($child)) {
continue;
}
foreach (explode("\n", $child->text) as $line) {
if ('' === $line && !$shortDescriptionCompleted) {
if ($shortDescription) {
$shortDescriptionCompleted = true;
}
continue;
}
if (!$shortDescriptionCompleted) {
$shortDescription = \sprintf("%s\n%s", $shortDescription, $line);
continue;
}
$longDescription = \sprintf("%s\n%s", $longDescription, $line);
}
}
}
$shortDescription = trim(preg_replace('/^#@[+-]{1}/m', '', $shortDescription), "\n");
$longDescription = trim($longDescription, "\n");
return [
$shortDescription ?: null,
$longDescription ?: null,
];
}
private function getShortDescriptionFromDocNode(PhpDocNode $docNode, string $property): ?string
{
if ($shortDescription = $this->getDescriptionsFromDocNode($docNode)[0]) {
return $shortDescription;
}
foreach ($docNode->getVarTagValues() as $var) {
if (!$var->description) {
continue;
}
if (null !== $var->variableName && '' !== $var->variableName && '$'.$property !== $var->variableName) {
continue;
}
return $var->description;
}
foreach ($docNode->getTagsByName('@param') as $tagNode) {
if (!$tagNode instanceof PhpDocTagNode || !$tagNode->value instanceof ParamTagValueNode) {
continue;
}
if ('$'.$property !== $tagNode->value->parameterName) {
continue;
}
if ($tagNode->value->description) {
return $tagNode->value->description;
}
}
return null;
}
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;
}
$class = $reflectionConstructor->class;
$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, $constructorDocBlock, $source, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
if (!$docBlock?->getTagsByName('@var') && $constructorDocBlock) {
$docBlock = $constructorDocBlock;
}
$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, ?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;
}
if (!$this->canAccessMemberBasedOnItsVisibility($reflectionProperty)) {
return null;
}
$reflector = $reflectionProperty->getDeclaringClass();
foreach ($reflector->getTraits() as $trait) {
if ($trait->hasProperty($property)) {
return $this->getDocBlockFromProperty($trait->getName(), $property);
}
}
$rawDocNode = $reflectionProperty->getDocComment();
$phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null;
$constructorPhpDocNode = null;
if ($reflectionProperty->isPromoted()) {
$constructorRawDocNode = (new \ReflectionMethod($class, '__construct'))->getDocComment();
$constructorPhpDocNode = $constructorRawDocNode ? $this->getPhpDocNode($constructorRawDocNode) : null;
}
$source = self::PROPERTY;
if (!$phpDocNode?->getTagsByName('@var') && $constructorPhpDocNode) {
$source = self::MUTATOR;
}
if (!$phpDocNode && !$constructorPhpDocNode) {
return null;
}
return [$phpDocNode, $constructorPhpDocNode, $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;
$method = null;
foreach ($prefixes as $prefix) {
$methodName = $prefix.$ucFirstProperty;
try {
$method = new \ReflectionMethod($class, $methodName);
if ($method->isStatic()) {
continue;
}
if (self::ACCESSOR === $type && \in_array((string) $method->getReturnType(), ['void', 'never'], true)) {
continue;
}
if (
(
(self::ACCESSOR === $type && !$method->getNumberOfRequiredParameters())
|| (self::MUTATOR === $type && $method->getNumberOfParameters() >= 1)
)
&& $this->canAccessMemberBasedOnItsVisibility($method)
) {
break;
}
} catch (\ReflectionException) {
// Try the next prefix if the method doesn't exist
}
}
if (!$method) {
return null;
}
if (null === $rawDocNode = $method->getDocComment() ?: null) {
return null;
}
$phpDocNode = $this->getPhpDocNode($rawDocNode);
return [$phpDocNode, $prefix, $method->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;
}
private function canAccessMemberBasedOnItsVisibility(\ReflectionProperty|\ReflectionMethod $member): bool
{
return $this->allowPrivateAccess || $member->isPublic();
}
}

File diff suppressed because it is too large Load Diff

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'] || \in_array('*', $context['serializer_groups'], true) || array_intersect($serializerAttributeMetadata->getGroups(), $context['serializer_groups']))) {
$properties[] = $serializerAttributeMetadata->getName();
}
}
return $properties;
}
}

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

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,36 @@
<?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 phpDocumentor\Reflection\DocBlock;
/**
* Extract a property's doc block.
*
* A property's doc block may be located on a constructor promoted argument, on
* the property or on a mutator for that property.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface PropertyDocBlockExtractorInterface
{
/**
* Gets the first available doc block for a property. It finds the doc block
* by the following priority:
* - constructor promoted argument,
* - the class property,
* - a mutator method for that property.
*
* If no doc block is found, it will return null.
*/
public function getDocBlock(string $class, string $property): ?DocBlock;
}

View File

@@ -0,0 +1,144 @@
<?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;
use Symfony\Component\PropertyInfo\Util\LegacyTypeConverter;
use Symfony\Component\TypeInfo\Type;
/**
* 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 getType(string $class, string $property, array $context = []): ?Type
{
try {
$serializedArguments = serialize([$class, $property, $context]);
} catch (\Exception) {
// If arguments are not serializable, skip the cache
if (method_exists($this->propertyInfoExtractor, 'getType')) {
return $this->propertyInfoExtractor->getType($class, $property, $context);
}
return LegacyTypeConverter::toTypeInfoType($this->propertyInfoExtractor->getTypes($class, $property, $context));
}
// Calling rawurlencode escapes special characters not allowed in PSR-6's keys
$key = rawurlencode('getType.'.$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();
}
if (method_exists($this->propertyInfoExtractor, 'getType')) {
$value = $this->propertyInfoExtractor->getType($class, $property, $context);
} else {
$value = LegacyTypeConverter::toTypeInfoType($this->propertyInfoExtractor->getTypes($class, $property, $context));
}
$item->set($value);
$this->cacheItemPool->save($item);
return $this->arrayCache[$key] = $value;
}
/**
* @deprecated since Symfony 7.3, use "getType" instead
*/
public function getTypes(string $class, string $property, array $context = []): ?array
{
trigger_deprecation('symfony/property-info', '7.3', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class);
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,119 @@
<?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 Symfony\Component\PropertyInfo\Util\LegacyTypeConverter;
use Symfony\Component\TypeInfo\Type;
/**
* 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 getType(string $class, string $property, array $context = []): ?Type
{
foreach ($this->typeExtractors as $extractor) {
if (!method_exists($extractor, 'getType')) {
$legacyTypes = $extractor->getTypes($class, $property, $context);
if (null !== $legacyTypes) {
return LegacyTypeConverter::toTypeInfoType($legacyTypes);
}
continue;
}
if (null !== $value = $extractor->getType($class, $property, $context)) {
return $value;
}
}
return null;
}
/**
* @deprecated since Symfony 7.3, use "getType" instead
*/
public function getTypes(string $class, string $property, array $context = []): ?array
{
trigger_deprecation('symfony/property-info', '7.3', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class);
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 = []): ?array;
}

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,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;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type;
/**
* Type Extractor Interface.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @method Type|null getType(string $class, string $property, array $context = [])
*/
interface PropertyTypeExtractorInterface
{
/**
* Gets types of a property.
*
* @deprecated since Symfony 7.3, use "getType" instead
*
* @return LegacyType[]|null
*/
public function getTypes(string $class, string $property, array $context = []): ?array;
}

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;
}

View File

@@ -0,0 +1,14 @@
PropertyInfo Component
======================
The PropertyInfo component extracts information about PHP class' properties
using metadata of popular sources.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/property_info.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,167 @@
<?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;
trigger_deprecation('symfony/property-info', '7.3', 'The "%s" class is deprecated. Use "%s" class from "symfony/type-info" instead.', Type::class, \Symfony\Component\TypeInfo\Type::class);
/**
* Type value object (immutable).
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @deprecated since Symfony 7.3, use "Symfony\Component\TypeInfo\Type" class from "symfony/type-info" instead
*
* @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 array $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 array $builtinCollectionTypes = [
self::BUILTIN_TYPE_ARRAY,
self::BUILTIN_TYPE_ITERABLE,
];
private array $collectionKeyType;
private array $collectionValueType;
/**
* @param Type[]|Type|null $collectionKeyType
* @param Type[]|Type|null $collectionValueType
*
* @throws \InvalidArgumentException
*/
public function __construct(
private string $builtinType,
private bool $nullable = false,
private ?string $class = null,
private bool $collection = false,
array|self|null $collectionKeyType = null,
array|self|null $collectionValueType = null,
) {
if (!\in_array($builtinType, self::$builtinTypes, true)) {
throw new \InvalidArgumentException(\sprintf('"%s" is not a valid PHP type.', $builtinType));
}
$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,94 @@
<?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 Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type;
/**
* @internal
*/
class LegacyTypeConverter
{
/**
* @param LegacyType[]|null $legacyTypes
*/
public static function toTypeInfoType(?array $legacyTypes): ?Type
{
if (null === $legacyTypes || [] === $legacyTypes) {
return null;
}
$nullable = false;
$types = [];
foreach ($legacyTypes as $legacyType) {
switch ($legacyType->getBuiltinType()) {
case LegacyType::BUILTIN_TYPE_ARRAY:
$typeInfoType = Type::array(self::toTypeInfoType($legacyType->getCollectionValueTypes()), self::toTypeInfoType($legacyType->getCollectionKeyTypes()));
break;
case LegacyType::BUILTIN_TYPE_BOOL:
$typeInfoType = Type::bool();
break;
case LegacyType::BUILTIN_TYPE_CALLABLE:
$typeInfoType = Type::callable();
break;
case LegacyType::BUILTIN_TYPE_FALSE:
$typeInfoType = Type::false();
break;
case LegacyType::BUILTIN_TYPE_FLOAT:
$typeInfoType = Type::float();
break;
case LegacyType::BUILTIN_TYPE_INT:
$typeInfoType = Type::int();
break;
case LegacyType::BUILTIN_TYPE_ITERABLE:
$typeInfoType = Type::iterable(self::toTypeInfoType($legacyType->getCollectionValueTypes()), self::toTypeInfoType($legacyType->getCollectionKeyTypes()));
break;
case LegacyType::BUILTIN_TYPE_OBJECT:
if ($legacyType->isCollection()) {
$typeInfoType = Type::collection(Type::object($legacyType->getClassName()), self::toTypeInfoType($legacyType->getCollectionValueTypes()), self::toTypeInfoType($legacyType->getCollectionKeyTypes()));
} else {
$typeInfoType = Type::object($legacyType->getClassName());
}
break;
case LegacyType::BUILTIN_TYPE_RESOURCE:
$typeInfoType = Type::resource();
break;
case LegacyType::BUILTIN_TYPE_STRING:
$typeInfoType = Type::string();
break;
case LegacyType::BUILTIN_TYPE_TRUE:
$typeInfoType = Type::true();
break;
default:
$typeInfoType = null;
break;
}
if (LegacyType::BUILTIN_TYPE_NULL === $legacyType->getBuiltinType() || $legacyType->isNullable()) {
$nullable = true;
}
if (null !== $typeInfoType) {
$types[] = $typeInfoType;
}
}
if (1 === \count($types)) {
return $nullable ? Type::nullable($types[0]) : $types[0];
}
return $nullable ? Type::nullable(Type::union(...$types)) : Type::union(...$types);
}
}

View File

@@ -0,0 +1,475 @@
<?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\Generic;
use phpDocumentor\Reflection\PseudoTypes\List_;
use phpDocumentor\Reflection\PseudoTypes\Scalar;
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\Mixed_;
use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Scalar as LegacyScalar;
use phpDocumentor\Reflection\Types\String_;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\TypeIdentifier;
// 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 LegacyType} from a PHPDoc type.
*
* @deprecated since Symfony 7.3, use "getType" instead
*
* @return LegacyType[]
*/
public function getTypes(DocType $varType): array
{
trigger_deprecation('symfony/property-info', '7.3', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class);
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 LegacyScalar || $varType instanceof Scalar) {
return [
new LegacyType(LegacyType::BUILTIN_TYPE_BOOL),
new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT),
new LegacyType(LegacyType::BUILTIN_TYPE_INT),
new LegacyType(LegacyType::BUILTIN_TYPE_STRING),
];
}
if (!$varType instanceof Compound) {
if ($varType instanceof Null_) {
$nullable = true;
}
$type = $this->createLegacyType($varType, $nullable);
if (null !== $type) {
$types[] = $type;
}
return $types;
}
$varTypes = [];
for ($typeIndex = 0; $varType->has($typeIndex); ++$typeIndex) {
$type = $varType->get($typeIndex);
if ($type instanceof Mixed_) {
return [];
}
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->createLegacyType($varType, $nullable);
if (null !== $type) {
$types[] = $type;
}
}
return $types;
}
/**
* Creates a {@see Type} from a PHPDoc type.
*/
public function getType(DocType $varType): ?Type
{
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 null;
}
$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);
return $nullable ? Type::nullable($type) : $type;
}
$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 null;
}
// 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;
}
$unionTypes = [];
foreach ($varTypes as $varType) {
if (!$t = $this->createType($varType)) {
continue;
}
if ($t instanceof BuiltinType && TypeIdentifier::MIXED === $t->getTypeIdentifier()) {
return Type::mixed();
}
$unionTypes[] = $t;
}
if (!$unionTypes) {
return null;
}
$type = 1 === \count($unionTypes) ? $unionTypes[0] : Type::union(...$unionTypes);
return $nullable ? Type::nullable($type) : $type;
}
/**
* Creates a {@see LegacyType} from a PHPDoc type.
*/
private function createLegacyType(DocType $type, bool $nullable): ?LegacyType
{
$docType = (string) $type;
if ('mixed[]' === $docType) {
$docType = 'array';
} elseif ('array' !== $docType && $type instanceof Array_ && $this->hasNoExplicitKeyType($type)) {
$docType = \sprintf('%s[]', $type->getValueType());
}
if ($type instanceof Collection || $type instanceof Generic) {
$fqsen = $type->getFqsen();
if ($type instanceof Collection && $fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) {
// Workaround for phpdocumentor/type-resolver < 1.6
return new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true, new LegacyType(LegacyType::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, false) && !interface_exists($class, false)) {
return null;
}
if ($type instanceof Generic) {
$genericTypes = $type->getTypes();
if (null === $valueType = $genericTypes[1] ?? null) {
$keyType = new Compound([new String_(), new Integer()]);
$valueType = $genericTypes[0] ?? new Mixed_();
} else {
$keyType = $genericTypes[0];
}
} else {
$keyType = $type->getKeyType();
$valueType = $type->getValueType();
}
$keys = $this->getTypes($keyType);
$values = $this->getTypes($valueType);
return new LegacyType($phpType, $nullable, $class, $collection, $keys, $values);
}
// Cannot guess
if (!$docType || 'mixed' === $docType) {
return null;
}
if (str_ends_with($docType, '[]') && $type instanceof Array_) {
$collectionKeyTypes = new LegacyType(LegacyType::BUILTIN_TYPE_INT);
$collectionValueTypes = $this->getTypes($type->getValueType());
return new LegacyType(LegacyType::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 LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes);
}
if ($type instanceof PseudoType) {
if ($type->underlyingType() instanceof Integer) {
return new LegacyType(LegacyType::BUILTIN_TYPE_INT, $nullable, null);
} elseif ($type->underlyingType() instanceof String_) {
return new LegacyType(LegacyType::BUILTIN_TYPE_STRING, $nullable, null);
}
}
$docType = $this->normalizeType($docType);
[$phpType, $class] = $this->getPhpTypeAndClass($docType);
if ('array' === $docType) {
return new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true, null, null);
}
return new LegacyType($phpType, $nullable, $class);
}
/**
* Creates a {@see Type} from a PHPDoc type.
*/
private function createType(DocType $docType): ?Type
{
$docTypeString = (string) $docType;
if ('mixed[]' === $docTypeString) {
$docTypeString = 'array';
}
if ($docType instanceof Generic) {
$fqsen = $docType->getFqsen();
[$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, false) && !interface_exists($class, false)) {
return null;
}
$genericTypes = $docType->getTypes();
$type = null !== $class ? Type::object($class) : Type::builtin($phpType);
if ($collection) {
if (null === $valueType = $genericTypes[1] ?? null) {
$keyType = null;
$valueType = $genericTypes[0] ?? null;
} else {
$keyType = $genericTypes[0] ?? null;
}
$value = $valueType ? $this->getType($valueType) : null;
$key = $keyType ? $this->getType($keyType) : null;
return Type::collection($type, $value, $key);
}
$variableTypes = array_map(fn ($t) => $this->getType($t), $genericTypes);
return Type::generic($type, ...array_filter($variableTypes));
}
if ($docType instanceof Collection) {
$fqsen = $docType->getFqsen();
if ($fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) {
// Workaround for phpdocumentor/type-resolver < 1.6
return Type::list($this->getType($docType->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, false) && !interface_exists($class, false)) {
return null;
}
$type = null !== $class ? Type::object($class) : Type::builtin($phpType);
if ($collection) {
$value = $this->getType($docType->getValueType());
$key = $this->getType($docType->getKeyType());
return Type::collection($type, $value, $key);
}
$variableTypes = [];
if (!$this->hasNoExplicitKeyType($docType) && null !== $keyType = $this->getType($docType->getKeyType())) {
$variableTypes[] = $keyType;
}
if (null !== $valueType = $this->getType($docType->getValueType())) {
$variableTypes[] = $valueType;
}
return Type::generic($type, ...$variableTypes);
}
if (!$docTypeString) {
return null;
}
if ($docType instanceof Array_ && $this->hasNoExplicitKeyType($docType) && str_starts_with($docTypeString, 'array<')) {
return Type::list($this->getType($docType->getValueType()));
}
if (str_ends_with($docTypeString, '[]') && $docType instanceof Array_) {
return Type::list($this->getType($docType->getValueType()));
}
if (str_starts_with($docTypeString, 'list<') && $docType instanceof Array_) {
$collectionValueType = $this->getType($docType->getValueType());
return Type::list($collectionValueType);
}
if (str_starts_with($docTypeString, 'array<') && $docType instanceof Array_) {
// array<value> is converted to x[] which is handled above
// so it's only necessary to handle array<key, value> here
$collectionKeyType = $this->getType($docType->getKeyType());
$collectionValueType = $this->getType($docType->getValueType());
return Type::array($collectionValueType, $collectionKeyType);
}
$docTypeString = match ($docTypeString) {
'integer' => 'int',
'boolean' => 'bool',
// real is not part of the PHPDoc standard, so we ignore it
'double' => 'float',
'callback' => 'callable',
'void' => 'null',
default => $docTypeString,
};
[$phpType, $class] = $this->getPhpTypeAndClass($docTypeString);
if ('array' === $docTypeString) {
return Type::array();
}
if (null === $class) {
return Type::builtin($phpType);
}
if ($docType instanceof LegacyScalar || $docType instanceof Scalar) {
return Type::object('scalar');
}
if ($docType instanceof PseudoType) {
if ($docType->underlyingType() instanceof Integer) {
return Type::int();
}
if ($docType->underlyingType() instanceof String_) {
return Type::string();
}
// It's safer to fall back to other extractors here, as resolving pseudo types correctly is not easy at the moment
return null;
}
return Type::object($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 hasNoExplicitKeyType(Array_|Collection $type): bool
{
if (method_exists($type, 'getOriginalKeyType')) {
return null === $type->getOriginalKeyType();
}
// Workaround for phpdocumentor/reflection-docblock < 6
// "getOriginalKeyType()" doesn't exist, so we check if key type is Compound(string, int) which is the default.
return $type->getKeyType() instanceof Compound;
}
private function getPhpTypeAndClass(string $docType): array
{
if (\in_array($docType, TypeIdentifier::values(), true)) {
return [$docType, null];
}
if (\in_array($docType, ['parent', 'self', 'static'], true)) {
return ['object', $docType];
}
return ['object', ltrim($docType, '\\')];
}
}

View File

@@ -0,0 +1,214 @@
<?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\Type;
use Symfony\Component\TypeInfo\TypeContext\TypeContext;
/**
* 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, TypeContext $typeContext): array
{
if ($node instanceof ParamTagValueNode || $node instanceof ReturnTagValueNode || $node instanceof VarTagValueNode) {
return $this->compressNullableType($this->extractTypes($node->type, $typeContext));
}
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, TypeContext $typeContext): 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, $typeContext) 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, $typeContext);
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], $typeContext) as $subType) {
$collectionKeyValues[] = $subType;
}
} elseif (2 === \count($node->genericTypes)) {
foreach ($this->extractTypes($node->genericTypes[0], $typeContext) as $keySubType) {
$collectionKeyTypes[] = $keySubType;
}
foreach ($this->extractTypes($node->genericTypes[1], $typeContext) 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, $typeContext))];
}
if ($node instanceof CallableTypeNode || $node instanceof CallableTypeParameterNode) {
return [new Type(Type::BUILTIN_TYPE_CALLABLE)];
}
if ($node instanceof NullableTypeNode) {
$subTypes = $this->extractTypes($node->type, $typeContext);
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, $typeContext->getCalledClass())];
}
if ($node instanceof IdentifierTypeNode) {
if (\in_array($node->name, Type::$builtinTypes, true)) {
return [new Type($node->name, false, null, \in_array($node->name, Type::$builtinCollectionTypes, true))];
}
return match ($node->name) {
'integer',
'positive-int',
'negative-int',
'non-positive-int',
'non-negative-int',
'non-zero-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, $typeContext->getCalledClass())],
'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, $typeContext->normalize($node->name))],
};
}
return [];
}
}

View File

@@ -0,0 +1,52 @@
{
"name": "symfony/property-info",
"type": "library",
"description": "Extracts information about PHP class' properties using metadata of popular sources",
"keywords": [
"property",
"type",
"phpdoc",
"symfony",
"validator",
"doctrine"
],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/string": "^6.4|^7.0|^8.0",
"symfony/type-info": "^7.4.7|^8.0.7"
},
"require-dev": {
"symfony/serializer": "^6.4|^7.0|^8.0",
"symfony/cache": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
"phpstan/phpdoc-parser": "^1.0|^2.0"
},
"conflict": {
"phpdocumentor/reflection-docblock": "<5.2|>=7",
"phpdocumentor/type-resolver": "<1.5.1",
"symfony/dependency-injection": "<6.4",
"symfony/cache": "<6.4",
"symfony/serializer": "<6.4"
},
"autoload": {
"psr-4": { "Symfony\\Component\\PropertyInfo\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}