Skip to content

Commit

Permalink
Merge pull request #4842 from vincentchalamon/feat/upgrade-api-proper…
Browse files Browse the repository at this point in the history
…ty-visitor

Migrate ApiProperty to new format
  • Loading branch information
vincentchalamon committed Jul 25, 2022
2 parents cbb96f6 + e5d1c13 commit f2c8a61
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 1 deletion.
1 change: 1 addition & 0 deletions phpstan.neon.dist
Expand Up @@ -222,6 +222,7 @@ parameters:
- src/Core/Test/DoctrineMongoDbOdmFilterTestCase.php
- src/Core/Test/DoctrineOrmFilterTestCase.php
- src/Core/Upgrade/SubresourceTransformer.php
- src/Core/Upgrade/UpgradeApiPropertyVisitor.php
- src/Core/Upgrade/UpgradeApiResourceVisitor.php
- src/Core/Upgrade/UpgradeApiSubresourceVisitor.php
- src/Core/Upgrade/UpgradeApiFilterVisitor.php
Expand Down
Expand Up @@ -20,6 +20,7 @@
use ApiPlatform\Core\Upgrade\ColorConsoleDiffFormatter;
use ApiPlatform\Core\Upgrade\SubresourceTransformer;
use ApiPlatform\Core\Upgrade\UpgradeApiFilterVisitor;
use ApiPlatform\Core\Upgrade\UpgradeApiPropertyVisitor;
use ApiPlatform\Core\Upgrade\UpgradeApiResourceVisitor;
use ApiPlatform\Core\Upgrade\UpgradeApiSubresourceVisitor;
use ApiPlatform\Exception\ResourceClassNotFoundException;
Expand Down Expand Up @@ -109,6 +110,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
[$attribute, $isAnnotation] = $this->readApiResource($resourceClass);

$traverser->addVisitor(new UpgradeApiFilterVisitor($this->reader, $resourceClass));
$traverser->addVisitor(new UpgradeApiPropertyVisitor($this->reader, $resourceClass));

if (!$attribute) {
continue;
Expand Down
239 changes: 239 additions & 0 deletions src/Core/Upgrade/UpgradeApiPropertyVisitor.php
@@ -0,0 +1,239 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Upgrade;

use ApiPlatform\Core\Annotation\ApiProperty as LegacyApiProperty;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Resource\DeprecationMetadataTrait;
use Doctrine\Common\Annotations\AnnotationReader;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;

final class UpgradeApiPropertyVisitor extends NodeVisitorAbstract
{
use DeprecationMetadataTrait;
use RemoveAnnotationTrait;

private ?AnnotationReader $reader;
private \ReflectionClass $reflectionClass;

public function __construct(?AnnotationReader $reader, string $resourceClass)
{
$this->reader = $reader;
$this->reflectionClass = new \ReflectionClass($resourceClass);
}

/**
* @return int|Node|null
*/
public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\Namespace_) {
$namespaces = [ApiProperty::class];

foreach ($node->stmts as $k => $stmt) {
if (!$stmt instanceof Node\Stmt\Use_) {
break;
}

$useStatement = implode('\\', $stmt->uses[0]->name->parts);

if (LegacyApiProperty::class === $useStatement) {
unset($node->stmts[$k]);
continue;
}

if (false !== ($key = array_search($useStatement, $namespaces, true))) {
unset($namespaces[$key]);
}
}

foreach ($namespaces as $namespace) {
array_unshift($node->stmts, new Node\Stmt\Use_([
new Node\Stmt\UseUse(
new Node\Name(
$namespace
)
),
]));
}
}

if ($node instanceof Node\Stmt\Property || $node instanceof Node\Stmt\ClassMethod) {
if ($node instanceof Node\Stmt\Property) {
$reflection = $this->reflectionClass->getProperty($node->props[0]->name->__toString());
} else {
$reflection = $this->reflectionClass->getMethod($node->name->__toString());
}

[$propertyAnnotation, $isAnnotation] = $this->readApiProperty($reflection);

if ($propertyAnnotation) {
if ($isAnnotation) {
$this->removeAnnotation($node);
} else {
$this->removeAttribute($node);
}

$arguments = [];

foreach ([
'description',
'readable',
'writable',
'readableLink',
'writableLink',
'required',
'iri',
'identifier',
'default',
'example',
'types',
'builtinTypes',
] as $key) {
if (null === ($value = $propertyAnnotation->{$key}) || (\in_array($key, ['types', 'builtinTypes'], true) && [] === $value)) {
continue;
}

if ('iri' === $key) {
$arguments['iris'] = new Node\Expr\Array_([new Node\Expr\ArrayItem(
new Node\Scalar\String_($value)
)], ['kind' => Node\Expr\Array_::KIND_SHORT]);
continue;
}

$arguments[$key] = $this->valueToNode($value);
}

foreach ($propertyAnnotation->attributes ?? [] as $key => $value) {
if (null === $value) {
continue;
}

[$key, $value] = $this->getKeyValue($key, $value);
$arguments[$key] = $this->valueToNode($value);
}

array_unshift($node->attrGroups, new Node\AttributeGroup([
new Node\Attribute(
new Node\Name('ApiProperty'),
$this->arrayToArguments($arguments)
),
]));
}
}
}

/**
* @return array<ApiProperty, bool>|null
*/
private function readApiProperty(\ReflectionProperty|\ReflectionMethod $reflection): ?array
{
if (\PHP_VERSION_ID >= 80000 && $attributes = $reflection->getAttributes(LegacyApiProperty::class)) {
return [$attributes[0]->newInstance(), false];
}

if (null === $this->reader) {
throw new \RuntimeException(sprintf('Resource "%s" not found.', $reflection->getDeclaringClass()->getName()));
}

if ($reflection instanceof \ReflectionMethod) {
$annotation = $this->reader->getMethodAnnotation($reflection, LegacyApiProperty::class);
} else {
$annotation = $this->reader->getPropertyAnnotation($reflection, LegacyApiProperty::class);
}

if ($annotation) {
return [$annotation, true];
}

return null;
}

private function removeAttribute(Node\Stmt\Property|Node\Stmt\ClassMethod $node)
{
foreach ($node->attrGroups as $k => $attrGroupNode) {
foreach ($attrGroupNode->attrs as $i => $attribute) {
if (str_ends_with(implode('\\', $attribute->name->parts), 'ApiProperty')) {
unset($node->attrGroups[$k]);
break;
}
}
}
}

private function removeAnnotation(Node\Stmt\Property|Node\Stmt\ClassMethod $node)
{
$comment = $node->getDocComment();

if (preg_match('/@ApiProperty/', $comment->getText())) {
$node->setDocComment($this->removeAnnotationByTag($comment, 'ApiProperty'));
}
}

/**
* @return Node\Arg[]
*/
private function arrayToArguments(array $arguments)
{
$args = [];
foreach ($arguments as $key => $value) {
$args[] = new Node\Arg($value, false, false, [], new Node\Identifier($key));
}

return $args;
}

private function valueToNode(mixed $value)
{
if (\is_string($value)) {
if (class_exists($value)) {
return new Node\Expr\ClassConstFetch(new Node\Name($this->getShortName($value)), 'class');
}

return new Node\Scalar\String_($value);
}

if (\is_bool($value)) {
return new Node\Expr\ConstFetch(new Node\Name($value ? 'true' : 'false'));
}

if (is_numeric($value)) {
return \is_int($value) ? new Node\Scalar\LNumber($value) : new Node\Scalar\DNumber($value);
}

if (\is_array($value)) {
return new Node\Expr\Array_(
array_map(function ($key, $value) {
return new Node\Expr\ArrayItem(
$this->valueToNode($value),
\is_string($key) ? $this->valueToNode($key) : null,
);
}, array_keys($value), array_values($value)),
[
'kind' => Node\Expr\Array_::KIND_SHORT,
]
);
}
}

private function getShortName(string $class): string
{
if (false !== $pos = strrpos($class, '\\')) {
return substr($class, $pos + 1);
}

return $class;
}
}
2 changes: 1 addition & 1 deletion src/Core/Upgrade/UpgradeApiResourceVisitor.php
Expand Up @@ -39,7 +39,7 @@ final class UpgradeApiResourceVisitor extends NodeVisitorAbstract

private LegacyApiResource $resourceAnnotation;
private IdentifiersExtractorInterface $identifiersExtractor;
private bool $isAnnotation = false;
private bool $isAnnotation;
private string $resourceClass;

public function __construct(LegacyApiResource $resourceAnnotation, bool $isAnnotation, IdentifiersExtractorInterface $identifiersExtractor, string $resourceClass)
Expand Down
Expand Up @@ -91,11 +91,13 @@ public function testDebugResource()

$expectedStrings = [
'-use ApiPlatform\\Core\\Annotation\\ApiSubresource',
'-use ApiPlatform\\Core\\Annotation\\ApiProperty',
'-use ApiPlatform\\Core\\Annotation\\ApiResource',
'-use ApiPlatform\\Core\\Annotation\\ApiFilter',
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\SearchFilter;',
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\ExistsFilter;',
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\DateFilter;',
'+use ApiPlatform\\Metadata\\ApiProperty',
'+use ApiPlatform\\Metadata\\ApiResource',
'+use ApiPlatform\\Metadata\\ApiFilter',
'+use ApiPlatform\\Doctrine\\Orm\\Filter\\SearchFilter',
Expand All @@ -108,6 +110,9 @@ public function testDebugResource()
'+ #[ApiFilter(filterClass: SearchFilter::class)]',
'+ #[ApiFilter(filterClass: ExistsFilter::class)]',
'+ #[ApiFilter(filterClass: DateFilter::class)]',
'+ #[ApiProperty(writable: false)]',
"+ #[ApiProperty(iris: ['RelatedDummy.name'])]",
"+ #[ApiProperty(deprecationReason: 'This property is deprecated for upgrade test')]",
];

$display = $commandTester->getDisplay();
Expand Down
2 changes: 2 additions & 0 deletions tests/Fixtures/TestBundle/Entity/RelatedDummy.php
Expand Up @@ -50,12 +50,14 @@ class RelatedDummy extends ParentDummy
/**
* @var string|null A name
*
* @ApiProperty(iri="RelatedDummy.name")
* @ORM\Column(nullable=true)
* @Groups({"friends"})
*/
public $name;

/**
* @ApiProperty(attributes={"deprecation_reason"="This property is deprecated for upgrade test"})
* @ORM\Column
* @Groups({"barcelona", "chicago", "friends"})
* @ApiFilter(SearchFilter::class)
Expand Down

0 comments on commit f2c8a61

Please sign in to comment.