Skip to content

Commit f2c8a61

Browse files
Merge pull request #4842 from vincentchalamon/feat/upgrade-api-property-visitor
Migrate ApiProperty to new format
2 parents cbb96f6 + e5d1c13 commit f2c8a61

File tree

6 files changed

+250
-1
lines changed

6 files changed

+250
-1
lines changed

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ parameters:
222222
- src/Core/Test/DoctrineMongoDbOdmFilterTestCase.php
223223
- src/Core/Test/DoctrineOrmFilterTestCase.php
224224
- src/Core/Upgrade/SubresourceTransformer.php
225+
- src/Core/Upgrade/UpgradeApiPropertyVisitor.php
225226
- src/Core/Upgrade/UpgradeApiResourceVisitor.php
226227
- src/Core/Upgrade/UpgradeApiSubresourceVisitor.php
227228
- src/Core/Upgrade/UpgradeApiFilterVisitor.php

src/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommand.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Core\Upgrade\ColorConsoleDiffFormatter;
2121
use ApiPlatform\Core\Upgrade\SubresourceTransformer;
2222
use ApiPlatform\Core\Upgrade\UpgradeApiFilterVisitor;
23+
use ApiPlatform\Core\Upgrade\UpgradeApiPropertyVisitor;
2324
use ApiPlatform\Core\Upgrade\UpgradeApiResourceVisitor;
2425
use ApiPlatform\Core\Upgrade\UpgradeApiSubresourceVisitor;
2526
use ApiPlatform\Exception\ResourceClassNotFoundException;
@@ -109,6 +110,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
109110
[$attribute, $isAnnotation] = $this->readApiResource($resourceClass);
110111

111112
$traverser->addVisitor(new UpgradeApiFilterVisitor($this->reader, $resourceClass));
113+
$traverser->addVisitor(new UpgradeApiPropertyVisitor($this->reader, $resourceClass));
112114

113115
if (!$attribute) {
114116
continue;
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Upgrade;
15+
16+
use ApiPlatform\Core\Annotation\ApiProperty as LegacyApiProperty;
17+
use ApiPlatform\Metadata\ApiProperty;
18+
use ApiPlatform\Metadata\Resource\DeprecationMetadataTrait;
19+
use Doctrine\Common\Annotations\AnnotationReader;
20+
use PhpParser\Node;
21+
use PhpParser\NodeVisitorAbstract;
22+
23+
final class UpgradeApiPropertyVisitor extends NodeVisitorAbstract
24+
{
25+
use DeprecationMetadataTrait;
26+
use RemoveAnnotationTrait;
27+
28+
private ?AnnotationReader $reader;
29+
private \ReflectionClass $reflectionClass;
30+
31+
public function __construct(?AnnotationReader $reader, string $resourceClass)
32+
{
33+
$this->reader = $reader;
34+
$this->reflectionClass = new \ReflectionClass($resourceClass);
35+
}
36+
37+
/**
38+
* @return int|Node|null
39+
*/
40+
public function enterNode(Node $node)
41+
{
42+
if ($node instanceof Node\Stmt\Namespace_) {
43+
$namespaces = [ApiProperty::class];
44+
45+
foreach ($node->stmts as $k => $stmt) {
46+
if (!$stmt instanceof Node\Stmt\Use_) {
47+
break;
48+
}
49+
50+
$useStatement = implode('\\', $stmt->uses[0]->name->parts);
51+
52+
if (LegacyApiProperty::class === $useStatement) {
53+
unset($node->stmts[$k]);
54+
continue;
55+
}
56+
57+
if (false !== ($key = array_search($useStatement, $namespaces, true))) {
58+
unset($namespaces[$key]);
59+
}
60+
}
61+
62+
foreach ($namespaces as $namespace) {
63+
array_unshift($node->stmts, new Node\Stmt\Use_([
64+
new Node\Stmt\UseUse(
65+
new Node\Name(
66+
$namespace
67+
)
68+
),
69+
]));
70+
}
71+
}
72+
73+
if ($node instanceof Node\Stmt\Property || $node instanceof Node\Stmt\ClassMethod) {
74+
if ($node instanceof Node\Stmt\Property) {
75+
$reflection = $this->reflectionClass->getProperty($node->props[0]->name->__toString());
76+
} else {
77+
$reflection = $this->reflectionClass->getMethod($node->name->__toString());
78+
}
79+
80+
[$propertyAnnotation, $isAnnotation] = $this->readApiProperty($reflection);
81+
82+
if ($propertyAnnotation) {
83+
if ($isAnnotation) {
84+
$this->removeAnnotation($node);
85+
} else {
86+
$this->removeAttribute($node);
87+
}
88+
89+
$arguments = [];
90+
91+
foreach ([
92+
'description',
93+
'readable',
94+
'writable',
95+
'readableLink',
96+
'writableLink',
97+
'required',
98+
'iri',
99+
'identifier',
100+
'default',
101+
'example',
102+
'types',
103+
'builtinTypes',
104+
] as $key) {
105+
if (null === ($value = $propertyAnnotation->{$key}) || (\in_array($key, ['types', 'builtinTypes'], true) && [] === $value)) {
106+
continue;
107+
}
108+
109+
if ('iri' === $key) {
110+
$arguments['iris'] = new Node\Expr\Array_([new Node\Expr\ArrayItem(
111+
new Node\Scalar\String_($value)
112+
)], ['kind' => Node\Expr\Array_::KIND_SHORT]);
113+
continue;
114+
}
115+
116+
$arguments[$key] = $this->valueToNode($value);
117+
}
118+
119+
foreach ($propertyAnnotation->attributes ?? [] as $key => $value) {
120+
if (null === $value) {
121+
continue;
122+
}
123+
124+
[$key, $value] = $this->getKeyValue($key, $value);
125+
$arguments[$key] = $this->valueToNode($value);
126+
}
127+
128+
array_unshift($node->attrGroups, new Node\AttributeGroup([
129+
new Node\Attribute(
130+
new Node\Name('ApiProperty'),
131+
$this->arrayToArguments($arguments)
132+
),
133+
]));
134+
}
135+
}
136+
}
137+
138+
/**
139+
* @return array<ApiProperty, bool>|null
140+
*/
141+
private function readApiProperty(\ReflectionProperty|\ReflectionMethod $reflection): ?array
142+
{
143+
if (\PHP_VERSION_ID >= 80000 && $attributes = $reflection->getAttributes(LegacyApiProperty::class)) {
144+
return [$attributes[0]->newInstance(), false];
145+
}
146+
147+
if (null === $this->reader) {
148+
throw new \RuntimeException(sprintf('Resource "%s" not found.', $reflection->getDeclaringClass()->getName()));
149+
}
150+
151+
if ($reflection instanceof \ReflectionMethod) {
152+
$annotation = $this->reader->getMethodAnnotation($reflection, LegacyApiProperty::class);
153+
} else {
154+
$annotation = $this->reader->getPropertyAnnotation($reflection, LegacyApiProperty::class);
155+
}
156+
157+
if ($annotation) {
158+
return [$annotation, true];
159+
}
160+
161+
return null;
162+
}
163+
164+
private function removeAttribute(Node\Stmt\Property|Node\Stmt\ClassMethod $node)
165+
{
166+
foreach ($node->attrGroups as $k => $attrGroupNode) {
167+
foreach ($attrGroupNode->attrs as $i => $attribute) {
168+
if (str_ends_with(implode('\\', $attribute->name->parts), 'ApiProperty')) {
169+
unset($node->attrGroups[$k]);
170+
break;
171+
}
172+
}
173+
}
174+
}
175+
176+
private function removeAnnotation(Node\Stmt\Property|Node\Stmt\ClassMethod $node)
177+
{
178+
$comment = $node->getDocComment();
179+
180+
if (preg_match('/@ApiProperty/', $comment->getText())) {
181+
$node->setDocComment($this->removeAnnotationByTag($comment, 'ApiProperty'));
182+
}
183+
}
184+
185+
/**
186+
* @return Node\Arg[]
187+
*/
188+
private function arrayToArguments(array $arguments)
189+
{
190+
$args = [];
191+
foreach ($arguments as $key => $value) {
192+
$args[] = new Node\Arg($value, false, false, [], new Node\Identifier($key));
193+
}
194+
195+
return $args;
196+
}
197+
198+
private function valueToNode(mixed $value)
199+
{
200+
if (\is_string($value)) {
201+
if (class_exists($value)) {
202+
return new Node\Expr\ClassConstFetch(new Node\Name($this->getShortName($value)), 'class');
203+
}
204+
205+
return new Node\Scalar\String_($value);
206+
}
207+
208+
if (\is_bool($value)) {
209+
return new Node\Expr\ConstFetch(new Node\Name($value ? 'true' : 'false'));
210+
}
211+
212+
if (is_numeric($value)) {
213+
return \is_int($value) ? new Node\Scalar\LNumber($value) : new Node\Scalar\DNumber($value);
214+
}
215+
216+
if (\is_array($value)) {
217+
return new Node\Expr\Array_(
218+
array_map(function ($key, $value) {
219+
return new Node\Expr\ArrayItem(
220+
$this->valueToNode($value),
221+
\is_string($key) ? $this->valueToNode($key) : null,
222+
);
223+
}, array_keys($value), array_values($value)),
224+
[
225+
'kind' => Node\Expr\Array_::KIND_SHORT,
226+
]
227+
);
228+
}
229+
}
230+
231+
private function getShortName(string $class): string
232+
{
233+
if (false !== $pos = strrpos($class, '\\')) {
234+
return substr($class, $pos + 1);
235+
}
236+
237+
return $class;
238+
}
239+
}

src/Core/Upgrade/UpgradeApiResourceVisitor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ final class UpgradeApiResourceVisitor extends NodeVisitorAbstract
3939

4040
private LegacyApiResource $resourceAnnotation;
4141
private IdentifiersExtractorInterface $identifiersExtractor;
42-
private bool $isAnnotation = false;
42+
private bool $isAnnotation;
4343
private string $resourceClass;
4444

4545
public function __construct(LegacyApiResource $resourceAnnotation, bool $isAnnotation, IdentifiersExtractorInterface $identifiersExtractor, string $resourceClass)

tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,13 @@ public function testDebugResource()
9191

9292
$expectedStrings = [
9393
'-use ApiPlatform\\Core\\Annotation\\ApiSubresource',
94+
'-use ApiPlatform\\Core\\Annotation\\ApiProperty',
9495
'-use ApiPlatform\\Core\\Annotation\\ApiResource',
9596
'-use ApiPlatform\\Core\\Annotation\\ApiFilter',
9697
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\SearchFilter;',
9798
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\ExistsFilter;',
9899
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\DateFilter;',
100+
'+use ApiPlatform\\Metadata\\ApiProperty',
99101
'+use ApiPlatform\\Metadata\\ApiResource',
100102
'+use ApiPlatform\\Metadata\\ApiFilter',
101103
'+use ApiPlatform\\Doctrine\\Orm\\Filter\\SearchFilter',
@@ -108,6 +110,9 @@ public function testDebugResource()
108110
'+ #[ApiFilter(filterClass: SearchFilter::class)]',
109111
'+ #[ApiFilter(filterClass: ExistsFilter::class)]',
110112
'+ #[ApiFilter(filterClass: DateFilter::class)]',
113+
'+ #[ApiProperty(writable: false)]',
114+
"+ #[ApiProperty(iris: ['RelatedDummy.name'])]",
115+
"+ #[ApiProperty(deprecationReason: 'This property is deprecated for upgrade test')]",
111116
];
112117

113118
$display = $commandTester->getDisplay();

tests/Fixtures/TestBundle/Entity/RelatedDummy.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ class RelatedDummy extends ParentDummy
5050
/**
5151
* @var string|null A name
5252
*
53+
* @ApiProperty(iri="RelatedDummy.name")
5354
* @ORM\Column(nullable=true)
5455
* @Groups({"friends"})
5556
*/
5657
public $name;
5758

5859
/**
60+
* @ApiProperty(attributes={"deprecation_reason"="This property is deprecated for upgrade test"})
5961
* @ORM\Column
6062
* @Groups({"barcelona", "chicago", "friends"})
6163
* @ApiFilter(SearchFilter::class)

0 commit comments

Comments
 (0)