diff --git a/src/Core/Upgrade/UpgradeApiResourceVisitor.php b/src/Core/Upgrade/UpgradeApiResourceVisitor.php index e36d85e0916..ead9a207f01 100644 --- a/src/Core/Upgrade/UpgradeApiResourceVisitor.php +++ b/src/Core/Upgrade/UpgradeApiResourceVisitor.php @@ -125,11 +125,8 @@ public function enterNode(Node $node) } if ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Interface_) { - if ($this->isAnnotation) { - $this->removeAnnotation($node); - } else { - $this->removeAttribute($node); - } + $this->removeAnnotation($node); + $this->removeAttribute($node); $arguments = []; $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) } } } + foreach ($node->stmts as $k => $stmts) { + foreach ($stmts->attrGroups as $i => $attrGroups) { + foreach ($attrGroups->attrs as $j => $attrs) { + if (str_ends_with(implode('\\', $attrs->name->parts), 'ApiSubresource')) { + unset($node->stmts[$k]->attrGroups[$i]); + break; + } + } + } + } } private function removeAnnotation(Node\Stmt\Class_|Node\Stmt\Interface_ $node) { $comment = $node->getDocComment(); - if (preg_match('/@ApiResource/', $comment->getText())) { + if ($comment && preg_match('/@ApiResource/', $comment->getText())) { $node->setDocComment($this->removeAnnotationByTag($comment, 'ApiResource')); } } diff --git a/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php b/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php index baf9c3e103f..6e54aa91755 100644 --- a/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php +++ b/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php @@ -22,7 +22,8 @@ use ApiPlatform\Core\Upgrade\SubresourceTransformer; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceNameCollection; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyToUpgradeWithOnlyAnnotation; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyToUpgradeWithOnlyAttribute; use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; @@ -49,75 +50,117 @@ private function getCommandTester(ResourceNameCollectionFactoryInterface $resour /** * @requires PHP 8.1 + * + * @dataProvider debugResourceProvider */ - public function testDebugResource() + public function testDebugResource(string $entityClass, array $subresourceOperationFactoryReturn, array $expectedStrings) { $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); - $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([RelatedDummy::class])); + $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([$entityClass])); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(RelatedDummy::class)->willReturn(new ResourceMetadata()); + $resourceMetadataFactoryProphecy->create($entityClass)->willReturn(new ResourceMetadata()); $subresourceOperationFactoryProphecy = $this->prophesize(SubresourceOperationFactoryInterface::class); - $subresourceOperationFactoryProphecy->create(RelatedDummy::class)->willReturn([[ - 'property' => 'id', - 'collection' => false, - 'resource_class' => RelatedDummy::class, - 'shortNames' => [ - 'RelatedDummy', - ], - 'legacy_filters' => [ - 'related_dummy.friends', - 'related_dummy.complex_sub_query', - ], - 'legacy_normalization_context' => [ - 'groups' => [ - 'friends', - ], - ], - 'legacy_type' => 'https://schema.org/Product', - 'identifiers' => [ - 'id' => [ - RelatedDummy::class, - 'id', - true, - ], - ], - 'operation_name' => 'id_get_subresource', - 'route_name' => 'api_related_dummies_id_get_subresource', - 'path' => '/related_dummies/{id}/id.{_format}', - ]]); + $subresourceOperationFactoryProphecy->create($entityClass)->willReturn($subresourceOperationFactoryReturn); $commandTester = $this->getCommandTester($resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $subresourceOperationFactoryProphecy->reveal()); $commandTester->execute([]); - $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', - '+use ApiPlatform\\Doctrine\\Orm\\Filter\\ExistsFilter', - '+use ApiPlatform\\Doctrine\\Orm\\Filter\\DateFilter', - '+use ApiPlatform\\Metadata\\Get', - "+#[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'])]", - "#[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()])]", - "+#[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])]", - '+ #[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(); foreach ($expectedStrings as $expectedString) { $this->assertStringContainsString($expectedString, $display); } } + + public function debugResourceProvider(): array + { + $entityClasses = [ + 'only_annotation' => DummyToUpgradeWithOnlyAnnotation::class, + 'only_attribute' => DummyToUpgradeWithOnlyAttribute::class, + ]; + + return array_map(function ($key, $entityClass) { + $expectedStrings = [ + '+#[ApiResource]', + '-use ApiPlatform\\Core\\Annotation\\ApiSubresource', + '-use ApiPlatform\\Core\\Annotation\\ApiProperty', + '-use ApiPlatform\\Core\\Annotation\\ApiResource', + '+use ApiPlatform\\Metadata\\ApiProperty', + '+use ApiPlatform\\Metadata\\ApiResource', + '+use ApiPlatform\\Metadata\\ApiFilter', + '+use ApiPlatform\\Metadata\\Get', + 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), + ]; + + if (DummyToUpgradeWithOnlyAnnotation::class === $entityClass) { + array_push($expectedStrings, + '+use ApiPlatform\\Doctrine\\Orm\\Filter\\SearchFilter', + '+use ApiPlatform\\Doctrine\\Orm\\Filter\\ExistsFilter', + '+use ApiPlatform\\Doctrine\\Orm\\Filter\\DateFilter', + '-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;', + '- * @ApiResource', + '- * @ApiFilter(SearchFilter::class, properties={"id"})', + "+#[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])]", + '- * @ApiProperty(writable=false)', + '+ #[ApiProperty(writable: false)]', + '- * @ApiSubresource', + '- * @ApiFilter(DateFilter::class)', + '- * @ApiProperty(iri="DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct")', + "+ #[ApiProperty(iris: ['DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct'])]", + '- * @ApiFilter(SearchFilter::class)', + '- * @ApiFilter(ExistsFilter::class)', + '+ #[ApiFilter(filterClass: SearchFilter::class)]', + '+ #[ApiFilter(filterClass: ExistsFilter::class)]', + '+ #[ApiFilter(filterClass: DateFilter::class)]' + ); + } + + if (DummyToUpgradeWithOnlyAttribute::class === $entityClass) { + array_push($expectedStrings, + '-#[ApiResource()]', + "+#[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()])]", + '- #[ApiSubresource]', + "- #[ApiProperty(iri: 'DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct')]", + "+ #[ApiProperty(iris: ['DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct'])]" + ); + } + + return [ + $entityClass, + [ + [ + 'property' => 'id', + 'collection' => false, + 'resource_class' => $entityClass, + 'shortNames' => [ + substr($entityClass, (\strlen($entityClass) - strrpos($entityClass, '\\') - 1) * (-1)), + ], + 'legacy_filters' => [ + 'related_dummy.friends', + 'related_dummy.complex_sub_query', + ], + 'legacy_normalization_context' => [ + 'groups' => [ + 'friends', + ], + ], + 'legacy_type' => 'https://schema.org/Product', + 'identifiers' => [ + 'id' => [ + $entityClass, + 'id', + true, + ], + ], + 'operation_name' => 'name_get_subresource', + 'route_name' => sprintf('api_%s_name_get_subresource', $key), + 'path' => sprintf('/%s/{id}/name.{_format}', $key), + ], + ], + array_merge($expectedStrings), + ]; + }, array_keys($entityClasses), array_values($entityClasses)); + } } diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php new file mode 100644 index 00000000000..cabc7a4352c --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php @@ -0,0 +1,49 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + * + * @ApiResource + */ +class DummyToUpgradeProduct +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @var Collection + * + * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAnnotation::class) + */ + private $dummysToUpgradeWithOnlyAnnotation; + + /** + * @var Collection + * + * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAttribute::class) + */ + private $dummysToUpgradeWithOnlyAttribute; +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php new file mode 100644 index 00000000000..576ca2a55f0 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php @@ -0,0 +1,61 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiFilter; +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Annotation\ApiSubresource; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity + * + * @ApiResource + * + * @ApiFilter(SearchFilter::class, properties={"id"}) + */ +class DummyToUpgradeWithOnlyAnnotation +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * @Groups({"chicago", "friends"}) + * @ApiProperty(writable=false) + * @ApiFilter(DateFilter::class) + */ + private $id; + + /** + * @var DummyToUpgradeProduct + * + * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", cascade={"persist"}, inversedBy="dummysToUpgradeWithOnlyAnnotation") + * @ORM\JoinColumn(nullable=false) + * @Groups({"barcelona", "chicago", "friends"}) + * + * @ApiSubresource + * + * @ApiProperty(iri="DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct") + * @ApiFilter(SearchFilter::class) + * @ApiFilter(ExistsFilter::class) + */ + private $dummyToUpgradeProduct; +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php new file mode 100644 index 00000000000..f9a1fd1f592 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php @@ -0,0 +1,49 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Annotation\ApiSubresource; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity + */ +#[ApiResource()] +class DummyToUpgradeWithOnlyAttribute +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[Groups(['chicago', 'friends'])] + #[ApiProperty(writable: false)] + private $id; + + /** + * @var DummyToUpgradeProduct + * + * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", inversedBy="dummysToUpgradeWithOnlyAttribute") + * @ORM\JoinColumn(nullable=false) + */ + #[Groups(['barcelona', 'chicago', 'friends'])] + #[ApiSubresource] + #[ApiProperty(iri: 'DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct')] + private $dummyToUpgradeProduct; +}