diff --git a/features/hal/collection_uri_template.feature b/features/hal/collection_uri_template.feature index 0af300f6c66..a71c2d9329c 100644 --- a/features/hal/collection_uri_template.feature +++ b/features/hal/collection_uri_template.feature @@ -36,9 +36,23 @@ Feature: Exposing a property being a collection of resources "_links": { "self": { "href": "/property_collection_iri_only_relations/1" + }, + "children": { + "href": "/property_collection_iri_only_relations/1/children" } }, - "name": "asb" + "name": "asb1" + }, + { + "_links": { + "self": { + "href": "/property_collection_iri_only_relations/2" + }, + "children": { + "href": "/property_collection_iri_only_relations/2/children" + } + }, + "name": "asb2" } ], "iterableIri": [ @@ -46,6 +60,9 @@ Feature: Exposing a property being a collection of resources "_links": { "self": { "href": "/property_collection_iri_only_relations/9999" + }, + "children": { + "href": "/property_collection_iri_only_relations/9999/children" } }, "name": "Michel" diff --git a/features/jsonapi/collection_uri_template.feature b/features/jsonapi/collection_uri_template.feature index 188895d0e7a..6fa13c00732 100644 --- a/features/jsonapi/collection_uri_template.feature +++ b/features/jsonapi/collection_uri_template.feature @@ -33,6 +33,10 @@ Feature: Exposing a property being a collection of resources { "type": "PropertyCollectionIriOnlyRelation", "id": "/property_collection_iri_only_relations/1" + }, + { + "type": "PropertyCollectionIriOnlyRelation", + "id": "/property_collection_iri_only_relations/2" } ] }, diff --git a/src/Doctrine/EventListener/PurgeHttpCacheListener.php b/src/Doctrine/EventListener/PurgeHttpCacheListener.php index 7aba69f65f5..fa7b081e4dc 100644 --- a/src/Doctrine/EventListener/PurgeHttpCacheListener.php +++ b/src/Doctrine/EventListener/PurgeHttpCacheListener.php @@ -24,7 +24,6 @@ use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; -use Doctrine\Common\Util\ClassUtils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Event\PreUpdateEventArgs; @@ -60,7 +59,7 @@ public function preUpdate(PreUpdateEventArgs $eventArgs): void $changeSet = $eventArgs->getEntityChangeSet(); // @phpstan-ignore-next-line $objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); - $associationMappings = $objectManager->getClassMetadata(ClassUtils::getClass($eventArgs->getObject()))->getAssociationMappings(); + $associationMappings = $objectManager->getClassMetadata(\get_class($eventArgs->getObject()))->getAssociationMappings(); foreach ($changeSet as $key => $value) { if (!isset($associationMappings[$key])) { @@ -127,7 +126,7 @@ private function gatherResourceAndItemTags(object $entity, bool $purgeItem): voi private function gatherRelationTags(EntityManagerInterface $em, object $entity): void { - $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings(); + $associationMappings = $em->getClassMetadata($entity::class)->getAssociationMappings(); /** @var array|AssociationMapping $associationMapping according to the version of doctrine orm */ foreach ($associationMappings as $property => $associationMapping) { if ($associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) { diff --git a/src/GraphQl/State/Provider/ResolverProvider.php b/src/GraphQl/State/Provider/ResolverProvider.php index 05dcb785c3e..1b6f1fe77f3 100644 --- a/src/GraphQl/State/Provider/ResolverProvider.php +++ b/src/GraphQl/State/Provider/ResolverProvider.php @@ -69,7 +69,7 @@ private function getResourceClass(?object $item, ?string $resourceClass, string return $itemClass; } - if ($resourceClass !== $itemClass) { + if ($resourceClass !== $itemClass && !$item instanceof $resourceClass) { throw new \UnexpectedValueException(sprintf($errorMessage, (new \ReflectionClass($resourceClass))->getShortName(), (new \ReflectionClass($itemClass))->getShortName())); } diff --git a/src/GraphQl/Tests/State/Provider/ResolverProviderTest.php b/src/GraphQl/Tests/State/Provider/ResolverProviderTest.php index 75d8f698ac9..64d9e9b8220 100644 --- a/src/GraphQl/Tests/State/Provider/ResolverProviderTest.php +++ b/src/GraphQl/Tests/State/Provider/ResolverProviderTest.php @@ -15,6 +15,9 @@ use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; use ApiPlatform\GraphQl\State\Provider\ResolverProvider; +use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ChildFoo; +use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ParentFoo; +use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\State\ProviderInterface; use PHPUnit\Framework\TestCase; @@ -35,4 +38,18 @@ public function testProvide(): void $provider = new ResolverProvider($decorated, $resolverLocator); $this->assertEquals($res, $provider->provide($operation, [], $context)); } + + public function testProvideInheritedClass(): void + { + $res = new ChildFoo(); + $operation = new Query(class: ParentFoo::class, resolver: 'foo'); + $context = []; + $decorated = $this->createMock(ProviderInterface::class); + $resolverMock = $this->createMock(QueryItemResolverInterface::class); + $resolverMock->expects($this->once())->method('__invoke')->willReturn($res); + $resolverLocator = $this->createMock(ContainerInterface::class); + $resolverLocator->expects($this->once())->method('get')->with('foo')->willReturn($resolverMock); + $provider = new ResolverProvider($decorated, $resolverLocator); + $this->assertEquals($res, $provider->provide($operation, [], $context)); + } } diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php index ee2758f0bb5..549dc146341 100644 --- a/src/Hal/Serializer/ItemNormalizer.php +++ b/src/Hal/Serializer/ItemNormalizer.php @@ -186,6 +186,7 @@ private function getComponents(object $object, ?string $format, array $context): $relation['iri'] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext); $relation['operation'] = $operation; + $cacheKey = null; } if ($propertyMetadata->isReadableLink()) { @@ -202,7 +203,7 @@ private function getComponents(object $object, ?string $format, array $context): } } - if (false !== $context['cache_key']) { + if ($cacheKey && false !== $context['cache_key']) { $this->componentsCache[$cacheKey] = $components; } diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php index 3211518cc3f..410b59ca445 100644 --- a/src/Metadata/Extractor/XmlResourceExtractor.php +++ b/src/Metadata/Extractor/XmlResourceExtractor.php @@ -408,6 +408,7 @@ private function buildOperations(\SimpleXMLElement $resource, array $root): ?arr 'queryParameterValidate' => $this->phpize($operation, 'queryParameterValidate', 'bool'), 'priority' => $this->phpize($operation, 'priority', 'integer'), 'name' => $this->phpize($operation, 'name', 'string'), + 'routeName' => $this->phpize($operation, 'routeName', 'string'), ]); } diff --git a/src/Metadata/Extractor/schema/resources.xsd b/src/Metadata/Extractor/schema/resources.xsd index 6121622d758..6c460cf9aa8 100644 --- a/src/Metadata/Extractor/schema/resources.xsd +++ b/src/Metadata/Extractor/schema/resources.xsd @@ -48,6 +48,7 @@ + diff --git a/src/Metadata/Tests/Extractor/XmlExtractorTest.php b/src/Metadata/Tests/Extractor/XmlExtractorTest.php index be195c2b976..be785206c8e 100644 --- a/src/Metadata/Tests/Extractor/XmlExtractorTest.php +++ b/src/Metadata/Tests/Extractor/XmlExtractorTest.php @@ -278,6 +278,7 @@ public function testValidXML(): void 'links' => null, 'headers' => ['hello' => 'world'], 'parameters' => null, + 'routeName' => 'custom_route_name', ], [ 'name' => null, @@ -389,6 +390,7 @@ public function testValidXML(): void extraProperties: ['foo' => 'bar'] ), ], + 'routeName' => null, ], ], 'graphQlOperations' => null, diff --git a/src/Metadata/Tests/Extractor/xml/valid.xml b/src/Metadata/Tests/Extractor/xml/valid.xml index a725e652894..81606a1f409 100644 --- a/src/Metadata/Tests/Extractor/xml/valid.xml +++ b/src/Metadata/Tests/Extractor/xml/valid.xml @@ -99,7 +99,7 @@ - + diff --git a/tests/.ignored-deprecations b/tests/.ignored-deprecations index 6b4de3448a5..2774847dfcd 100644 --- a/tests/.ignored-deprecations +++ b/tests/.ignored-deprecations @@ -10,3 +10,5 @@ # Fixed when ApiPlatform\Api\FilterLocatorTrait will we deleted %ApiPlatform\\Api\\FilterInterface is deprecated in favor of ApiPlatform\\Metadata\\FilterInterface% + +%The "Symfony\\Bundle\\MakerBundle\\Maker\\MakeAuthenticator" class is deprecated, use any of the Security\\Make\* commands instead% diff --git a/tests/.ignored-deprecations-legacy-events b/tests/.ignored-deprecations-legacy-events index 74dea1488eb..5dcd9cdd7d5 100644 --- a/tests/.ignored-deprecations-legacy-events +++ b/tests/.ignored-deprecations-legacy-events @@ -23,3 +23,5 @@ # Fixed when ApiPlatform\Api\FilterLocatorTrait will we deleted %ApiPlatform\\Api\\FilterInterface is deprecated in favor of ApiPlatform\\Metadata\\FilterInterface% + +%The "Symfony\\Bundle\\MakerBundle\\Maker\\MakeAuthenticator" class is deprecated, use any of the Security\\Make\* commands instead% diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 8894e737d37..90c9d183dcf 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -2019,18 +2019,23 @@ public function thereAreIriOnlyDummies(int $nb): void */ public function thereAreResourcesWithPropertyUriTemplates(): void { - $propertyCollectionIriOnlyRelation = $this->isOrm() ? new PropertyCollectionIriOnlyRelation() : new PropertyCollectionIriOnlyRelationDocument(); - $propertyCollectionIriOnlyRelation->name = 'asb'; + $propertyCollectionIriOnlyRelation1 = $this->isOrm() ? new PropertyCollectionIriOnlyRelation() : new PropertyCollectionIriOnlyRelationDocument(); + $propertyCollectionIriOnlyRelation1->name = 'asb1'; + + $propertyCollectionIriOnlyRelation2 = $this->isOrm() ? new PropertyCollectionIriOnlyRelation() : new PropertyCollectionIriOnlyRelationDocument(); + $propertyCollectionIriOnlyRelation2->name = 'asb2'; $propertyToOneRelation = $this->isOrm() ? new PropertyUriTemplateOneToOneRelation() : new PropertyUriTemplateOneToOneRelationDocument(); $propertyToOneRelation->name = 'xarguš'; $propertyCollectionIriOnly = $this->isOrm() ? new PropertyCollectionIriOnly() : new PropertyCollectionIriOnlyDocument(); - $propertyCollectionIriOnly->addPropertyCollectionIriOnlyRelation($propertyCollectionIriOnlyRelation); + $propertyCollectionIriOnly->addPropertyCollectionIriOnlyRelation($propertyCollectionIriOnlyRelation1); + $propertyCollectionIriOnly->addPropertyCollectionIriOnlyRelation($propertyCollectionIriOnlyRelation2); $propertyCollectionIriOnly->setToOneRelation($propertyToOneRelation); $this->manager->persist($propertyCollectionIriOnly); - $this->manager->persist($propertyCollectionIriOnlyRelation); + $this->manager->persist($propertyCollectionIriOnlyRelation1); + $this->manager->persist($propertyCollectionIriOnlyRelation2); $this->manager->persist($propertyToOneRelation); $this->manager->flush(); } diff --git a/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelation.php b/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelation.php index a7fcbd46d8d..af90a375e5e 100644 --- a/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelation.php +++ b/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelation.php @@ -13,9 +13,12 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Post; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Serializer\Annotation\Groups; @@ -42,6 +45,16 @@ class PropertyCollectionIriOnlyRelation #[ODM\ReferenceOne(targetDocument: PropertyCollectionIriOnly::class)] private ?PropertyCollectionIriOnly $propertyCollectionIriOnly = null; + #[ODM\ReferenceMany(targetDocument: PropertyCollectionIriOnlyRelationSecondLevel::class)] + #[ApiProperty(uriTemplate: '/property_collection_iri_only_relations/{parentId}/children')] + #[Groups('read')] + private Collection $children; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + public function getId(): ?int { return $this->id ?? 9999; @@ -56,4 +69,34 @@ public function setPropertyCollectionIriOnly(?PropertyCollectionIriOnly $propert { $this->propertyCollectionIriOnly = $propertyCollectionIriOnly; } + + /** + * @return Collection + */ + public function getChildren(): Collection + { + return $this->children; + } + + public function addChild(PropertyCollectionIriOnlyRelationSecondLevel $child): self + { + if (!$this->children->contains($child)) { + $this->children->add($child); + $child->setParent($this); + } + + return $this; + } + + public function removeChild(PropertyCollectionIriOnlyRelationSecondLevel $child): self + { + if ($this->children->removeElement($child)) { + // set the owning side to null (unless already changed) + if ($child->getParent() === $this) { + $child->setParent(null); + } + } + + return $this; + } } diff --git a/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelationSecondLevel.php b/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelationSecondLevel.php new file mode 100644 index 00000000000..b190aa0a94e --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelationSecondLevel.php @@ -0,0 +1,54 @@ + + * + * 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\Document; + +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Post; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +#[ + Post, + GetCollection(uriTemplate: '/property-collection-relation-second-levels'), + GetCollection( + uriTemplate: '/property_collection_iri_only_relations/{parentId}/children', + uriVariables: [ + 'parentId' => new Link(toProperty: 'parent', fromClass: PropertyCollectionIriOnlyRelation::class), + ] + ) +] +#[ODM\Document] +class PropertyCollectionIriOnlyRelationSecondLevel +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + private ?int $id = null; + + #[ODM\ReferenceOne(targetDocument: PropertyCollectionIriOnlyRelation::class)] + private ?PropertyCollectionIriOnlyRelation $parent = null; + + public function getId(): ?int + { + return $this->id ?? 9999; + } + + public function getParent(): ?PropertyCollectionIriOnlyRelation + { + return $this->parent; + } + + public function setParent(?PropertyCollectionIriOnlyRelation $parent): void + { + $this->parent = $parent; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelation.php b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelation.php index ebc12163c3b..4eae5c6a749 100644 --- a/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelation.php +++ b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelation.php @@ -13,9 +13,12 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Post; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints\NotBlank; @@ -49,6 +52,16 @@ class PropertyCollectionIriOnlyRelation #[ORM\ManyToOne(inversedBy: 'propertyCollectionIriOnlyRelation')] private ?PropertyCollectionIriOnly $propertyCollectionIriOnly = null; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: PropertyCollectionIriOnlyRelationSecondLevel::class)] + #[ApiProperty(uriTemplate: '/property_collection_iri_only_relations/{parentId}/children')] + #[Groups('read')] + private Collection $children; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + public function getId(): ?int { return $this->id ?? 9999; @@ -63,4 +76,34 @@ public function setPropertyCollectionIriOnly(?PropertyCollectionIriOnly $propert { $this->propertyCollectionIriOnly = $propertyCollectionIriOnly; } + + /** + * @return Collection + */ + public function getChildren(): Collection + { + return $this->children; + } + + public function addChild(PropertyCollectionIriOnlyRelationSecondLevel $child): self + { + if (!$this->children->contains($child)) { + $this->children->add($child); + $child->setParent($this); + } + + return $this; + } + + public function removeChild(PropertyCollectionIriOnlyRelationSecondLevel $child): self + { + if ($this->children->removeElement($child)) { + // set the owning side to null (unless already changed) + if ($child->getParent() === $this) { + $child->setParent(null); + } + } + + return $this; + } } diff --git a/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelationSecondLevel.php b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelationSecondLevel.php new file mode 100644 index 00000000000..7522ee8da46 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelationSecondLevel.php @@ -0,0 +1,59 @@ + + * + * 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\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Post; +use Doctrine\ORM\Mapping as ORM; + +#[ + Post, + GetCollection(uriTemplate: '/property-collection-relation-second-levels'), + GetCollection( + uriTemplate: '/property_collection_iri_only_relations/{parentId}/children', + uriVariables: [ + 'parentId' => new Link(toProperty: 'parent', fromClass: PropertyCollectionIriOnlyRelation::class), + ] + ) +] +#[ORM\Entity] +class PropertyCollectionIriOnlyRelationSecondLevel +{ + /** + * The entity ID. + */ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue] + private ?int $id = null; + + #[ORM\ManyToOne(inversedBy: 'children')] + private ?PropertyCollectionIriOnlyRelation $parent = null; + + public function getId(): ?int + { + return $this->id ?? 9999; + } + + public function getParent(): ?PropertyCollectionIriOnlyRelation + { + return $this->parent; + } + + public function setParent(?PropertyCollectionIriOnlyRelation $parent): void + { + $this->parent = $parent; + } +}