v1.0 Initial commit of project

This commit is contained in:
2026-01-01 10:54:18 +01:00
commit 768cf78b57
990 changed files with 241213 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.git* export-ignore

View File

@@ -0,0 +1,8 @@
Please do not submit any Pull Requests here. They will be closed.
---
Please submit your PR here instead:
https://github.com/symfony/symfony
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!

View File

@@ -0,0 +1,20 @@
name: Close Pull Request
on:
pull_request_target:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thanks for your Pull Request! We love contributions.
However, you should instead open your PR on the main repository:
https://github.com/symfony/symfony
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!

View File

@@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@@ -0,0 +1,57 @@
CHANGELOG
=========
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,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;
}

14
vendor/symfony/property-info/README.md vendored Normal file
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,81 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor;
use Symfony\Component\PropertyInfo\Tests\Fixtures\NullExtractor;
use Symfony\Component\PropertyInfo\Type;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AbstractPropertyInfoExtractorTest extends TestCase
{
protected PropertyInfoExtractorInterface $propertyInfo;
protected function setUp(): void
{
$extractors = [new NullExtractor(), new DummyExtractor()];
$this->propertyInfo = new PropertyInfoExtractor($extractors, $extractors, $extractors, $extractors, $extractors);
}
public function testInstanceOf()
{
$this->assertInstanceOf(PropertyInfoExtractorInterface::class, $this->propertyInfo);
$this->assertInstanceOf(PropertyTypeExtractorInterface::class, $this->propertyInfo);
$this->assertInstanceOf(PropertyDescriptionExtractorInterface::class, $this->propertyInfo);
$this->assertInstanceOf(PropertyAccessExtractorInterface::class, $this->propertyInfo);
$this->assertInstanceOf(PropertyInitializableExtractorInterface::class, $this->propertyInfo);
}
public function testGetShortDescription()
{
$this->assertSame('short', $this->propertyInfo->getShortDescription('Foo', 'bar', []));
}
public function testGetLongDescription()
{
$this->assertSame('long', $this->propertyInfo->getLongDescription('Foo', 'bar', []));
}
public function testGetTypes()
{
$this->assertEquals([new Type(Type::BUILTIN_TYPE_INT)], $this->propertyInfo->getTypes('Foo', 'bar', []));
}
public function testIsReadable()
{
$this->assertTrue($this->propertyInfo->isReadable('Foo', 'bar', []));
}
public function testIsWritable()
{
$this->assertTrue($this->propertyInfo->isWritable('Foo', 'bar', []));
}
public function testGetProperties()
{
$this->assertEquals(['a', 'b'], $this->propertyInfo->getProperties('Foo'));
}
public function testIsInitializable()
{
$this->assertTrue($this->propertyInfo->isInitializable('Foo', 'bar', []));
}
}

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\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoConstructorPass;
class PropertyInfoConstructorPassTest extends TestCase
{
public function testServicesAreOrderedAccordingToPriority()
{
$container = new ContainerBuilder();
$tag = 'property_info.constructor_extractor';
$definition = $container->register('property_info.constructor_extractor')->setArguments([null, null]);
$container->register('n2')->addTag($tag, ['priority' => 100]);
$container->register('n1')->addTag($tag, ['priority' => 200]);
$container->register('n3')->addTag($tag);
$pass = new PropertyInfoConstructorPass();
$pass->process($container);
$expected = new IteratorArgument([
new Reference('n1'),
new Reference('n2'),
new Reference('n3'),
]);
$this->assertEquals($expected, $definition->getArgument(0));
}
public function testReturningEmptyArrayWhenNoService()
{
$container = new ContainerBuilder();
$propertyInfoExtractorDefinition = $container->register('property_info.constructor_extractor')
->setArguments([[]]);
$pass = new PropertyInfoConstructorPass();
$pass->process($container);
$this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(0));
}
}

View File

@@ -0,0 +1,71 @@
<?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\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass;
class PropertyInfoPassTest extends TestCase
{
/**
* @dataProvider provideTags
*/
public function testServicesAreOrderedAccordingToPriority($index, $tag)
{
$container = new ContainerBuilder();
$definition = $container->register('property_info')->setArguments([null, null, null, null, null]);
$container->register('n2')->addTag($tag, ['priority' => 100]);
$container->register('n1')->addTag($tag, ['priority' => 200]);
$container->register('n3')->addTag($tag);
$propertyInfoPass = new PropertyInfoPass();
$propertyInfoPass->process($container);
$expected = new IteratorArgument([
new Reference('n1'),
new Reference('n2'),
new Reference('n3'),
]);
$this->assertEquals($expected, $definition->getArgument($index));
}
public static function provideTags()
{
return [
[0, 'property_info.list_extractor'],
[1, 'property_info.type_extractor'],
[2, 'property_info.description_extractor'],
[3, 'property_info.access_extractor'],
[4, 'property_info.initializable_extractor'],
];
}
public function testReturningEmptyArrayWhenNoService()
{
$container = new ContainerBuilder();
$propertyInfoExtractorDefinition = $container->register('property_info')
->setArguments([[], [], [], [], []]);
$propertyInfoPass = new PropertyInfoPass();
$propertyInfoPass->process($container);
$this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(0));
$this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(1));
$this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(2));
$this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(3));
$this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(4));
}
}

View File

@@ -0,0 +1,46 @@
<?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\Tests\Extractor;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor;
use Symfony\Component\PropertyInfo\Type;
/**
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
*/
class ConstructorExtractorTest extends TestCase
{
private ConstructorExtractor $extractor;
protected function setUp(): void
{
$this->extractor = new ConstructorExtractor([new DummyExtractor()]);
}
public function testInstanceOf()
{
$this->assertInstanceOf(\Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface::class, $this->extractor);
}
public function testGetTypes()
{
$this->assertEquals([new Type(Type::BUILTIN_TYPE_STRING)], $this->extractor->getTypes('Foo', 'bar', []));
}
public function testGetTypesIfNoExtractors()
{
$extractor = new ConstructorExtractor([]);
$this->assertNull($extractor->getTypes('Foo', 'bar', []));
}
}

View File

@@ -0,0 +1,483 @@
<?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\Tests\Extractor;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyCollection;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\PseudoTypeDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait;
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsingTrait;
use Symfony\Component\PropertyInfo\Type;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PhpDocExtractorTest extends TestCase
{
private PhpDocExtractor $extractor;
protected function setUp(): void
{
$this->extractor = new PhpDocExtractor();
}
/**
* @dataProvider typesProvider
*/
public function testExtract($property, ?array $type, $shortDescription, $longDescription)
{
$this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property));
$this->assertSame($shortDescription, $this->extractor->getShortDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property));
$this->assertSame($longDescription, $this->extractor->getLongDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property));
}
public function testParamTagTypeIsOmitted()
{
$this->assertNull($this->extractor->getTypes(OmittedParamTagTypeDocBlock::class, 'omittedType'));
}
public static function invalidTypesProvider()
{
return [
'pub' => ['pub', null, null],
'stat' => ['stat', null, null],
'bar' => ['bar', 'Bar.', null],
];
}
/**
* @dataProvider invalidTypesProvider
*/
public function testInvalid($property, $shortDescription, $longDescription)
{
$this->assertNull($this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property));
$this->assertSame($shortDescription, $this->extractor->getShortDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property));
$this->assertSame($longDescription, $this->extractor->getLongDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property));
}
/**
* @group legacy
*/
public function testEmptyParamAnnotation()
{
$this->assertNull($this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', 'foo'));
$this->assertSame('Foo.', $this->extractor->getShortDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', 'foo'));
$this->assertNull($this->extractor->getLongDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', 'foo'));
}
/**
* @dataProvider typesWithNoPrefixesProvider
*/
public function testExtractTypesWithNoPrefixes($property, ?array $type = null)
{
$noPrefixExtractor = new PhpDocExtractor(null, [], [], []);
$this->assertEquals($type, $noPrefixExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property));
}
public static function typesProvider()
{
return [
['foo', null, 'Short description.', 'Long description.'],
['bar', [new Type(Type::BUILTIN_TYPE_STRING)], 'This is bar', null],
['baz', [new Type(Type::BUILTIN_TYPE_INT)], 'Should be used.', null],
['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)], null, null],
['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)], null, null],
['foo4', [new Type(Type::BUILTIN_TYPE_NULL)], null, null],
['foo5', null, null, null],
[
'files',
[
new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')),
new Type(Type::BUILTIN_TYPE_RESOURCE),
],
null,
null,
],
['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null],
['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null],
['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null],
['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))], null, null],
['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, null)], null, null],
['a', [new Type(Type::BUILTIN_TYPE_INT)], 'A.', null],
['b', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], 'B.', null],
['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)], null, null],
['ct', [new Type(Type::BUILTIN_TYPE_TRUE, true)], null, null],
['cf', [new Type(Type::BUILTIN_TYPE_FALSE, true)], null, null],
['d', [new Type(Type::BUILTIN_TYPE_BOOL)], null, null],
['dt', [new Type(Type::BUILTIN_TYPE_TRUE)], null, null],
['df', [new Type(Type::BUILTIN_TYPE_FALSE)], null, null],
['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))], null, null],
['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null],
['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null],
['h', [new Type(Type::BUILTIN_TYPE_STRING, true)], null, null],
['i', [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT, true)], null, null],
['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null],
['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))], null, null],
['donotexist', null, null, null],
['staticGetter', null, null, null],
['staticSetter', null, null, null],
['emptyVar', null, 'This should not be removed.', null],
['arrayWithKeys', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_STRING))], null, null],
['arrayOfMixed', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), null)], null, null],
['listOfStrings', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], null, null],
['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], null, null],
['collectionAsObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyCollection::class, true, [new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_STRING)])], null, null],
];
}
/**
* @dataProvider provideCollectionTypes
*/
public function testExtractCollection($property, ?array $type, $shortDescription, $longDescription)
{
$this->testExtract($property, $type, $shortDescription, $longDescription);
}
public static function provideCollectionTypes()
{
return [
['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)], new Type(Type::BUILTIN_TYPE_STRING))], null, null],
['iteratorCollectionWithKey', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], null, null],
[
'nestedIterators',
[new Type(
Type::BUILTIN_TYPE_OBJECT,
false,
'Iterator',
true,
new Type(Type::BUILTIN_TYPE_INT),
new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))
)],
null,
null,
],
[
'arrayWithKeys',
[new Type(
Type::BUILTIN_TYPE_ARRAY,
false,
null,
true,
new Type(Type::BUILTIN_TYPE_STRING),
new Type(Type::BUILTIN_TYPE_STRING)
)],
null,
null,
],
[
'arrayWithKeysAndComplexValue',
[new Type(
Type::BUILTIN_TYPE_ARRAY,
false,
null,
true,
new Type(Type::BUILTIN_TYPE_STRING),
new Type(
Type::BUILTIN_TYPE_ARRAY,
true,
null,
true,
new Type(Type::BUILTIN_TYPE_INT),
new Type(Type::BUILTIN_TYPE_STRING, true)
)
)],
null,
null,
],
];
}
/**
* @dataProvider typesWithCustomPrefixesProvider
*/
public function testExtractTypesWithCustomPrefixes($property, ?array $type = null)
{
$customExtractor = new PhpDocExtractor(null, ['add', 'remove'], ['is', 'can']);
$this->assertEquals($type, $customExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property));
}
public static function typesWithCustomPrefixesProvider()
{
return [
['foo', null, 'Short description.', 'Long description.'],
['bar', [new Type(Type::BUILTIN_TYPE_STRING)], 'This is bar', null],
['baz', [new Type(Type::BUILTIN_TYPE_INT)], 'Should be used.', null],
['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)], null, null],
['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)], null, null],
['foo4', [new Type(Type::BUILTIN_TYPE_NULL)], null, null],
['foo5', null, null, null],
[
'files',
[
new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')),
new Type(Type::BUILTIN_TYPE_RESOURCE),
],
null,
null,
],
['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null],
['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null],
['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null],
['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))], null, null],
['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, null)], null, null],
['a', null, 'A.', null],
['b', null, 'B.', null],
['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)], null, null],
['d', [new Type(Type::BUILTIN_TYPE_BOOL)], null, null],
['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))], null, null],
['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null],
['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null],
['h', [new Type(Type::BUILTIN_TYPE_STRING, true)], null, null],
['i', [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT, true)], null, null],
['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null],
['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))], null, null],
['nonNullableCollectionOfNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, true))], null, null],
['nullableCollectionOfMultipleNonNullableElementTypes', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)])], null, null],
['donotexist', null, null, null],
['staticGetter', null, null, null],
['staticSetter', null, null, null],
];
}
public static function typesWithNoPrefixesProvider()
{
return [
['foo', null, 'Short description.', 'Long description.'],
['bar', [new Type(Type::BUILTIN_TYPE_STRING)], 'This is bar', null],
['baz', [new Type(Type::BUILTIN_TYPE_INT)], 'Should be used.', null],
['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)], null, null],
['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)], null, null],
['foo4', [new Type(Type::BUILTIN_TYPE_NULL)], null, null],
['foo5', null, null, null],
[
'files',
[
new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')),
new Type(Type::BUILTIN_TYPE_RESOURCE),
],
null,
null,
],
['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null],
['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null],
['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null],
['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))], null, null],
['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, null)], null, null],
['a', null, 'A.', null],
['b', null, 'B.', null],
['c', null, null, null],
['d', null, null, null],
['e', null, null, null],
['f', null, null, null],
['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null],
['h', [new Type(Type::BUILTIN_TYPE_STRING, true)], null, null],
['i', [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT, true)], null, null],
['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null],
['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))], null, null],
['donotexist', null, null, null],
['staticGetter', null, null, null],
['staticSetter', null, null, null],
];
}
public function testReturnNullOnEmptyDocBlock()
{
$this->assertNull($this->extractor->getShortDescription(EmptyDocBlock::class, 'foo'));
}
public static function dockBlockFallbackTypesProvider()
{
return [
'pub' => [
'pub', [new Type(Type::BUILTIN_TYPE_STRING)],
],
'protAcc' => [
'protAcc', [new Type(Type::BUILTIN_TYPE_INT)],
],
'protMut' => [
'protMut', [new Type(Type::BUILTIN_TYPE_BOOL)],
],
];
}
/**
* @dataProvider dockBlockFallbackTypesProvider
*/
public function testDocBlockFallback($property, $types)
{
$this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback', $property));
}
/**
* @dataProvider propertiesDefinedByTraitsProvider
*/
public function testPropertiesDefinedByTraits(string $property, Type $type)
{
$this->assertEquals([$type], $this->extractor->getTypes(DummyUsingTrait::class, $property));
}
public static function propertiesDefinedByTraitsProvider(): array
{
return [
['propertyInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)],
['propertyInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)],
['propertyInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
['propertyInExternalTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)],
['propertyInExternalTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
['propertyInExternalTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)],
];
}
/**
* @dataProvider methodsDefinedByTraitsProvider
*/
public function testMethodsDefinedByTraits(string $property, Type $type)
{
$this->assertEquals([$type], $this->extractor->getTypes(DummyUsingTrait::class, $property));
}
public static function methodsDefinedByTraitsProvider(): array
{
return [
['methodInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)],
['methodInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)],
['methodInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
['methodInExternalTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)],
['methodInExternalTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
['methodInExternalTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)],
];
}
/**
* @dataProvider propertiesStaticTypeProvider
*/
public function testPropertiesStaticType(string $class, string $property, Type $type)
{
$this->assertEquals([$type], $this->extractor->getTypes($class, $property));
}
public static function propertiesStaticTypeProvider(): array
{
return [
[ParentDummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)],
[Dummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
];
}
/**
* @dataProvider propertiesParentTypeProvider
*/
public function testPropertiesParentType(string $class, string $property, ?array $types)
{
$this->assertEquals($types, $this->extractor->getTypes($class, $property));
}
public static function propertiesParentTypeProvider(): array
{
return [
[ParentDummy::class, 'parentAnnotationNoParent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'parent')]],
[Dummy::class, 'parentAnnotation', [new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]],
];
}
public function testUnknownPseudoType()
{
$this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'scalar')], $this->extractor->getTypes(PseudoTypeDummy::class, 'unknownPseudoType'));
}
public function testGenericInterface()
{
$this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface'));
}
/**
* @dataProvider constructorTypesProvider
*/
public function testExtractConstructorTypes($property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property));
}
public static function constructorTypesProvider()
{
return [
['date', [new Type(Type::BUILTIN_TYPE_INT)]],
['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]],
['dateObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]],
['dateTime', null],
['ddd', null],
['mixed', null],
];
}
/**
* @dataProvider pseudoTypesProvider
*/
public function testPseudoTypes($property, array $type)
{
$this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\PseudoTypesDummy', $property));
}
public static function pseudoTypesProvider(): array
{
return [
['classString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['classStringGeneric', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['htmlEscapedString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['lowercaseString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['nonEmptyLowercaseString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['nonEmptyString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['numericString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['traitString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['positiveInt', [new Type(Type::BUILTIN_TYPE_INT, false, null)]],
];
}
/**
* @dataProvider promotedPropertyProvider
*/
public function testExtractPromotedProperty(string $property, ?array $types)
{
$this->assertEquals($types, $this->extractor->getTypes(Php80Dummy::class, $property));
}
public static function promotedPropertyProvider(): array
{
return [
['promoted', null],
['promotedAndMutated', [new Type(Type::BUILTIN_TYPE_STRING)]],
];
}
}
class EmptyDocBlock
{
public $foo;
}
class OmittedParamTagTypeDocBlock
{
/**
* The type is omitted here to ensure that the extractor doesn't choke on missing types.
*
* @param $omittedTagType
*/
public function setOmittedType(array $omittedTagType)
{
}
}

View File

@@ -0,0 +1,583 @@
<?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\Tests\Extractor;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Clazz;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyCollection;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyGeneric;
use Symfony\Component\PropertyInfo\Tests\Fixtures\IFace;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80PromotedDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\RootDummy\RootDummyItem;
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\AnotherNamespace\DummyInAnotherNamespace;
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait;
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsingTrait;
use Symfony\Component\PropertyInfo\Type;
require_once __DIR__.'/../Fixtures/Extractor/DummyNamespace.php';
/**
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*/
class PhpStanExtractorTest extends TestCase
{
private PhpStanExtractor $extractor;
private PhpDocExtractor $phpDocExtractor;
protected function setUp(): void
{
$this->extractor = new PhpStanExtractor();
$this->phpDocExtractor = new PhpDocExtractor();
}
/**
* @dataProvider typesProvider
*/
public function testExtract($property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property));
}
public function testParamTagTypeIsOmitted()
{
$this->assertNull($this->extractor->getTypes(PhpStanOmittedParamTagTypeDocBlock::class, 'omittedType'));
}
public static function invalidTypesProvider()
{
return [
'pub' => ['pub'],
'stat' => ['stat'],
'foo' => ['foo'],
'bar' => ['bar'],
];
}
/**
* @dataProvider invalidTypesProvider
*/
public function testInvalid($property)
{
$this->assertNull($this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property));
}
/**
* @dataProvider typesWithNoPrefixesProvider
*/
public function testExtractTypesWithNoPrefixes($property, ?array $type = null)
{
$noPrefixExtractor = new PhpStanExtractor([], [], []);
$this->assertEquals($type, $noPrefixExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property));
}
public static function typesProvider()
{
return [
['foo', null],
['bar', [new Type(Type::BUILTIN_TYPE_STRING)]],
['baz', [new Type(Type::BUILTIN_TYPE_INT)]],
['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)]],
['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)]],
['foo4', [new Type(Type::BUILTIN_TYPE_NULL)]],
['foo5', null],
[
'files',
[
new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')),
new Type(Type::BUILTIN_TYPE_RESOURCE),
],
],
['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]],
['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]],
['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]],
['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))]],
['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], null)]],
['a', [new Type(Type::BUILTIN_TYPE_INT)]],
['b', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]],
['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)]],
['d', [new Type(Type::BUILTIN_TYPE_BOOL)]],
['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))]],
['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]],
['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]],
['h', [new Type(Type::BUILTIN_TYPE_STRING, true)]],
['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')]],
['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))]],
['donotexist', null],
['staticGetter', null],
['staticSetter', null],
['emptyVar', null],
['arrayWithKeys', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_STRING))]],
['arrayOfMixed', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), null)]],
['listOfStrings', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]],
['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]],
['rootDummyItems', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, RootDummyItem::class))]],
['rootDummyItem', [new Type(Type::BUILTIN_TYPE_OBJECT, false, RootDummyItem::class)]],
['collectionAsObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyCollection::class, true, [new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_STRING)])]],
];
}
/**
* @dataProvider provideCollectionTypes
*/
public function testExtractCollection($property, ?array $type = null)
{
$this->testExtract($property, $type);
}
public static function provideCollectionTypes()
{
return [
['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, null, new Type(Type::BUILTIN_TYPE_STRING))]],
['iteratorCollectionWithKey', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]],
[
'nestedIterators',
[new Type(
Type::BUILTIN_TYPE_OBJECT,
false,
'Iterator',
true,
new Type(Type::BUILTIN_TYPE_INT),
new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))
)],
],
[
'arrayWithKeys',
[new Type(
Type::BUILTIN_TYPE_ARRAY,
false,
null,
true,
new Type(Type::BUILTIN_TYPE_STRING),
new Type(Type::BUILTIN_TYPE_STRING)
)],
],
[
'arrayWithKeysAndComplexValue',
[new Type(
Type::BUILTIN_TYPE_ARRAY,
false,
null,
true,
new Type(Type::BUILTIN_TYPE_STRING),
new Type(
Type::BUILTIN_TYPE_ARRAY,
true,
null,
true,
new Type(Type::BUILTIN_TYPE_INT),
new Type(Type::BUILTIN_TYPE_STRING, true)
)
)],
],
];
}
/**
* @dataProvider typesWithCustomPrefixesProvider
*/
public function testExtractTypesWithCustomPrefixes($property, ?array $type = null)
{
$customExtractor = new PhpStanExtractor(['add', 'remove'], ['is', 'can']);
$this->assertEquals($type, $customExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property));
}
public static function typesWithCustomPrefixesProvider()
{
return [
['foo', null],
['bar', [new Type(Type::BUILTIN_TYPE_STRING)]],
['baz', [new Type(Type::BUILTIN_TYPE_INT)]],
['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)]],
['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)]],
['foo4', [new Type(Type::BUILTIN_TYPE_NULL)]],
['foo5', null],
[
'files',
[
new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')),
new Type(Type::BUILTIN_TYPE_RESOURCE),
],
],
['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]],
['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]],
['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]],
['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))]],
['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], null)]],
['a', null],
['b', null],
['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)]],
['d', [new Type(Type::BUILTIN_TYPE_BOOL)]],
['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))]],
['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]],
['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]],
['h', [new Type(Type::BUILTIN_TYPE_STRING, true)]],
['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')]],
['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))]],
['donotexist', null],
['staticGetter', null],
['staticSetter', null],
];
}
public static function typesWithNoPrefixesProvider()
{
return [
['foo', null],
['bar', [new Type(Type::BUILTIN_TYPE_STRING)]],
['baz', [new Type(Type::BUILTIN_TYPE_INT)]],
['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)]],
['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)]],
['foo4', [new Type(Type::BUILTIN_TYPE_NULL)]],
['foo5', null],
[
'files',
[
new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')),
new Type(Type::BUILTIN_TYPE_RESOURCE),
],
],
['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]],
['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]],
['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]],
['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))]],
['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], null)]],
['a', null],
['b', null],
['c', null],
['d', null],
['e', null],
['f', null],
['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]],
['h', [new Type(Type::BUILTIN_TYPE_STRING, true)]],
['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')]],
['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))]],
['donotexist', null],
['staticGetter', null],
['staticSetter', null],
];
}
public static function dockBlockFallbackTypesProvider()
{
return [
'pub' => [
'pub', [new Type(Type::BUILTIN_TYPE_STRING)],
],
'protAcc' => [
'protAcc', [new Type(Type::BUILTIN_TYPE_INT)],
],
'protMut' => [
'protMut', [new Type(Type::BUILTIN_TYPE_BOOL)],
],
];
}
/**
* @dataProvider dockBlockFallbackTypesProvider
*/
public function testDocBlockFallback($property, $types)
{
$this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback', $property));
}
/**
* @dataProvider propertiesDefinedByTraitsProvider
*/
public function testPropertiesDefinedByTraits(string $property, Type $type)
{
$this->assertEquals([$type], $this->extractor->getTypes(DummyUsingTrait::class, $property));
}
public static function propertiesDefinedByTraitsProvider(): array
{
return [
['propertyInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)],
['propertyInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)],
['propertyInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
['dummyInAnotherNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyInAnotherNamespace::class)],
];
}
/**
* @dataProvider propertiesStaticTypeProvider
*/
public function testPropertiesStaticType(string $class, string $property, Type $type)
{
$this->assertEquals([$type], $this->extractor->getTypes($class, $property));
}
public static function propertiesStaticTypeProvider(): array
{
return [
[ParentDummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)],
[Dummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
];
}
/**
* @dataProvider propertiesParentTypeProvider
*/
public function testPropertiesParentType(string $class, string $property, ?array $types)
{
$this->assertEquals($types, $this->extractor->getTypes($class, $property));
}
public static function propertiesParentTypeProvider(): array
{
return [
[ParentDummy::class, 'parentAnnotationNoParent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'parent')]],
[Dummy::class, 'parentAnnotation', [new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]],
];
}
/**
* @dataProvider constructorTypesProvider
*/
public function testExtractConstructorTypes($property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property));
}
/**
* @dataProvider constructorTypesProvider
*/
public function testExtractConstructorTypesReturnNullOnEmptyDocBlock($property)
{
$this->assertNull($this->extractor->getTypesFromConstructor(ConstructorDummyWithoutDocBlock::class, $property));
}
public static function constructorTypesProvider()
{
return [
['date', [new Type(Type::BUILTIN_TYPE_INT)]],
['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]],
['dateObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]],
['dateTime', null],
['ddd', null],
];
}
/**
* @dataProvider unionTypesProvider
*/
public function testExtractorUnionTypes(string $property, ?array $types)
{
$this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DummyUnionType', $property));
}
public static function unionTypesProvider(): array
{
return [
['a', [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)]],
['b', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)])]],
['c', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)])]],
['d', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING)])])]],
['e', [new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class, false, [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING)])], [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_OBJECT, false, \Traversable::class, true, [], [new Type(Type::BUILTIN_TYPE_OBJECT, false, DefaultValue::class)])])]), new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]],
['f', null],
['g', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)])]],
];
}
/**
* @dataProvider pseudoTypesProvider
*/
public function testPseudoTypes($property, array $type)
{
$this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\PhpStanPseudoTypesDummy', $property));
}
public static function pseudoTypesProvider(): array
{
return [
['classString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['classStringGeneric', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['htmlEscapedString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['lowercaseString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['nonEmptyLowercaseString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['nonEmptyString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['numericString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['traitString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['interfaceString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['literalString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]],
['positiveInt', [new Type(Type::BUILTIN_TYPE_INT, false, null)]],
['negativeInt', [new Type(Type::BUILTIN_TYPE_INT, false, null)]],
['nonEmptyArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]],
['nonEmptyList', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT))]],
['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)]],
['arrayKey', [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)]],
['double', [new Type(Type::BUILTIN_TYPE_FLOAT)]],
];
}
public function testDummyNamespace()
{
$this->assertEquals(
[new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy')],
$this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DummyNamespace', 'dummy')
);
}
public function testDummyNamespaceWithProperty()
{
$phpStanTypes = $this->extractor->getTypes(\B\Dummy::class, 'property');
$phpDocTypes = $this->phpDocExtractor->getTypes(\B\Dummy::class, 'property');
$this->assertEquals('A\Property', $phpStanTypes[0]->getClassName());
$this->assertEquals($phpDocTypes[0]->getClassName(), $phpStanTypes[0]->getClassName());
}
/**
* @dataProvider intRangeTypeProvider
*/
public function testExtractorIntRangeType(string $property, ?array $types)
{
$this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\IntRangeDummy', $property));
}
public static function intRangeTypeProvider(): array
{
return [
['a', [new Type(Type::BUILTIN_TYPE_INT)]],
['b', [new Type(Type::BUILTIN_TYPE_INT, true)]],
['c', [new Type(Type::BUILTIN_TYPE_INT)]],
];
}
/**
* @dataProvider php80TypesProvider
*/
public function testExtractPhp80Type(string $class, $property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypes($class, $property, []));
}
public static function php80TypesProvider()
{
return [
[Php80Dummy::class, 'promotedWithDocCommentAndType', [new Type(Type::BUILTIN_TYPE_INT)]],
[Php80Dummy::class, 'promotedWithDocComment', [new Type(Type::BUILTIN_TYPE_STRING)]],
[Php80Dummy::class, 'promotedAndMutated', [new Type(Type::BUILTIN_TYPE_STRING)]],
[Php80Dummy::class, 'promoted', null],
[Php80Dummy::class, 'collection', [new Type(Type::BUILTIN_TYPE_ARRAY, collection: true, collectionValueType: new Type(Type::BUILTIN_TYPE_STRING))]],
[Php80PromotedDummy::class, 'promoted', null],
];
}
public function testGenericInterface()
{
$this->assertEquals(
[
new Type(
builtinType: Type::BUILTIN_TYPE_OBJECT,
class: \BackedEnum::class,
collectionValueType: new Type(
builtinType: Type::BUILTIN_TYPE_STRING,
)
),
],
$this->extractor->getTypes(Dummy::class, 'genericInterface')
);
}
/**
* @param list<Type> $expectedTypes
* @dataProvider genericsProvider
*/
public function testGenericsLegacy(string $property, array $expectedTypes)
{
$this->assertEquals($expectedTypes, $this->extractor->getTypes(DummyGeneric::class, $property));
}
/**
* @return iterable<array{0: string, 1: list<Type>}>
*/
public static function genericsProvider(): iterable
{
yield [
'basicClass',
[
new Type(
builtinType: Type::BUILTIN_TYPE_OBJECT,
class: Clazz::class,
collectionValueType: new Type(
builtinType: Type::BUILTIN_TYPE_OBJECT,
class: Dummy::class,
)
),
],
];
yield [
'nullableClass',
[
new Type(
builtinType: Type::BUILTIN_TYPE_OBJECT,
class: Clazz::class,
nullable: true,
collectionValueType: new Type(
builtinType: Type::BUILTIN_TYPE_OBJECT,
class: Dummy::class,
)
),
],
];
yield [
'basicInterface',
[
new Type(
builtinType: Type::BUILTIN_TYPE_OBJECT,
class: IFace::class,
collectionValueType: new Type(
builtinType: Type::BUILTIN_TYPE_OBJECT,
class: Dummy::class,
)
),
],
];
yield [
'nullableInterface',
[
new Type(
builtinType: Type::BUILTIN_TYPE_OBJECT,
class: IFace::class,
nullable: true,
collectionValueType: new Type(
builtinType: Type::BUILTIN_TYPE_OBJECT,
class: Dummy::class,
)
),
],
];
}
}
class PhpStanOmittedParamTagTypeDocBlock
{
/**
* The type is omitted here to ensure that the extractor doesn't choke on missing types.
*
* @param $omittedTagType
*/
public function setOmittedType(array $omittedTagType)
{
}
}

View File

@@ -0,0 +1,793 @@
<?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\Tests\Extractor;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyReadInfo;
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
use Symfony\Component\PropertyInfo\Tests\Fixtures\AdderRemoverDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\AsymmetricVisibility;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended2;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php74Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php7Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php7ParentDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\SnakeCaseDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\VirtualProperties;
use Symfony\Component\PropertyInfo\Type;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ReflectionExtractorTest extends TestCase
{
private ReflectionExtractor $extractor;
protected function setUp(): void
{
$this->extractor = new ReflectionExtractor();
}
public function testGetProperties()
{
$this->assertSame(
[
'bal',
'parent',
'collection',
'collectionAsObject',
'nestedCollection',
'mixedCollection',
'B',
'Guid',
'g',
'h',
'i',
'j',
'nullableCollectionOfNonNullableElements',
'nonNullableCollectionOfNullableElements',
'nullableCollectionOfMultipleNonNullableElementTypes',
'emptyVar',
'iteratorCollection',
'iteratorCollectionWithKey',
'nestedIterators',
'arrayWithKeys',
'arrayWithKeysAndComplexValue',
'arrayOfMixed',
'listOfStrings',
'parentAnnotation',
'genericInterface',
'foo',
'foo2',
'foo3',
'foo4',
'foo5',
'files',
'propertyTypeStatic',
'parentAnnotationNoParent',
'rootDummyItems',
'rootDummyItem',
'a',
'DOB',
'Id',
'123',
'self',
'realParent',
'xTotals',
'YT',
'date',
'element',
'c',
'ct',
'cf',
'd',
'dt',
'df',
'e',
'f',
],
$this->extractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy')
);
$this->assertNull($this->extractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\NoProperties'));
}
public function testGetPropertiesWithCustomPrefixes()
{
$customExtractor = new ReflectionExtractor(['add', 'remove'], ['is', 'can']);
$this->assertSame(
[
'bal',
'parent',
'collection',
'collectionAsObject',
'nestedCollection',
'mixedCollection',
'B',
'Guid',
'g',
'h',
'i',
'j',
'nullableCollectionOfNonNullableElements',
'nonNullableCollectionOfNullableElements',
'nullableCollectionOfMultipleNonNullableElementTypes',
'emptyVar',
'iteratorCollection',
'iteratorCollectionWithKey',
'nestedIterators',
'arrayWithKeys',
'arrayWithKeysAndComplexValue',
'arrayOfMixed',
'listOfStrings',
'parentAnnotation',
'genericInterface',
'foo',
'foo2',
'foo3',
'foo4',
'foo5',
'files',
'propertyTypeStatic',
'parentAnnotationNoParent',
'rootDummyItems',
'rootDummyItem',
'date',
'c',
'ct',
'cf',
'd',
'dt',
'df',
'e',
'f',
],
$customExtractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy')
);
}
public function testGetPropertiesWithNoPrefixes()
{
$noPrefixExtractor = new ReflectionExtractor([], [], []);
$this->assertSame(
[
'bal',
'parent',
'collection',
'collectionAsObject',
'nestedCollection',
'mixedCollection',
'B',
'Guid',
'g',
'h',
'i',
'j',
'nullableCollectionOfNonNullableElements',
'nonNullableCollectionOfNullableElements',
'nullableCollectionOfMultipleNonNullableElementTypes',
'emptyVar',
'iteratorCollection',
'iteratorCollectionWithKey',
'nestedIterators',
'arrayWithKeys',
'arrayWithKeysAndComplexValue',
'arrayOfMixed',
'listOfStrings',
'parentAnnotation',
'genericInterface',
'foo',
'foo2',
'foo3',
'foo4',
'foo5',
'files',
'propertyTypeStatic',
'parentAnnotationNoParent',
'rootDummyItems',
'rootDummyItem',
],
$noPrefixExtractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy')
);
}
/**
* @dataProvider typesProvider
*/
public function testExtractors($property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property, []));
}
public static function typesProvider()
{
return [
['a', null],
['b', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]],
['c', [new Type(Type::BUILTIN_TYPE_BOOL)]],
['d', [new Type(Type::BUILTIN_TYPE_BOOL)]],
['e', null],
['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]],
['donotexist', null],
['staticGetter', null],
['staticSetter', null],
['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy')]],
['realParent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]],
['date', [new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]],
['dates', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class))]],
];
}
/**
* @dataProvider php7TypesProvider
*/
public function testExtractPhp7Type(string $class, string $property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypes($class, $property, []));
}
public static function php7TypesProvider()
{
return [
[Php7Dummy::class, 'foo', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]],
[Php7Dummy::class, 'bar', [new Type(Type::BUILTIN_TYPE_INT)]],
[Php7Dummy::class, 'baz', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]],
[Php7Dummy::class, 'buz', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Php7Dummy')]],
[Php7Dummy::class, 'biz', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Php7ParentDummy::class)]],
[Php7Dummy::class, 'donotexist', null],
[Php7ParentDummy::class, 'parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, \stdClass::class)]],
];
}
/**
* @dataProvider php71TypesProvider
*/
public function testExtractPhp71Type($property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy', $property, []));
}
public static function php71TypesProvider()
{
return [
['foo', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]],
['buz', [new Type(Type::BUILTIN_TYPE_NULL)]],
['bar', [new Type(Type::BUILTIN_TYPE_INT, true)]],
['baz', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]],
['donotexist', null],
];
}
/**
* @dataProvider php80TypesProvider
*/
public function testExtractPhp80Type($property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy', $property, []));
}
public static function php80TypesProvider()
{
return [
['foo', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]],
['bar', [new Type(Type::BUILTIN_TYPE_INT, true)]],
['timeout', [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT)]],
['optional', [new Type(Type::BUILTIN_TYPE_INT, true), new Type(Type::BUILTIN_TYPE_FLOAT, true)]],
['string', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Stringable'), new Type(Type::BUILTIN_TYPE_STRING)]],
['payload', null],
['data', null],
['mixedProperty', null],
];
}
/**
* @dataProvider php81TypesProvider
*/
public function testExtractPhp81Type($property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy', $property, []));
}
public static function php81TypesProvider()
{
return [
['nothing', null],
['collection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Traversable'), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Countable')]],
];
}
public function testReadonlyPropertiesAreNotWriteable()
{
$this->assertFalse($this->extractor->isWritable(Php81Dummy::class, 'foo'));
}
/**
* @dataProvider php82TypesProvider
*
* @requires PHP 8.2
*/
public function testExtractPhp82Type($property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php82Dummy', $property, []));
}
public static function php82TypesProvider(): iterable
{
yield ['nil', null];
yield ['false', [new Type(Type::BUILTIN_TYPE_FALSE)]];
yield ['true', [new Type(Type::BUILTIN_TYPE_TRUE)]];
// Nesting intersection and union types is not supported yet,
// but we should make sure this kind of composite types does not crash the extractor.
yield ['someCollection', null];
}
/**
* @dataProvider defaultValueProvider
*/
public function testExtractWithDefaultValue($property, $type)
{
$this->assertEquals($type, $this->extractor->getTypes(DefaultValue::class, $property, []));
}
public static function defaultValueProvider()
{
return [
['defaultInt', [new Type(Type::BUILTIN_TYPE_INT, false)]],
['defaultFloat', [new Type(Type::BUILTIN_TYPE_FLOAT, false)]],
['defaultString', [new Type(Type::BUILTIN_TYPE_STRING, false)]],
['defaultArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]],
['defaultNull', null],
];
}
/**
* @dataProvider getReadableProperties
*/
public function testIsReadable($property, $expected)
{
$this->assertSame(
$expected,
$this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property, [])
);
}
public static function getReadableProperties()
{
return [
['bar', false],
['baz', false],
['parent', true],
['a', true],
['b', false],
['c', true],
['d', true],
['e', false],
['f', false],
['Id', true],
['id', true],
['Guid', true],
['guid', false],
['element', false],
];
}
/**
* @dataProvider getWritableProperties
*/
public function testIsWritable($property, $expected)
{
$this->assertSame(
$expected,
$this->extractor->isWritable(Dummy::class, $property, [])
);
}
public static function getWritableProperties()
{
return [
['bar', false],
['baz', false],
['parent', true],
['a', false],
['b', true],
['c', false],
['d', false],
['e', true],
['f', true],
['Id', false],
['Guid', true],
['guid', false],
];
}
public function testIsReadableSnakeCase()
{
$this->assertTrue($this->extractor->isReadable(SnakeCaseDummy::class, 'snake_property'));
$this->assertTrue($this->extractor->isReadable(SnakeCaseDummy::class, 'snake_readonly'));
}
public function testIsWriteableSnakeCase()
{
$this->assertTrue($this->extractor->isWritable(SnakeCaseDummy::class, 'snake_property'));
$this->assertFalse($this->extractor->isWritable(SnakeCaseDummy::class, 'snake_readonly'));
// Ensure that it's still possible to write to the property using the (old) snake name
$this->assertTrue($this->extractor->isWritable(SnakeCaseDummy::class, 'snake_method'));
}
public function testSingularize()
{
$this->assertTrue($this->extractor->isWritable(AdderRemoverDummy::class, 'analyses'));
$this->assertTrue($this->extractor->isWritable(AdderRemoverDummy::class, 'feet'));
$this->assertEquals(['analyses', 'feet'], $this->extractor->getProperties(AdderRemoverDummy::class));
}
public function testPrivatePropertyExtractor()
{
$privateExtractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PRIVATE | ReflectionExtractor::ALLOW_PROTECTED);
$properties = $privateExtractor->getProperties(Dummy::class);
$this->assertContains('bar', $properties);
$this->assertContains('baz', $properties);
$this->assertTrue($privateExtractor->isReadable(Dummy::class, 'bar'));
$this->assertTrue($privateExtractor->isReadable(Dummy::class, 'baz'));
$protectedExtractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED);
$properties = $protectedExtractor->getProperties(Dummy::class);
$this->assertNotContains('bar', $properties);
$this->assertContains('baz', $properties);
$this->assertFalse($protectedExtractor->isReadable(Dummy::class, 'bar'));
$this->assertTrue($protectedExtractor->isReadable(Dummy::class, 'baz'));
}
/**
* @dataProvider getInitializableProperties
*/
public function testIsInitializable(string $class, string $property, bool $expected)
{
$this->assertSame($expected, $this->extractor->isInitializable($class, $property));
}
public static function getInitializableProperties(): array
{
return [
[Php71Dummy::class, 'string', true],
[Php71Dummy::class, 'intPrivate', true],
[Php71Dummy::class, 'notExist', false],
[Php71DummyExtended2::class, 'intWithAccessor', true],
[Php71DummyExtended2::class, 'intPrivate', false],
[NotInstantiable::class, 'foo', false],
];
}
/**
* @dataProvider constructorTypesProvider
*/
public function testExtractTypeConstructor(string $class, string $property, ?array $type = null)
{
/* Check that constructor extractions works by default, and if passed in via context.
Check that null is returned if constructor extraction is disabled */
$this->assertEquals($type, $this->extractor->getTypes($class, $property, []));
$this->assertEquals($type, $this->extractor->getTypes($class, $property, ['enable_constructor_extraction' => true]));
$this->assertNull($this->extractor->getTypes($class, $property, ['enable_constructor_extraction' => false]));
}
public static function constructorTypesProvider(): array
{
return [
// php71 dummy has following constructor: __construct(string $string, int $intPrivate)
[Php71Dummy::class, 'string', [new Type(Type::BUILTIN_TYPE_STRING, false)]],
[Php71Dummy::class, 'intPrivate', [new Type(Type::BUILTIN_TYPE_INT, false)]],
// Php71DummyExtended2 adds int $intWithAccessor
[Php71DummyExtended2::class, 'intWithAccessor', [new Type(Type::BUILTIN_TYPE_INT, false)]],
[Php71DummyExtended2::class, 'intPrivate', [new Type(Type::BUILTIN_TYPE_INT, false)]],
[DefaultValue::class, 'foo', null],
];
}
public function testNullOnPrivateProtectedAccessor()
{
$barAcessor = $this->extractor->getReadInfo(Dummy::class, 'bar');
$barMutator = $this->extractor->getWriteInfo(Dummy::class, 'bar');
$bazAcessor = $this->extractor->getReadInfo(Dummy::class, 'baz');
$bazMutator = $this->extractor->getWriteInfo(Dummy::class, 'baz');
$this->assertNull($barAcessor);
$this->assertEquals(PropertyWriteInfo::TYPE_NONE, $barMutator->getType());
$this->assertNull($bazAcessor);
$this->assertEquals(PropertyWriteInfo::TYPE_NONE, $bazMutator->getType());
}
public function testTypedProperties()
{
$this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], $this->extractor->getTypes(Php74Dummy::class, 'dummy'));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_BOOL, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableBoolProp'));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], $this->extractor->getTypes(Php74Dummy::class, 'stringCollection'));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_INT, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableWithDefault'));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)], $this->extractor->getTypes(Php74Dummy::class, 'collection'));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class))], $this->extractor->getTypes(Php74Dummy::class, 'nullableTypedCollection'));
}
/**
* @dataProvider readAccessorProvider
*/
public function testGetReadAccessor($class, $property, $found, $type, $name, $visibility, $static)
{
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE);
$readAcessor = $extractor->getReadInfo($class, $property);
if (!$found) {
$this->assertNull($readAcessor);
return;
}
$this->assertNotNull($readAcessor);
$this->assertSame($type, $readAcessor->getType());
$this->assertSame($name, $readAcessor->getName());
$this->assertSame($visibility, $readAcessor->getVisibility());
$this->assertSame($static, $readAcessor->isStatic());
}
public static function readAccessorProvider(): array
{
return [
[Dummy::class, 'bar', true, PropertyReadInfo::TYPE_PROPERTY, 'bar', PropertyReadInfo::VISIBILITY_PRIVATE, false],
[Dummy::class, 'baz', true, PropertyReadInfo::TYPE_PROPERTY, 'baz', PropertyReadInfo::VISIBILITY_PROTECTED, false],
[Dummy::class, 'bal', true, PropertyReadInfo::TYPE_PROPERTY, 'bal', PropertyReadInfo::VISIBILITY_PUBLIC, false],
[Dummy::class, 'parent', true, PropertyReadInfo::TYPE_PROPERTY, 'parent', PropertyReadInfo::VISIBILITY_PUBLIC, false],
[Dummy::class, 'static', true, PropertyReadInfo::TYPE_METHOD, 'getStatic', PropertyReadInfo::VISIBILITY_PUBLIC, true],
[Dummy::class, 'foo', true, PropertyReadInfo::TYPE_PROPERTY, 'foo', PropertyReadInfo::VISIBILITY_PUBLIC, false],
[Php71Dummy::class, 'foo', true, PropertyReadInfo::TYPE_METHOD, 'getFoo', PropertyReadInfo::VISIBILITY_PUBLIC, false],
[Php71Dummy::class, 'buz', true, PropertyReadInfo::TYPE_METHOD, 'getBuz', PropertyReadInfo::VISIBILITY_PUBLIC, false],
];
}
/**
* @dataProvider writeMutatorProvider
*/
public function testGetWriteMutator($class, $property, $allowConstruct, $found, $type, $name, $addName, $removeName, $visibility, $static)
{
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE);
$writeMutator = $extractor->getWriteInfo($class, $property, [
'enable_constructor_extraction' => $allowConstruct,
'enable_getter_setter_extraction' => true,
]);
if (!$found) {
$this->assertEquals(PropertyWriteInfo::TYPE_NONE, $writeMutator->getType());
return;
}
$this->assertNotNull($writeMutator);
$this->assertSame($type, $writeMutator->getType());
if (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $writeMutator->getType()) {
$this->assertNotNull($writeMutator->getAdderInfo());
$this->assertSame($addName, $writeMutator->getAdderInfo()->getName());
$this->assertNotNull($writeMutator->getRemoverInfo());
$this->assertSame($removeName, $writeMutator->getRemoverInfo()->getName());
}
if (PropertyWriteInfo::TYPE_CONSTRUCTOR === $writeMutator->getType()) {
$this->assertSame($name, $writeMutator->getName());
}
if (PropertyWriteInfo::TYPE_PROPERTY === $writeMutator->getType()) {
$this->assertSame($name, $writeMutator->getName());
$this->assertSame($visibility, $writeMutator->getVisibility());
$this->assertSame($static, $writeMutator->isStatic());
}
if (PropertyWriteInfo::TYPE_METHOD === $writeMutator->getType()) {
$this->assertSame($name, $writeMutator->getName());
$this->assertSame($visibility, $writeMutator->getVisibility());
$this->assertSame($static, $writeMutator->isStatic());
}
}
public static function writeMutatorProvider(): array
{
return [
[Dummy::class, 'bar', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'bar', null, null, PropertyWriteInfo::VISIBILITY_PRIVATE, false],
[Dummy::class, 'baz', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'baz', null, null, PropertyWriteInfo::VISIBILITY_PROTECTED, false],
[Dummy::class, 'bal', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'bal', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Dummy::class, 'parent', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'parent', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Dummy::class, 'staticSetter', false, true, PropertyWriteInfo::TYPE_METHOD, 'staticSetter', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, true],
[Dummy::class, 'foo', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'foo', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71Dummy::class, 'bar', false, true, PropertyWriteInfo::TYPE_METHOD, 'setBar', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71Dummy::class, 'string', false, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71Dummy::class, 'string', true, true, PropertyWriteInfo::TYPE_CONSTRUCTOR, 'string', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71Dummy::class, 'baz', false, true, PropertyWriteInfo::TYPE_ADDER_AND_REMOVER, null, 'addBaz', 'removeBaz', PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71DummyExtended::class, 'bar', false, true, PropertyWriteInfo::TYPE_METHOD, 'setBar', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71DummyExtended::class, 'string', false, false, -1, '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71DummyExtended::class, 'string', true, true, PropertyWriteInfo::TYPE_CONSTRUCTOR, 'string', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71DummyExtended::class, 'baz', false, true, PropertyWriteInfo::TYPE_ADDER_AND_REMOVER, null, 'addBaz', 'removeBaz', PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71DummyExtended2::class, 'bar', false, true, PropertyWriteInfo::TYPE_METHOD, 'setBar', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71DummyExtended2::class, 'string', false, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71DummyExtended2::class, 'string', true, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false],
[Php71DummyExtended2::class, 'baz', false, true, PropertyWriteInfo::TYPE_ADDER_AND_REMOVER, null, 'addBaz', 'removeBaz', PropertyWriteInfo::VISIBILITY_PUBLIC, false],
];
}
public function testDisabledAdderAndRemoverReturnsError()
{
$writeMutator = $this->extractor->getWriteInfo(Php71Dummy::class, 'baz', [
'enable_adder_remover_extraction' => false,
]);
self::assertNotNull($writeMutator);
self::assertSame(PropertyWriteInfo::TYPE_NONE, $writeMutator->getType());
self::assertSame([\sprintf('The property "baz" in class "%s" can be defined with the methods "addBaz()", "removeBaz()" but the new value must be an array or an instance of \Traversable', Php71Dummy::class)], $writeMutator->getErrors());
}
public function testGetWriteInfoReadonlyProperties()
{
$writeMutatorConstructor = $this->extractor->getWriteInfo(Php81Dummy::class, 'foo', ['enable_constructor_extraction' => true]);
$writeMutatorWithoutConstructor = $this->extractor->getWriteInfo(Php81Dummy::class, 'foo', ['enable_constructor_extraction' => false]);
$this->assertSame(PropertyWriteInfo::TYPE_CONSTRUCTOR, $writeMutatorConstructor->getType());
$this->assertSame(PropertyWriteInfo::TYPE_NONE, $writeMutatorWithoutConstructor->getType());
}
/**
* @dataProvider extractConstructorTypesProvider
*/
public function testExtractConstructorTypes(string $property, ?array $type = null)
{
$this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property));
}
public static function extractConstructorTypesProvider(): array
{
return [
['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]],
['date', null],
['dateObject', null],
['dateTime', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]],
['ddd', null],
];
}
/**
* @requires PHP 8.4
*/
public function testAsymmetricVisibility()
{
$this->assertTrue($this->extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate'));
$this->assertTrue($this->extractor->isReadable(AsymmetricVisibility::class, 'publicProtected'));
$this->assertFalse($this->extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate'));
$this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate'));
$this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'publicProtected'));
$this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
}
/**
* @requires PHP 8.4
*/
public function testAsymmetricVisibilityAllowPublicOnly()
{
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC);
$this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate'));
$this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected'));
$this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate'));
$this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate'));
$this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected'));
$this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
}
/**
* @requires PHP 8.4
*/
public function testAsymmetricVisibilityAllowProtectedOnly()
{
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PROTECTED);
$this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate'));
$this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected'));
$this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate'));
$this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate'));
$this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected'));
$this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
}
/**
* @requires PHP 8.4
*/
public function testAsymmetricVisibilityAllowPrivateOnly()
{
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PRIVATE);
$this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate'));
$this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected'));
$this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate'));
$this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate'));
$this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected'));
$this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
}
/**
* @requires PHP 8.4
*/
public function testVirtualProperties()
{
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualNoSetHook'));
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualSetHookOnly'));
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualHook'));
$this->assertFalse($this->extractor->isWritable(VirtualProperties::class, 'virtualNoSetHook'));
$this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualSetHookOnly'));
$this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualHook'));
}
/**
* @dataProvider provideAsymmetricVisibilityMutator
* @requires PHP 8.4
*/
public function testAsymmetricVisibilityMutator(string $property, string $readVisibility, string $writeVisibility)
{
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE);
$readMutator = $extractor->getReadInfo(AsymmetricVisibility::class, $property);
$writeMutator = $extractor->getWriteInfo(AsymmetricVisibility::class, $property, [
'enable_getter_setter_extraction' => true,
]);
$this->assertSame(PropertyReadInfo::TYPE_PROPERTY, $readMutator->getType());
$this->assertSame(PropertyWriteInfo::TYPE_PROPERTY, $writeMutator->getType());
$this->assertSame($readVisibility, $readMutator->getVisibility());
$this->assertSame($writeVisibility, $writeMutator->getVisibility());
}
public static function provideAsymmetricVisibilityMutator(): iterable
{
yield ['publicPrivate', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PRIVATE];
yield ['publicProtected', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PROTECTED];
yield ['protectedPrivate', PropertyReadInfo::VISIBILITY_PROTECTED, PropertyWriteInfo::VISIBILITY_PRIVATE];
}
/**
* @dataProvider provideVirtualPropertiesMutator
* @requires PHP 8.4
*/
public function testVirtualPropertiesMutator(string $property, string $readVisibility, string $writeVisibility)
{
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE);
$readMutator = $extractor->getReadInfo(VirtualProperties::class, $property);
$writeMutator = $extractor->getWriteInfo(VirtualProperties::class, $property, [
'enable_getter_setter_extraction' => true,
]);
$this->assertSame(PropertyReadInfo::TYPE_PROPERTY, $readMutator->getType());
$this->assertSame(PropertyWriteInfo::TYPE_PROPERTY, $writeMutator->getType());
$this->assertSame($readVisibility, $readMutator->getVisibility());
$this->assertSame($writeVisibility, $writeMutator->getVisibility());
}
public static function provideVirtualPropertiesMutator(): iterable
{
yield ['virtualNoSetHook', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PRIVATE];
yield ['virtualSetHookOnly', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PUBLIC];
yield ['virtualHook', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PUBLIC];
}
}

View File

@@ -0,0 +1,63 @@
<?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\Tests\Extractor;
use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor;
use Symfony\Component\PropertyInfo\Tests\Fixtures\AdderRemoverDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\IgnorePropertyDummy;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class SerializerExtractorTest extends TestCase
{
private SerializerExtractor $extractor;
protected function setUp(): void
{
if (class_exists(AttributeLoader::class)) {
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
} else {
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
}
$this->extractor = new SerializerExtractor($classMetadataFactory);
}
public function testGetProperties()
{
$this->assertEquals(
['collection'],
$this->extractor->getProperties(Dummy::class, ['serializer_groups' => ['a']])
);
}
public function testGetPropertiesWithIgnoredProperties()
{
$this->assertSame(['visibleProperty'], $this->extractor->getProperties(IgnorePropertyDummy::class, ['serializer_groups' => ['a']]));
}
public function testGetPropertiesWithAnyGroup()
{
$this->assertSame(['analyses', 'feet'], $this->extractor->getProperties(AdderRemoverDummy::class, ['serializer_groups' => null]));
}
public function testGetPropertiesWithNonExistentClassReturnsNull()
{
$this->assertSame(null, $this->extractor->getProperties('NonExistent'));
}
}

View File

@@ -0,0 +1,29 @@
<?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\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AdderRemoverDummy
{
private $analyses;
private $feet;
public function addAnalyse(Dummy $analyse)
{
}
public function removeFoot(Dummy $foot)
{
}
}

View File

@@ -0,0 +1,19 @@
<?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\Tests\Fixtures;
class AsymmetricVisibility
{
public private(set) mixed $publicPrivate;
public protected(set) mixed $publicProtected;
protected private(set) mixed $protectedPrivate;
}

View File

@@ -0,0 +1,40 @@
<?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\Tests\Fixtures;
/**
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
*/
class ConstructorDummy
{
/** @var string */
private $timezone;
/** @var \DateTimeInterface */
private $date;
/** @var int */
private $dateTime;
/**
* @param \DateTimeZone $timezone
* @param int $date Timestamp
* @param \DateTimeInterface $dateObject
* @param mixed $mixed
*/
public function __construct(\DateTimeZone $timezone, $date, $dateObject, \DateTimeImmutable $dateTime, $mixed)
{
$this->timezone = $timezone->getName();
$this->date = \DateTimeImmutable::createFromFormat('U', $date);
$this->dateTime = $dateTime->getTimestamp();
}
}

View File

@@ -0,0 +1,19 @@
<?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\Tests\Fixtures;
class ConstructorDummyWithoutDocBlock
{
public function __construct(\DateTimeZone $timezone, $date, $dateObject, \DateTimeImmutable $dateTime, $mixed)
{
}
}

View File

@@ -0,0 +1,24 @@
<?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\Tests\Fixtures;
/**
* @author Tales Santos <tales.augusto.santos@gmail.com>
*/
class DefaultValue
{
public $defaultInt = 30;
public $defaultFloat = 30.5;
public $defaultString = 'foo';
public $defaultArray = [];
public $defaultNull = null;
}

View File

@@ -0,0 +1,62 @@
<?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\Tests\Fixtures;
/**
* PhpDocExtractor should fallback from property -> accessor -> mutator when looking up docblocks.
*
* @author Martin Rademacher <mano@radebatz.net>
*/
class DockBlockFallback
{
/** @var string $pub */
public $pub = 'pub';
protected $protAcc;
protected $protMut;
public function getPub()
{
return $this->pub;
}
public function setPub($pub)
{
$this->pub = $pub;
}
/**
* @return int
*/
public function getProtAcc(): int
{
return $this->protAcc;
}
public function setProt($protAcc)
{
$this->protAcc = $protAcc;
}
public function getProtMut()
{
return $this->protMut;
}
/**
* @param bool $protMut
*/
public function setProtMut($protMut)
{
$this->protMut = $protMut;
}
}

View File

@@ -0,0 +1,272 @@
<?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\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\Groups as GroupsAnnotation;
use Symfony\Component\Serializer\Attribute\Groups;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class Dummy extends ParentDummy
{
/**
* @var string This is bar
*/
private $bar;
/**
* Should be used.
*
* @var int Should be ignored
*/
protected $baz;
/**
* @var \DateTimeImmutable
*/
public $bal;
/**
* @var ParentDummy
*/
public $parent;
/**
* @var \DateTimeImmutable[]
* @GroupsAnnotation({"a", "b"})
*/
#[Groups(['a', 'b'])]
public $collection;
/**
* @var DummyCollection<int, string>
*/
public $collectionAsObject;
/**
* @var string[][]
*/
public $nestedCollection;
/**
* @var mixed[]
*/
public $mixedCollection;
/**
* @var ParentDummy
*/
public $B;
/**
* @var int
*/
protected $Id;
/**
* @var string
*/
public $Guid;
/**
* Nullable array.
*
* @var array|null
*/
public $g;
/**
* @var ?string
*/
public $h;
/**
* @var string|int|null
*/
public $i;
/**
* @var ?\DateTimeImmutable
*/
public $j;
/**
* @var int[]|null
*/
public $nullableCollectionOfNonNullableElements;
/**
* @var array<null|int>
*/
public $nonNullableCollectionOfNullableElements;
/**
* @var null|array<int|string>
*/
public $nullableCollectionOfMultipleNonNullableElementTypes;
/**
* @var array
*/
private $xTotals;
/**
* @var string
*/
private $YT;
/**
* This should not be removed.
*
* @var
*/
public $emptyVar;
/**
* @var \Iterator<string>
*/
public $iteratorCollection;
/**
* @var \Iterator<integer,string>
*/
public $iteratorCollectionWithKey;
/**
* @var \Iterator<integer,\Iterator<integer,string>>
*/
public $nestedIterators;
/**
* @var array<string,string>
*/
public $arrayWithKeys;
/**
* @var array<string,array<integer,null|string>|null>
*/
public $arrayWithKeysAndComplexValue;
/**
* @var array<string,mixed>
*/
public $arrayOfMixed;
/**
* @var list<string>
*/
public $listOfStrings;
/**
* @var parent
*/
public $parentAnnotation;
/**
* @var \BackedEnum<string>
*/
public $genericInterface;
public static function getStatic()
{
}
/**
* @return string
*/
public static function staticGetter()
{
}
public static function staticSetter(\DateTimeImmutable $d)
{
}
/**
* A.
*
* @return int
*/
public function getA()
{
}
/**
* B.
*
* @param ParentDummy|null $parent
*/
public function setB(?ParentDummy $parent = null)
{
}
/**
* Date of Birth.
*
* @return \DateTimeImmutable
*/
public function getDOB()
{
}
/**
* @return int
*/
public function getId()
{
}
public function get123()
{
}
/**
* @param self $self
*/
public function setSelf(self $self)
{
}
/**
* @param parent $realParent
*/
public function setRealParent(parent $realParent)
{
}
/**
* @return array
*/
public function getXTotals()
{
}
/**
* @return string
*/
public function getYT()
{
}
public function setDate(\DateTimeImmutable $date)
{
}
public function addDate(\DateTimeImmutable $date)
{
}
public function hasElement(string $element): bool
{
}
}

View File

@@ -0,0 +1,20 @@
<?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\Tests\Fixtures;
final class DummyCollection implements \IteratorAggregate
{
public function getIterator(): \Traversable
{
return [];
}
}

View File

@@ -0,0 +1,66 @@
<?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\Tests\Fixtures;
use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, ConstructorArgumentTypeExtractorInterface
{
public function getShortDescription($class, $property, array $context = []): ?string
{
return 'short';
}
public function getLongDescription($class, $property, array $context = []): ?string
{
return 'long';
}
public function getTypes($class, $property, array $context = []): ?array
{
return [new Type(Type::BUILTIN_TYPE_INT)];
}
public function getTypesFromConstructor(string $class, string $property): ?array
{
return [new Type(Type::BUILTIN_TYPE_STRING)];
}
public function isReadable($class, $property, array $context = []): ?bool
{
return true;
}
public function isWritable($class, $property, array $context = []): ?bool
{
return true;
}
public function getProperties($class, array $context = []): ?array
{
return ['a', 'b'];
}
public function isInitializable(string $class, string $property, array $context = []): ?bool
{
return true;
}
}

View File

@@ -0,0 +1,41 @@
<?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\Tests\Fixtures;
interface IFace {}
class Clazz {}
class DummyGeneric
{
/**
* @var Clazz<Dummy>
*/
public $basicClass;
/**
* @var ?Clazz<Dummy>
*/
public $nullableClass;
/**
* @var IFace<Dummy>
*/
public $basicInterface;
/**
* @var ?IFace<Dummy>
*/
public $nullableInterface;
}

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\Tests\Fixtures;
use Symfony\Component\PropertyInfo\Tests as TestNamespace;
/**
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*/
class DummyNamespace
{
/** @var TestNamespace\Fixtures\Dummy */
private $dummy;
}

View File

@@ -0,0 +1,56 @@
<?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\Tests\Fixtures;
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait;
trait DummyTraitExternal
{
/**
* @var string
*/
private $propertyInExternalTraitPrimitiveType;
/**
* @var Dummy
*/
private $propertyInExternalTraitObjectSameNamespace;
/**
* @var DummyUsedInTrait
*/
private $propertyInExternalTraitObjectDifferentNamespace;
/**
* @return string
*/
public function getMethodInExternalTraitPrimitiveType()
{
return 'value';
}
/**
* @return Dummy
*/
public function getMethodInExternalTraitObjectSameNamespace()
{
return new Dummy();
}
/**
* @return DummyUsedInTrait
*/
public function getMethodInExternalTraitObjectDifferentNamespace()
{
return new DummyUsedInTrait();
}
}

View File

@@ -0,0 +1,56 @@
<?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\Tests\Fixtures;
/**
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*/
class DummyUnionType
{
private const TYPE_A = 'a';
private const TYPE_B = 'b';
/**
* @var string|int
*/
public $a;
/**
* @var (string|int)[]
*/
public $b;
/**
* @var array<string|int>
*/
public $c;
/**
* @var array<string|int, array<string>>
*/
public $d;
/**
* @var (Dummy<array<mixed, string>, (int | (\Traversable<DefaultValue>)[])> | ParentDummy | null)
*/
public $e;
/**
* @var self::TYPE_*|null
*/
public $f;
/**
* @var non-empty-array<string|int>
*/
public $g;
}

View File

@@ -0,0 +1,29 @@
<?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 A {
class Property {
}
class Dummy {
/**
* @var Property
*/
public $property;
}
}
namespace B {
class Dummy extends \A\Dummy {
}
}

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\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\Groups as GroupsAnnotation;
use Symfony\Component\Serializer\Annotation\Ignore as IgnoreAnnotation;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\Ignore;
/**
* @author Vadim Borodavko <vadim.borodavko@gmail.com>
*/
class IgnorePropertyDummy
{
/**
* @GroupsAnnotation({"a"})
*/
#[Groups(['a'])]
public $visibleProperty;
/**
* @GroupsAnnotation({"a"})
* @IgnoreAnnotation
*/
#[Groups(['a']), Ignore]
private $ignoredProperty;
}

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\Tests\Fixtures;
class IntRangeDummy
{
/**
* @var int<0, 100>
*/
public $a;
/**
* @var int<min, 100>|null
*/
public $b;
/**
* @var int<50, max>
*/
public $c;
}

View File

@@ -0,0 +1,50 @@
<?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\Tests\Fixtures;
/**
* @author Martin Rademacher <mano@radebatz.net>
*/
class InvalidDummy
{
/**
* @var
*/
public $pub;
/**
* @return
*/
public static function getStat()
{
return 'stat';
}
/**
* Foo.
*
* @param
*/
public function setFoo($foo)
{
}
/**
* Bar.
*
* @return
*/
public function getBar()
{
return 'bar';
}
}

View File

@@ -0,0 +1,19 @@
<?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\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class NoProperties
{
}

View File

@@ -0,0 +1,22 @@
<?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\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class NotInstantiable
{
private function __construct(string $foo)
{
}
}

View File

@@ -0,0 +1,85 @@
<?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\Tests\Fixtures;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
/**
* Not able to guess anything.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class NullExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface
{
public function getShortDescription($class, $property, array $context = []): ?string
{
$this->assertIsString($class);
$this->assertIsString($property);
return null;
}
public function getLongDescription($class, $property, array $context = []): ?string
{
$this->assertIsString($class);
$this->assertIsString($property);
return null;
}
public function getTypes($class, $property, array $context = []): ?array
{
$this->assertIsString($class);
$this->assertIsString($property);
return null;
}
public function isReadable($class, $property, array $context = []): ?bool
{
$this->assertIsString($class);
$this->assertIsString($property);
return null;
}
public function isWritable($class, $property, array $context = []): ?bool
{
$this->assertIsString($class);
$this->assertIsString($property);
return null;
}
public function getProperties($class, array $context = []): ?array
{
$this->assertIsString($class);
return null;
}
public function isInitializable(string $class, string $property, array $context = []): ?bool
{
return null;
}
private function assertIsString($string)
{
if (!\is_string($string)) {
throw new \InvalidArgumentException(sprintf('"%s" expects strings, given "%s".', __CLASS__, get_debug_type($string)));
}
}
}

View File

@@ -0,0 +1,128 @@
<?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\Tests\Fixtures;
use Symfony\Component\PropertyInfo\Tests\Fixtures\RootDummy\RootDummyItem;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ParentDummy
{
/**
* Short description.
*
* Long description.
*/
public $foo;
/**
* @var float
*/
public $foo2;
/**
* @var callable
*/
public $foo3;
/**
* @var void
*/
public $foo4;
/**
* @var mixed
*/
public $foo5;
/**
* @var \SplFileInfo[]|resource
*/
public $files;
/**
* @var static
*/
public $propertyTypeStatic;
/**
* @var parent
*/
public $parentAnnotationNoParent;
/**
* @var RootDummyItem[]
*/
public $rootDummyItems;
/**
* @var \Symfony\Component\PropertyInfo\Tests\Fixtures\RootDummy\RootDummyItem
*/
public $rootDummyItem;
/**
* @return bool|null
*/
public function isC()
{
}
/**
* @return true|null
*/
public function isCt()
{
}
/**
* @return false|null
*/
public function isCf()
{
}
/**
* @return bool
*/
public function canD()
{
}
/**
* @return true
*/
public function canDt()
{
}
/**
* @return false
*/
public function canDf()
{
}
/**
* @param resource $e
*/
public function addE($e)
{
}
/**
* @param \DateTimeImmutable $f
*/
public function removeF(\DateTimeImmutable $f)
{
}
}

View File

@@ -0,0 +1,57 @@
<?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\Tests\Fixtures;
/**
* @author Teoh Han Hui <teohhanhui@gmail.com>
*/
class Php71Dummy
{
public function __construct(string $string, int $intPrivate)
{
}
public function getFoo(): ?array
{
}
public function getBuz(): void
{
}
public function setBar(?int $bar)
{
}
public function addBaz(string $baz)
{
}
public function removeBaz(string $baz)
{
}
}
class Php71DummyExtended extends Php71Dummy
{
}
class Php71DummyExtended2 extends Php71Dummy
{
public function __construct(int $intWithAccessor)
{
}
public function getIntWithAccessor()
{
}
}

View File

@@ -0,0 +1,40 @@
<?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\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class Php74Dummy
{
public Dummy $dummy;
private ?bool $nullableBoolProp;
/** @var string[] */
private array $stringCollection;
private ?int $nullableWithDefault = 1;
public array $collection = [];
/** @var Dummy[]|null */
public ?array $nullableTypedCollection = null;
public function addStringCollection(string $string): void
{
}
public function removeStringCollection(string $string): void
{
}
public function addNullableTypedCollection(Dummy $dummy): void
{
}
}

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\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class Php7Dummy extends Php7ParentDummy
{
public function getFoo(): array
{
}
public function setBar(int $bar)
{
}
public function addBaz(string $baz)
{
}
public function getBuz(): self
{
}
public function getBiz(): parent
{
}
}

View File

@@ -0,0 +1,19 @@
<?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\Tests\Fixtures;
class Php7ParentDummy extends \stdClass
{
public function getParent(): parent
{
}
}

View File

@@ -0,0 +1,67 @@
<?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\Tests\Fixtures;
class Php80Dummy
{
public mixed $mixedProperty;
/**
* @param string $promotedAndMutated
* @param string $promotedWithDocComment
* @param string $promotedWithDocCommentAndType
* @param array<string> $collection
*/
public function __construct(
private mixed $promoted,
private mixed $promotedAndMutated,
/**
* Comment without @var.
*/
private mixed $promotedWithDocComment,
/**
* @var int
*/
private mixed $promotedWithDocCommentAndType,
private array $collection,
)
{
}
public function getFoo(): array|null
{
}
public function setBar(int|null $bar)
{
}
public function setTimeout(int|float $timeout)
{
}
public function getOptional(): int|float|null
{
}
public function setString(string|\Stringable $string)
{
}
public function setPayload(mixed $payload)
{
}
public function getData(): mixed
{
}
}

View File

@@ -0,0 +1,24 @@
<?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\Tests\Fixtures;
class Php80PromotedDummy
{
public function __construct(private string $promoted)
{
}
public function getPromoted(): string
{
return $this->promoted;
}
}

View File

@@ -0,0 +1,28 @@
<?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\Tests\Fixtures;
class Php81Dummy
{
public function __construct(public readonly string $foo)
{
}
public function getNothing(): never
{
throw new \Exception('Oops');
}
public function getCollection(): \Traversable&\Countable
{
}
}

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\Tests\Fixtures;
class Php82Dummy
{
public null $nil = null;
public false $false = false;
public true $true = true;
public (\Traversable&\Countable)|null $someCollection = null;
}

View File

@@ -0,0 +1,45 @@
<?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\Tests\Fixtures;
/**
* @author Emil Masiakowski <emil.masiakowski@gmail.com>
*/
class PhpStanPseudoTypesDummy extends PseudoTypesDummy
{
/** @var negative-int */
public $negativeInt;
/** @var non-empty-array */
public $nonEmptyArray;
/** @var non-empty-list */
public $nonEmptyList;
/** @var interface-string */
public $interfaceString;
/** @var scalar */
public $scalar;
/** @var array-key */
public $arrayKey;
/** @var number */
public $number;
/** @var numeric */
public $numeric;
/** @var double */
public $double;
}

View File

@@ -0,0 +1,20 @@
<?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\Tests\Fixtures;
class PseudoTypeDummy
{
/**
* @var scalar
*/
public $unknownPseudoType;
}

View File

@@ -0,0 +1,48 @@
<?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\Tests\Fixtures;
/**
* @author Emil Masiakowski <emil.masiakowski@gmail.com>
*/
class PseudoTypesDummy
{
/** @var class-string */
public $classString;
/** @var class-string<\stdClass> */
public $classStringGeneric;
/** @var html-escaped-string */
public $htmlEscapedString;
/** @var lowercase-string */
public $lowercaseString;
/** @var non-empty-lowercase-string */
public $nonEmptyLowercaseString;
/** @var non-empty-string */
public $nonEmptyString;
/** @var numeric-string */
public $numericString;
/** @var trait-string */
public $traitString;
/** @var positive-int */
public $positiveInt;
/** @var literal-string */
public $literalString;
}

View File

@@ -0,0 +1,16 @@
<?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\Tests\Fixtures\RootDummy;
class RootDummyItem
{
}

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\Tests\Fixtures;
class SnakeCaseDummy
{
private string $snake_property;
private string $snake_readOnly;
private string $snake_method;
public function getSnakeProperty()
{
return $this->snake_property;
}
public function getSnakeReadOnly()
{
return $this->snake_readOnly;
}
public function setSnakeProperty($snake_property)
{
$this->snake_property = $snake_property;
}
public function setSnake_method($snake_method)
{
$this->snake_method = $snake_method;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\AnotherNamespace;
class DummyInAnotherNamespace
{
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\AnotherNamespace;
trait DummyTraitInAnotherNamespace
{
/**
* @var DummyInAnotherNamespace
*/
public $dummyInAnotherNamespace;
}

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\Tests\Fixtures\TraitUsage;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyTraitExternal;
trait DummyTrait
{
use DummyTraitExternal;
/**
* @var string
*/
private $propertyInTraitPrimitiveType;
/**
* @var DummyUsedInTrait
*/
private $propertyInTraitObjectSameNamespace;
/**
* @var Dummy
*/
private $propertyInTraitObjectDifferentNamespace;
/**
* @return string
*/
public function getMethodInTraitPrimitiveType()
{
return 'value';
}
/**
* @return DummyUsedInTrait
*/
public function getMethodInTraitObjectSameNamespace()
{
return new DummyUsedInTrait();
}
/**
* @return Dummy
*/
public function getMethodInTraitObjectDifferentNamespace()
{
return new Dummy();
}
}

View File

@@ -0,0 +1,16 @@
<?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\Tests\Fixtures\TraitUsage;
class DummyUsedInTrait
{
}

View File

@@ -0,0 +1,20 @@
<?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\Tests\Fixtures\TraitUsage;
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\AnotherNamespace\DummyTraitInAnotherNamespace;
class DummyUsingTrait
{
use DummyTrait;
use DummyTraitInAnotherNamespace;
}

View File

@@ -0,0 +1,19 @@
<?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\Tests\Fixtures;
class VirtualProperties
{
public bool $virtualNoSetHook { get => true; }
public bool $virtualSetHookOnly { set => $value; }
public bool $virtualHook { get => true; set => $value; }
}

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\Tests;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertyInfoCacheExtractorTest extends AbstractPropertyInfoExtractorTest
{
protected function setUp(): void
{
parent::setUp();
$this->propertyInfo = new PropertyInfoCacheExtractor($this->propertyInfo, new ArrayAdapter());
}
public function testGetShortDescription()
{
parent::testGetShortDescription();
parent::testGetShortDescription();
}
public function testGetLongDescription()
{
parent::testGetLongDescription();
parent::testGetLongDescription();
}
public function testGetTypes()
{
parent::testGetTypes();
parent::testGetTypes();
}
public function testIsReadable()
{
parent::testIsReadable();
parent::testIsReadable();
}
public function testIsWritable()
{
parent::testIsWritable();
parent::testIsWritable();
}
public function testGetProperties()
{
parent::testGetProperties();
parent::testGetProperties();
}
public function testIsInitializable()
{
parent::testIsInitializable();
parent::testIsInitializable();
}
}

View File

@@ -0,0 +1,19 @@
<?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\Tests;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertyInfoExtractorTest extends AbstractPropertyInfoExtractorTest
{
}

View File

@@ -0,0 +1,88 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Type;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class TypeTest extends TestCase
{
public function testConstruct()
{
$type = new Type('object', true, 'ArrayObject', true, new Type('int'), new Type('string'));
$this->assertEquals(Type::BUILTIN_TYPE_OBJECT, $type->getBuiltinType());
$this->assertTrue($type->isNullable());
$this->assertEquals('ArrayObject', $type->getClassName());
$this->assertTrue($type->isCollection());
$collectionKeyTypes = $type->getCollectionKeyTypes();
$this->assertIsArray($collectionKeyTypes);
$this->assertContainsOnlyInstancesOf('Symfony\Component\PropertyInfo\Type', $collectionKeyTypes);
$this->assertEquals(Type::BUILTIN_TYPE_INT, $collectionKeyTypes[0]->getBuiltinType());
$collectionValueTypes = $type->getCollectionValueTypes();
$this->assertIsArray($collectionValueTypes);
$this->assertContainsOnlyInstancesOf('Symfony\Component\PropertyInfo\Type', $collectionValueTypes);
$this->assertEquals(Type::BUILTIN_TYPE_STRING, $collectionValueTypes[0]->getBuiltinType());
}
public function testIterable()
{
$type = new Type('iterable');
$this->assertSame('iterable', $type->getBuiltinType());
}
public function testInvalidType()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('"foo" is not a valid PHP type.');
new Type('foo');
}
public function testArrayCollection()
{
$type = new Type('array', false, null, true, [new Type('int'), new Type('string')], [new Type('object', false, \ArrayObject::class, true), new Type('array', false, null, true)]);
$this->assertEquals(Type::BUILTIN_TYPE_ARRAY, $type->getBuiltinType());
$this->assertFalse($type->isNullable());
$this->assertTrue($type->isCollection());
[$firstKeyType, $secondKeyType] = $type->getCollectionKeyTypes();
$this->assertEquals(Type::BUILTIN_TYPE_INT, $firstKeyType->getBuiltinType());
$this->assertFalse($firstKeyType->isNullable());
$this->assertFalse($firstKeyType->isCollection());
$this->assertEquals(Type::BUILTIN_TYPE_STRING, $secondKeyType->getBuiltinType());
$this->assertFalse($secondKeyType->isNullable());
$this->assertFalse($secondKeyType->isCollection());
[$firstValueType, $secondValueType] = $type->getCollectionValueTypes();
$this->assertEquals(Type::BUILTIN_TYPE_OBJECT, $firstValueType->getBuiltinType());
$this->assertEquals(\ArrayObject::class, $firstValueType->getClassName());
$this->assertFalse($firstValueType->isNullable());
$this->assertTrue($firstValueType->isCollection());
$this->assertEquals(Type::BUILTIN_TYPE_ARRAY, $secondValueType->getBuiltinType());
$this->assertFalse($secondValueType->isNullable());
$this->assertTrue($secondValueType->isCollection());
}
public function testInvalidCollectionValueArgument()
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('"Symfony\Component\PropertyInfo\Type::validateCollectionArgument()": Argument #5 ($collectionKeyType) must be of type "Symfony\Component\PropertyInfo\Type[]", "Symfony\Component\PropertyInfo\Type" or "null", array value "array" given.');
new Type('array', false, null, true, [new \stdClass()], [new Type('string')]);
}
}

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

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.3/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony Property Info Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>./</directory>
</include>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</coverage>
</phpunit>