diff --git a/features/bootstrap/DoctrineContext.php b/features/bootstrap/DoctrineContext.php index 67b7949b7cc..bc9a83e1ec2 100644 --- a/features/bootstrap/DoctrineContext.php +++ b/features/bootstrap/DoctrineContext.php @@ -1561,4 +1561,44 @@ public function testEagerLoadingNotDuplicateRelation() $this->manager->flush(); $this->manager->clear(); } + + /** + * @Given there are :nb sites with internal owner + */ + public function thereAreSitesWithInternalOwner(int $nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $internalUser = new \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\InternalUser(); + $internalUser->setFirstname('Internal'); + $internalUser->setLastname('User'); + $internalUser->setEmail('john.doe@example.com'); + $internalUser->setInternalId('INT'); + $site = new \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Site(); + $site->setTitle('title'); + $site->setDescription('description'); + $site->setOwner($internalUser); + $this->manager->persist($site); + } + $this->manager->flush(); + } + + /** + * @Given there are :nb sites with external owner + */ + public function thereAreSitesWithExternalOwner(int $nb) + { + for ($i = 1; $i <= $nb; ++$i) { + $externalUser = new \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ExternalUser(); + $externalUser->setFirstname('External'); + $externalUser->setLastname('User'); + $externalUser->setEmail('john.doe@example.com'); + $externalUser->setExternalId('EXT'); + $site = new \ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Site(); + $site->setTitle('title'); + $site->setDescription('description'); + $site->setOwner($externalUser); + $this->manager->persist($site); + } + $this->manager->flush(); + } } diff --git a/features/main/table_inheritance.feature b/features/main/table_inheritance.feature index 7a5c568283a..10b7fe43727 100644 --- a/features/main/table_inheritance.feature +++ b/features/main/table_inheritance.feature @@ -446,3 +446,118 @@ Feature: Table inheritance } } """ + + @!mongodb + @createSchema + @dropSchema + Scenario: Generate iri from parent resource + Given there are 3 sites with internal owner + When I add "Content-Type" header equal to "application/ld+json" + And I send a "GET" request to "/sites" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "pattern": "^Site$", + "required": "true" + }, + "@id": { + "type": "string", + "pattern": "^/sites/\\d+$", + "required": "true" + }, + "id": { + "type": "integer", + "required": "true" + }, + "title": { + "type": "string", + "required": "true" + }, + "description": { + "type": "string", + "required": "true" + }, + "owner": { + "type": "string", + "pattern": "^/custom_users/\\d+$", + "required": "true" + } + } + }, + "minItems": 3, + "maxItems": 3, + "required": "true" + } + } + } + """ + + @!mongodb + @createSchema + Scenario: Generate iri from current resource even if parent class is a resource + Given there are 3 sites with external owner + When I add "Content-Type" header equal to "application/ld+json" + And I send a "GET" request to "/sites" + And print last JSON response + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And print last JSON response + And the JSON should be valid according to this schema: + """ + { + "type": "object", + "properties": { + "hydra:member": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "pattern": "^Site$", + "required": "true" + }, + "@id": { + "type": "string", + "pattern": "^/sites/\\d+$", + "required": "true" + }, + "id": { + "type": "integer", + "required": "true" + }, + "title": { + "type": "string", + "required": "true" + }, + "description": { + "type": "string", + "required": "true" + }, + "owner": { + "type": "string", + "pattern": "^/external_users/\\d+$", + "required": "true" + } + } + }, + "minItems": 3, + "maxItems": 3, + "required": "true" + } + } + } + """ diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 3fde229d47d..6f5645d61ec 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -546,7 +546,8 @@ protected function getAttributeValue($object, $attribute, $format = null, array $type->isCollection() && ($collectionValueType = $type->getCollectionValueType()) && ($className = $collectionValueType->getClassName()) && - $this->resourceClassResolver->isResourceClass($className) + $this->resourceClassResolver->isResourceClass($className) && + ($className = $this->resourceClassResolver->getResourceClass($attributeValue, $className, true)) ) { return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $className, $format, $this->createChildContext($context, $attribute)); } @@ -554,7 +555,8 @@ protected function getAttributeValue($object, $attribute, $format = null, array if ( $type && ($className = $type->getClassName()) && - $this->resourceClassResolver->isResourceClass($className) + $this->resourceClassResolver->isResourceClass($className) && + ($className = $this->resourceClassResolver->getResourceClass($attributeValue, $className, true)) ) { return $this->normalizeRelation($propertyMetadata, $attributeValue, $className, $format, $this->createChildContext($context, $attribute)); } @@ -606,7 +608,8 @@ protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relate return $this->serializer->normalize($relatedObject, $format, $context); } - $iri = $this->iriConverter->getIriFromItem($relatedObject); + $iri = $this->iriConverter->getIriFromItemWithResource($relatedObject, $resourceClass); + if (isset($context['resources'])) { $context['resources'][$iri] = $iri; } diff --git a/tests/Fixtures/TestBundle/Entity/AbstractUser.php b/tests/Fixtures/TestBundle/Entity/AbstractUser.php new file mode 100644 index 00000000000..bcea76900ec --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/AbstractUser.php @@ -0,0 +1,92 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + * @ORM\InheritanceType("JOINED") + * @ApiResource( + * collectionOperations={ + * "get"={"path"="/custom_users"} + * }, + * itemOperations={ + * "get"={"path"="/custom_users/{id}"} + * } + * ) + */ +abstract class AbstractUser +{ + /** + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + /** + * @ORM\Column + */ + private $firstname; + /** + * @ORM\Column + */ + private $lastname; + /** + * @ORM\Column + */ + private $email; + + public function getId(): ?int + { + return $this->id; + } + + public function getFirstname(): ?string + { + return $this->firstname; + } + + public function setFirstname(string $firstname): self + { + $this->firstname = $firstname; + + return $this; + } + + public function getLastname(): ?string + { + return $this->lastname; + } + + public function setLastname(string $lastname): self + { + $this->lastname = $lastname; + + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/ExternalUser.php b/tests/Fixtures/TestBundle/Entity/ExternalUser.php new file mode 100644 index 00000000000..e98284e664c --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/ExternalUser.php @@ -0,0 +1,41 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + * @ApiResource + */ +class ExternalUser extends AbstractUser +{ + /** + * @ORM\Column + */ + private $externalId; + + public function getExternalId(): ?string + { + return $this->externalId; + } + + public function setExternalId(string $externalId): self + { + $this->externalId = $externalId; + + return $this; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/InternalUser.php b/tests/Fixtures/TestBundle/Entity/InternalUser.php new file mode 100644 index 00000000000..fd89fdb0ac4 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/InternalUser.php @@ -0,0 +1,39 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + */ +class InternalUser extends AbstractUser +{ + /** + * @ORM\Column + */ + private $internalId; + + public function getInternalId(): ?string + { + return $this->internalId; + } + + public function setInternalId(string $internalId): self + { + $this->internalId = $internalId; + + return $this; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Site.php b/tests/Fixtures/TestBundle/Entity/Site.php new file mode 100644 index 00000000000..4899d7a8efa --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Site.php @@ -0,0 +1,85 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ApiResource + * @ORM\Entity + */ +class Site +{ + /** + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + /** + * @ORM\Column + */ + private $title; + /** + * @ORM\Column + */ + private $description; + /** + * @ORM\OneToOne(targetEntity="AbstractUser", cascade={"persist", "remove"}) + * @ORM\JoinColumn(nullable=false) + */ + private $owner; + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(string $description): self + { + $this->description = $description; + + return $this; + } + + public function getOwner(): ?AbstractUser + { + return $this->owner; + } + + public function setOwner(AbstractUser $owner): self + { + $this->owner = $owner; + + return $this; + } +}