Skip to content

Commit

Permalink
feat: use phpdoc-parser instead of phpdocumentor (#5214)
Browse files Browse the repository at this point in the history
PHPStan PHPDoc-Parser is used instead of phpDocumentor because the latter
is not actively maintained anymore.
  • Loading branch information
alanpoulain authored Nov 29, 2022
1 parent 40b637f commit a828af0
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 32 deletions.
5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"jangregor/phpstan-prophecy": "^1.0",
"justinrainbow/json-schema": "^5.2.1",
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.1",
"phpdocumentor/type-resolver": "^0.3 || ^0.4 || ^1.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpdoc-parser": "^1.13",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-doctrine": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
Expand Down Expand Up @@ -101,7 +100,7 @@
"doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.",
"elasticsearch/elasticsearch": "To support Elasticsearch.",
"ocramius/package-versions": "To display the API Platform's version in the debug bar.",
"phpdocumentor/reflection-docblock": "To support extracting metadata from PHPDoc.",
"phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.",
"psr/cache-implementation": "To use metadata caching.",
"ramsey/uuid": "To support Ramsey's UUID identifiers.",
"symfony/cache": "To have metadata caching when using Symfony integration.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\DocBlockFactoryInterface;
use phpDocumentor\Reflection\Types\ContextFactory;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
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;

/**
* Extracts descriptions from PHPDoc.
Expand All @@ -26,13 +33,33 @@
*/
final class PhpDocResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
{
private readonly DocBlockFactoryInterface $docBlockFactory;
private readonly ContextFactory $contextFactory;
private readonly ?DocBlockFactoryInterface $docBlockFactory;
private readonly ?ContextFactory $contextFactory;
private readonly ?PhpDocParser $phpDocParser;
private readonly ?Lexer $lexer;

public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated, DocBlockFactoryInterface $docBlockFactory = null)
/** @var array<string, PhpDocNode> */
private array $docBlocks = [];

public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated, ?DocBlockFactoryInterface $docBlockFactory = null)
{
$this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance();
$this->contextFactory = new ContextFactory();
$contextFactory = null;
if ($docBlockFactory instanceof DocBlockFactoryInterface) {
trigger_deprecation('api-platform/core', '3.1', 'Using a 2nd argument to PhpDocResourceMetadataCollectionFactory is deprecated.');
}
if (class_exists(DocBlockFactory::class) && class_exists(ContextFactory::class)) {
$docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance();
$contextFactory = new ContextFactory();
}
$this->docBlockFactory = $docBlockFactory;
$this->contextFactory = $contextFactory;
if (class_exists(DocBlockFactory::class) && !class_exists(PhpDocParser::class)) {
trigger_deprecation('api-platform/core', '3.1', 'Using phpdocumentor/reflection-docblock is deprecated. Require phpstan/phpdoc-parser instead.');
}
if (class_exists(PhpDocParser::class)) {
$this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
$this->lexer = new Lexer();
}
}

/**
Expand All @@ -47,41 +74,97 @@ public function create(string $resourceClass): ResourceMetadataCollection
continue;
}

$reflectionClass = new \ReflectionClass($resourceClass);
$description = null;

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

$operations = $resourceMetadata->getOperations() ?? new Operations();
foreach ($operations as $operationName => $operation) {
if (null !== $operation->getDescription()) {
continue;
}

$operations->add($operationName, $operation->withDescription($docBlock->getSummary()));
try {
$docBlock = $this->docBlockFactory->create($reflectionClass, $this->contextFactory->createFromReflector($reflectionClass));
$description = $docBlock->getSummary();
} catch (\InvalidArgumentException) {
// Ignore empty DocBlocks
}
} else {
$description = $this->getShortDescription($resourceClass);
}

$resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withOperations($operations);
if (!$description) {
return $resourceMetadataCollection;
}

if (!$resourceMetadata->getGraphQlOperations()) {
$resourceMetadataCollection[$key] = $resourceMetadata->withDescription($description);

$operations = $resourceMetadata->getOperations() ?? new Operations();
foreach ($operations as $operationName => $operation) {
if (null !== $operation->getDescription()) {
continue;
}

foreach ($graphQlOperations = $resourceMetadata->getGraphQlOperations() as $operationName => $operation) {
if (null !== $operation->getDescription()) {
continue;
}
$operations->add($operationName, $operation->withDescription($description));
}

$graphQlOperations[$operationName] = $operation->withDescription($docBlock->getSummary());
$resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withOperations($operations);

if (!$resourceMetadata->getGraphQlOperations()) {
continue;
}

foreach ($graphQlOperations = $resourceMetadata->getGraphQlOperations() as $operationName => $operation) {
if (null !== $operation->getDescription()) {
continue;
}

$resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withGraphQlOperations($graphQlOperations);
} catch (\InvalidArgumentException) {
// Ignore empty DocBlocks
$graphQlOperations[$operationName] = $operation->withDescription($description);
}

$resourceMetadataCollection[$key] = $resourceMetadataCollection[$key]->withGraphQlOperations($graphQlOperations);
}

return $resourceMetadataCollection;
}

/**
* Gets the short description of the class.
*/
private function getShortDescription(string $class): ?string
{
if (!$docBlock = $this->getDocBlock($class)) {
return null;
}

foreach ($docBlock->children as $docChild) {
if ($docChild instanceof PhpDocTextNode && !empty($docChild->text)) {
return $docChild->text;
}
}

return null;
}

private function getDocBlock(string $class): ?PhpDocNode
{
if (isset($this->docBlocks[$class])) {
return $this->docBlocks[$class];
}

try {
$reflectionClass = new \ReflectionClass($class);
} catch (\ReflectionException) {
return null;
}

$rawDocNode = $reflectionClass->getDocComment();

if (!$rawDocNode) {
return null;
}

$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
$phpDocNode = $this->phpDocParser->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_END);

return $this->docBlocks[$class] = $phpDocNode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface;
use Doctrine\Persistence\ManagerRegistry;
use phpDocumentor\Reflection\DocBlockFactoryInterface;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Config\FileLocator;
Expand Down Expand Up @@ -266,7 +267,7 @@ private function registerMetadataConfiguration(ContainerBuilder $container, arra
$container->getDefinition('api_platform.metadata.resource_extractor.xml')->replaceArgument(0, $xmlResources);
$container->getDefinition('api_platform.metadata.property_extractor.xml')->replaceArgument(0, $xmlResources);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\ORM\OptimisticLockException;
use phpDocumentor\Reflection\DocBlockFactoryInterface;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
Expand Down Expand Up @@ -427,8 +428,8 @@ public function testMetadataConfiguration(): void

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

$config = self::DEFAULT_CONFIG;
Expand Down

0 comments on commit a828af0

Please sign in to comment.