Skip to content

Commit a828af0

Browse files
authored
feat: use phpdoc-parser instead of phpdocumentor (#5214)
PHPStan PHPDoc-Parser is used instead of phpDocumentor because the latter is not actively maintained anymore.
1 parent 40b637f commit a828af0

File tree

4 files changed

+116
-32
lines changed

4 files changed

+116
-32
lines changed

composer.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,9 @@
4545
"guzzlehttp/guzzle": "^6.0 || ^7.0",
4646
"jangregor/phpstan-prophecy": "^1.0",
4747
"justinrainbow/json-schema": "^5.2.1",
48-
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.1",
49-
"phpdocumentor/type-resolver": "^0.3 || ^0.4 || ^1.4",
5048
"phpspec/prophecy-phpunit": "^2.0",
5149
"phpstan/extension-installer": "^1.1",
50+
"phpstan/phpdoc-parser": "^1.13",
5251
"phpstan/phpstan": "^1.1",
5352
"phpstan/phpstan-doctrine": "^1.0",
5453
"phpstan/phpstan-phpunit": "^1.0",
@@ -101,7 +100,7 @@
101100
"doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.",
102101
"elasticsearch/elasticsearch": "To support Elasticsearch.",
103102
"ocramius/package-versions": "To display the API Platform's version in the debug bar.",
104-
"phpdocumentor/reflection-docblock": "To support extracting metadata from PHPDoc.",
103+
"phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.",
105104
"psr/cache-implementation": "To use metadata caching.",
106105
"ramsey/uuid": "To support Ramsey's UUID identifiers.",
107106
"symfony/cache": "To have metadata caching when using Symfony integration.",

src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
use phpDocumentor\Reflection\DocBlockFactory;
1919
use phpDocumentor\Reflection\DocBlockFactoryInterface;
2020
use phpDocumentor\Reflection\Types\ContextFactory;
21+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
22+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
23+
use PHPStan\PhpDocParser\Lexer\Lexer;
24+
use PHPStan\PhpDocParser\Parser\ConstExprParser;
25+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
26+
use PHPStan\PhpDocParser\Parser\TokenIterator;
27+
use PHPStan\PhpDocParser\Parser\TypeParser;
2128

2229
/**
2330
* Extracts descriptions from PHPDoc.
@@ -26,13 +33,33 @@
2633
*/
2734
final class PhpDocResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
2835
{
29-
private readonly DocBlockFactoryInterface $docBlockFactory;
30-
private readonly ContextFactory $contextFactory;
36+
private readonly ?DocBlockFactoryInterface $docBlockFactory;
37+
private readonly ?ContextFactory $contextFactory;
38+
private readonly ?PhpDocParser $phpDocParser;
39+
private readonly ?Lexer $lexer;
3140

32-
public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated, DocBlockFactoryInterface $docBlockFactory = null)
41+
/** @var array<string, PhpDocNode> */
42+
private array $docBlocks = [];
43+
44+
public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated, ?DocBlockFactoryInterface $docBlockFactory = null)
3345
{
34-
$this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance();
35-
$this->contextFactory = new ContextFactory();
46+
$contextFactory = null;
47+
if ($docBlockFactory instanceof DocBlockFactoryInterface) {
48+
trigger_deprecation('api-platform/core', '3.1', 'Using a 2nd argument to PhpDocResourceMetadataCollectionFactory is deprecated.');
49+
}
50+
if (class_exists(DocBlockFactory::class) && class_exists(ContextFactory::class)) {
51+
$docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance();
52+
$contextFactory = new ContextFactory();
53+
}
54+
$this->docBlockFactory = $docBlockFactory;
55+
$this->contextFactory = $contextFactory;
56+
if (class_exists(DocBlockFactory::class) && !class_exists(PhpDocParser::class)) {
57+
trigger_deprecation('api-platform/core', '3.1', 'Using phpdocumentor/reflection-docblock is deprecated. Require phpstan/phpdoc-parser instead.');
58+
}
59+
if (class_exists(PhpDocParser::class)) {
60+
$this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
61+
$this->lexer = new Lexer();
62+
}
3663
}
3764

3865
/**
@@ -47,41 +74,97 @@ public function create(string $resourceClass): ResourceMetadataCollection
4774
continue;
4875
}
4976

50-
$reflectionClass = new \ReflectionClass($resourceClass);
77+
$description = null;
5178

52-
try {
53-
$docBlock = $this->docBlockFactory->create($reflectionClass, $this->contextFactory->createFromReflector($reflectionClass));
54-
$resourceMetadataCollection[$key] = $resourceMetadata->withDescription($docBlock->getSummary());
79+
// Deprecated path. To remove in API Platform 4.
80+
if (!$this->phpDocParser instanceof PhpDocParser && $this->docBlockFactory instanceof DocBlockFactoryInterface && $this->contextFactory) {
81+
$reflectionClass = new \ReflectionClass($resourceClass);
5582

56-
$operations = $resourceMetadata->getOperations() ?? new Operations();
57-
foreach ($operations as $operationName => $operation) {
58-
if (null !== $operation->getDescription()) {
59-
continue;
60-
}
61-
62-
$operations->add($operationName, $operation->withDescription($docBlock->getSummary()));
83+
try {
84+
$docBlock = $this->docBlockFactory->create($reflectionClass, $this->contextFactory->createFromReflector($reflectionClass));
85+
$description = $docBlock->getSummary();
86+
} catch (\InvalidArgumentException) {
87+
// Ignore empty DocBlocks
6388
}
89+
} else {
90+
$description = $this->getShortDescription($resourceClass);
91+
}
6492

65-
$resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withOperations($operations);
93+
if (!$description) {
94+
return $resourceMetadataCollection;
95+
}
6696

67-
if (!$resourceMetadata->getGraphQlOperations()) {
97+
$resourceMetadataCollection[$key] = $resourceMetadata->withDescription($description);
98+
99+
$operations = $resourceMetadata->getOperations() ?? new Operations();
100+
foreach ($operations as $operationName => $operation) {
101+
if (null !== $operation->getDescription()) {
68102
continue;
69103
}
70104

71-
foreach ($graphQlOperations = $resourceMetadata->getGraphQlOperations() as $operationName => $operation) {
72-
if (null !== $operation->getDescription()) {
73-
continue;
74-
}
105+
$operations->add($operationName, $operation->withDescription($description));
106+
}
75107

76-
$graphQlOperations[$operationName] = $operation->withDescription($docBlock->getSummary());
108+
$resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withOperations($operations);
109+
110+
if (!$resourceMetadata->getGraphQlOperations()) {
111+
continue;
112+
}
113+
114+
foreach ($graphQlOperations = $resourceMetadata->getGraphQlOperations() as $operationName => $operation) {
115+
if (null !== $operation->getDescription()) {
116+
continue;
77117
}
78118

79-
$resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withGraphQlOperations($graphQlOperations);
80-
} catch (\InvalidArgumentException) {
81-
// Ignore empty DocBlocks
119+
$graphQlOperations[$operationName] = $operation->withDescription($description);
82120
}
121+
122+
$resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withGraphQlOperations($graphQlOperations);
83123
}
84124

85125
return $resourceMetadataCollection;
86126
}
127+
128+
/**
129+
* Gets the short description of the class.
130+
*/
131+
private function getShortDescription(string $class): ?string
132+
{
133+
if (!$docBlock = $this->getDocBlock($class)) {
134+
return null;
135+
}
136+
137+
foreach ($docBlock->children as $docChild) {
138+
if ($docChild instanceof PhpDocTextNode && !empty($docChild->text)) {
139+
return $docChild->text;
140+
}
141+
}
142+
143+
return null;
144+
}
145+
146+
private function getDocBlock(string $class): ?PhpDocNode
147+
{
148+
if (isset($this->docBlocks[$class])) {
149+
return $this->docBlocks[$class];
150+
}
151+
152+
try {
153+
$reflectionClass = new \ReflectionClass($class);
154+
} catch (\ReflectionException) {
155+
return null;
156+
}
157+
158+
$rawDocNode = $reflectionClass->getDocComment();
159+
160+
if (!$rawDocNode) {
161+
return null;
162+
}
163+
164+
$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
165+
$phpDocNode = $this->phpDocParser->parse($tokens);
166+
$tokens->consumeTokenType(Lexer::TOKEN_END);
167+
168+
return $this->docBlocks[$class] = $phpDocNode;
169+
}
87170
}

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface;
3838
use Doctrine\Persistence\ManagerRegistry;
3939
use phpDocumentor\Reflection\DocBlockFactoryInterface;
40+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
4041
use Ramsey\Uuid\Uuid;
4142
use Symfony\Component\Cache\Adapter\ArrayAdapter;
4243
use Symfony\Component\Config\FileLocator;
@@ -266,7 +267,7 @@ private function registerMetadataConfiguration(ContainerBuilder $container, arra
266267
$container->getDefinition('api_platform.metadata.resource_extractor.xml')->replaceArgument(0, $xmlResources);
267268
$container->getDefinition('api_platform.metadata.property_extractor.xml')->replaceArgument(0, $xmlResources);
268269

269-
if (interface_exists(DocBlockFactoryInterface::class)) {
270+
if (class_exists(PhpDocParser::class) || interface_exists(DocBlockFactoryInterface::class)) {
270271
$loader->load('metadata/php_doc.xml');
271272
}
272273

tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
6767
use Doctrine\ORM\OptimisticLockException;
6868
use phpDocumentor\Reflection\DocBlockFactoryInterface;
69+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
6970
use PHPUnit\Framework\TestCase;
7071
use Prophecy\PhpUnit\ProphecyTrait;
7172
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
@@ -427,8 +428,8 @@ public function testMetadataConfiguration(): void
427428

428429
public function testMetadataConfigurationDocBlockFactoryInterface(): void
429430
{
430-
if (!interface_exists(DocBlockFactoryInterface::class)) {
431-
$this->markTestSkipped('class phpDocumentor\Reflection\DocBlockFactoryInterface does not exist');
431+
if (!class_exists(PhpDocParser::class) || !interface_exists(DocBlockFactoryInterface::class)) {
432+
$this->markTestSkipped('class PHPStan\PhpDocParser\Parser\PhpDocParser or phpDocumentor\Reflection\DocBlockFactoryInterface does not exist');
432433
}
433434

434435
$config = self::DEFAULT_CONFIG;

0 commit comments

Comments
 (0)