Skip to content

Commit 1b64ebf

Browse files
authored
fix: upgrade command remove ApiSubresource attribute (#5049)
Fixes #5038
1 parent 9c19fa1 commit 1b64ebf

File tree

5 files changed

+273
-64
lines changed

5 files changed

+273
-64
lines changed

src/Core/Upgrade/UpgradeApiResourceVisitor.php

+13-6
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,8 @@ public function enterNode(Node $node)
125125
}
126126

127127
if ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Interface_) {
128-
if ($this->isAnnotation) {
129-
$this->removeAnnotation($node);
130-
} else {
131-
$this->removeAttribute($node);
132-
}
128+
$this->removeAnnotation($node);
129+
$this->removeAttribute($node);
133130

134131
$arguments = [];
135132
$operations = null === $this->resourceAnnotation->itemOperations && null === $this->resourceAnnotation->collectionOperations ? null : array_merge(
@@ -361,13 +358,23 @@ private function removeAttribute(Node\Stmt\Class_|Node\Stmt\Interface_ $node)
361358
}
362359
}
363360
}
361+
foreach ($node->stmts as $k => $stmts) {
362+
foreach ($stmts->attrGroups as $i => $attrGroups) {
363+
foreach ($attrGroups->attrs as $j => $attrs) {
364+
if (str_ends_with(implode('\\', $attrs->name->parts), 'ApiSubresource')) {
365+
unset($node->stmts[$k]->attrGroups[$i]);
366+
break;
367+
}
368+
}
369+
}
370+
}
364371
}
365372

366373
private function removeAnnotation(Node\Stmt\Class_|Node\Stmt\Interface_ $node)
367374
{
368375
$comment = $node->getDocComment();
369376

370-
if (preg_match('/@ApiResource/', $comment->getText())) {
377+
if ($comment && preg_match('/@ApiResource/', $comment->getText())) {
371378
$node->setDocComment($this->removeAnnotationByTag($comment, 'ApiResource'));
372379
}
373380
}

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

+101-58
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
use ApiPlatform\Core\Upgrade\SubresourceTransformer;
2323
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
2424
use ApiPlatform\Metadata\Resource\ResourceNameCollection;
25-
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
25+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyToUpgradeWithOnlyAnnotation;
26+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyToUpgradeWithOnlyAttribute;
2627
use Doctrine\Common\Annotations\AnnotationReader;
2728
use PHPUnit\Framework\TestCase;
2829
use Symfony\Component\Console\Application;
@@ -49,75 +50,117 @@ private function getCommandTester(ResourceNameCollectionFactoryInterface $resour
4950

5051
/**
5152
* @requires PHP 8.1
53+
*
54+
* @dataProvider debugResourceProvider
5255
*/
53-
public function testDebugResource()
56+
public function testDebugResource(string $entityClass, array $subresourceOperationFactoryReturn, array $expectedStrings)
5457
{
5558
$resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
56-
$resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([RelatedDummy::class]));
59+
$resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([$entityClass]));
5760
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
58-
$resourceMetadataFactoryProphecy->create(RelatedDummy::class)->willReturn(new ResourceMetadata());
61+
$resourceMetadataFactoryProphecy->create($entityClass)->willReturn(new ResourceMetadata());
5962
$subresourceOperationFactoryProphecy = $this->prophesize(SubresourceOperationFactoryInterface::class);
60-
$subresourceOperationFactoryProphecy->create(RelatedDummy::class)->willReturn([[
61-
'property' => 'id',
62-
'collection' => false,
63-
'resource_class' => RelatedDummy::class,
64-
'shortNames' => [
65-
'RelatedDummy',
66-
],
67-
'legacy_filters' => [
68-
'related_dummy.friends',
69-
'related_dummy.complex_sub_query',
70-
],
71-
'legacy_normalization_context' => [
72-
'groups' => [
73-
'friends',
74-
],
75-
],
76-
'legacy_type' => 'https://schema.org/Product',
77-
'identifiers' => [
78-
'id' => [
79-
RelatedDummy::class,
80-
'id',
81-
true,
82-
],
83-
],
84-
'operation_name' => 'id_get_subresource',
85-
'route_name' => 'api_related_dummies_id_get_subresource',
86-
'path' => '/related_dummies/{id}/id.{_format}',
87-
]]);
63+
$subresourceOperationFactoryProphecy->create($entityClass)->willReturn($subresourceOperationFactoryReturn);
8864

8965
$commandTester = $this->getCommandTester($resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $subresourceOperationFactoryProphecy->reveal());
9066
$commandTester->execute([]);
9167

92-
$expectedStrings = [
93-
'-use ApiPlatform\\Core\\Annotation\\ApiSubresource',
94-
'-use ApiPlatform\\Core\\Annotation\\ApiProperty',
95-
'-use ApiPlatform\\Core\\Annotation\\ApiResource',
96-
'-use ApiPlatform\\Core\\Annotation\\ApiFilter',
97-
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\SearchFilter;',
98-
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\ExistsFilter;',
99-
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\DateFilter;',
100-
'+use ApiPlatform\\Metadata\\ApiProperty',
101-
'+use ApiPlatform\\Metadata\\ApiResource',
102-
'+use ApiPlatform\\Metadata\\ApiFilter',
103-
'+use ApiPlatform\\Doctrine\\Orm\\Filter\\SearchFilter',
104-
'+use ApiPlatform\\Doctrine\\Orm\\Filter\\ExistsFilter',
105-
'+use ApiPlatform\\Doctrine\\Orm\\Filter\\DateFilter',
106-
'+use ApiPlatform\\Metadata\\Get',
107-
"+#[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'])]",
108-
"#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])]",
109-
"+#[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])]",
110-
'+ #[ApiFilter(filterClass: SearchFilter::class)]',
111-
'+ #[ApiFilter(filterClass: ExistsFilter::class)]',
112-
'+ #[ApiFilter(filterClass: DateFilter::class)]',
113-
'+ #[ApiProperty(writable: false)]',
114-
"+ #[ApiProperty(iris: ['RelatedDummy.name'])]",
115-
"+ #[ApiProperty(deprecationReason: 'This property is deprecated for upgrade test')]",
116-
];
117-
11868
$display = $commandTester->getDisplay();
11969
foreach ($expectedStrings as $expectedString) {
12070
$this->assertStringContainsString($expectedString, $display);
12171
}
12272
}
73+
74+
public function debugResourceProvider(): array
75+
{
76+
$entityClasses = [
77+
'only_annotation' => DummyToUpgradeWithOnlyAnnotation::class,
78+
'only_attribute' => DummyToUpgradeWithOnlyAttribute::class,
79+
];
80+
81+
return array_map(function ($key, $entityClass) {
82+
$expectedStrings = [
83+
'+#[ApiResource]',
84+
'-use ApiPlatform\\Core\\Annotation\\ApiSubresource',
85+
'-use ApiPlatform\\Core\\Annotation\\ApiProperty',
86+
'-use ApiPlatform\\Core\\Annotation\\ApiResource',
87+
'+use ApiPlatform\\Metadata\\ApiProperty',
88+
'+use ApiPlatform\\Metadata\\ApiResource',
89+
'+use ApiPlatform\\Metadata\\ApiFilter',
90+
'+use ApiPlatform\\Metadata\\Get',
91+
sprintf("#[ApiResource(uriTemplate: '/%s/{id}/name.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])]", $key),
92+
];
93+
94+
if (DummyToUpgradeWithOnlyAnnotation::class === $entityClass) {
95+
array_push($expectedStrings,
96+
'+use ApiPlatform\\Doctrine\\Orm\\Filter\\SearchFilter',
97+
'+use ApiPlatform\\Doctrine\\Orm\\Filter\\ExistsFilter',
98+
'+use ApiPlatform\\Doctrine\\Orm\\Filter\\DateFilter',
99+
'-use ApiPlatform\\Core\\Annotation\\ApiFilter',
100+
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\SearchFilter;',
101+
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\ExistsFilter;',
102+
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\DateFilter;',
103+
'- * @ApiResource',
104+
'- * @ApiFilter(SearchFilter::class, properties={"id"})',
105+
"+#[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])]",
106+
'- * @ApiProperty(writable=false)',
107+
'+ #[ApiProperty(writable: false)]',
108+
'- * @ApiSubresource',
109+
'- * @ApiFilter(DateFilter::class)',
110+
'- * @ApiProperty(iri="DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct")',
111+
"+ #[ApiProperty(iris: ['DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct'])]",
112+
'- * @ApiFilter(SearchFilter::class)',
113+
'- * @ApiFilter(ExistsFilter::class)',
114+
'+ #[ApiFilter(filterClass: SearchFilter::class)]',
115+
'+ #[ApiFilter(filterClass: ExistsFilter::class)]',
116+
'+ #[ApiFilter(filterClass: DateFilter::class)]'
117+
);
118+
}
119+
120+
if (DummyToUpgradeWithOnlyAttribute::class === $entityClass) {
121+
array_push($expectedStrings,
122+
'-#[ApiResource()]',
123+
"+#[ApiResource(uriTemplate: '/only_attribute/{id}/name.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])]",
124+
'- #[ApiSubresource]',
125+
"- #[ApiProperty(iri: 'DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct')]",
126+
"+ #[ApiProperty(iris: ['DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct'])]"
127+
);
128+
}
129+
130+
return [
131+
$entityClass,
132+
[
133+
[
134+
'property' => 'id',
135+
'collection' => false,
136+
'resource_class' => $entityClass,
137+
'shortNames' => [
138+
substr($entityClass, (\strlen($entityClass) - strrpos($entityClass, '\\') - 1) * (-1)),
139+
],
140+
'legacy_filters' => [
141+
'related_dummy.friends',
142+
'related_dummy.complex_sub_query',
143+
],
144+
'legacy_normalization_context' => [
145+
'groups' => [
146+
'friends',
147+
],
148+
],
149+
'legacy_type' => 'https://schema.org/Product',
150+
'identifiers' => [
151+
'id' => [
152+
$entityClass,
153+
'id',
154+
true,
155+
],
156+
],
157+
'operation_name' => 'name_get_subresource',
158+
'route_name' => sprintf('api_%s_name_get_subresource', $key),
159+
'path' => sprintf('/%s/{id}/name.{_format}', $key),
160+
],
161+
],
162+
array_merge($expectedStrings),
163+
];
164+
}, array_keys($entityClasses), array_values($entityClasses));
165+
}
123166
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiResource;
17+
use Doctrine\Common\Collections\Collection;
18+
use Doctrine\ORM\Mapping as ORM;
19+
20+
/**
21+
* @ORM\Entity
22+
*
23+
* @ApiResource
24+
*/
25+
class DummyToUpgradeProduct
26+
{
27+
/**
28+
* @var int
29+
*
30+
* @ORM\Id
31+
* @ORM\GeneratedValue
32+
* @ORM\Column(type="integer")
33+
*/
34+
private $id;
35+
36+
/**
37+
* @var Collection<int,DummyToUpgradeWithOnlyAnnotation>
38+
*
39+
* @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAnnotation::class)
40+
*/
41+
private $dummysToUpgradeWithOnlyAnnotation;
42+
43+
/**
44+
* @var Collection<int,DummyToUpgradeWithOnlyAttribute>
45+
*
46+
* @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAttribute::class)
47+
*/
48+
private $dummysToUpgradeWithOnlyAttribute;
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiFilter;
17+
use ApiPlatform\Core\Annotation\ApiProperty;
18+
use ApiPlatform\Core\Annotation\ApiResource;
19+
use ApiPlatform\Core\Annotation\ApiSubresource;
20+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
21+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter;
22+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
23+
use Doctrine\ORM\Mapping as ORM;
24+
use Symfony\Component\Serializer\Annotation\Groups;
25+
26+
/**
27+
* @ORM\Entity
28+
*
29+
* @ApiResource
30+
*
31+
* @ApiFilter(SearchFilter::class, properties={"id"})
32+
*/
33+
class DummyToUpgradeWithOnlyAnnotation
34+
{
35+
/**
36+
* @var int
37+
*
38+
* @ORM\Id
39+
* @ORM\GeneratedValue
40+
* @ORM\Column(type="integer")
41+
* @Groups({"chicago", "friends"})
42+
* @ApiProperty(writable=false)
43+
* @ApiFilter(DateFilter::class)
44+
*/
45+
private $id;
46+
47+
/**
48+
* @var DummyToUpgradeProduct
49+
*
50+
* @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", cascade={"persist"}, inversedBy="dummysToUpgradeWithOnlyAnnotation")
51+
* @ORM\JoinColumn(nullable=false)
52+
* @Groups({"barcelona", "chicago", "friends"})
53+
*
54+
* @ApiSubresource
55+
*
56+
* @ApiProperty(iri="DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct")
57+
* @ApiFilter(SearchFilter::class)
58+
* @ApiFilter(ExistsFilter::class)
59+
*/
60+
private $dummyToUpgradeProduct;
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiProperty;
17+
use ApiPlatform\Core\Annotation\ApiResource;
18+
use ApiPlatform\Core\Annotation\ApiSubresource;
19+
use Doctrine\ORM\Mapping as ORM;
20+
use Symfony\Component\Serializer\Annotation\Groups;
21+
22+
/**
23+
* @ORM\Entity
24+
*/
25+
#[ApiResource()]
26+
class DummyToUpgradeWithOnlyAttribute
27+
{
28+
/**
29+
* @var int
30+
*
31+
* @ORM\Id
32+
* @ORM\GeneratedValue
33+
* @ORM\Column(type="integer")
34+
*/
35+
#[Groups(['chicago', 'friends'])]
36+
#[ApiProperty(writable: false)]
37+
private $id;
38+
39+
/**
40+
* @var DummyToUpgradeProduct
41+
*
42+
* @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", inversedBy="dummysToUpgradeWithOnlyAttribute")
43+
* @ORM\JoinColumn(nullable=false)
44+
*/
45+
#[Groups(['barcelona', 'chicago', 'friends'])]
46+
#[ApiSubresource]
47+
#[ApiProperty(iri: 'DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct')]
48+
private $dummyToUpgradeProduct;
49+
}

0 commit comments

Comments
 (0)