diff --git a/features/doctrine/issue5722/subresource_without_get.feature b/features/doctrine/issue5722/subresource_without_get.feature new file mode 100644 index 00000000000..ff54949e926 --- /dev/null +++ b/features/doctrine/issue5722/subresource_without_get.feature @@ -0,0 +1,8 @@ +Feature: Get a subresource from inverse side that has no item operation + + @!mongodb + @createSchema + Scenario: Get a subresource from inverse side that has no item operation + Given there are logs on an event + When I send a "GET" request to "/events/03af3507-271e-4cca-8eee-6244fb06e95b/logs" + Then the response status code should be 200 diff --git a/src/Api/IdentifiersExtractor.php b/src/Api/IdentifiersExtractor.php index f93c31e03f6..5a8af1a87b0 100644 --- a/src/Api/IdentifiersExtractor.php +++ b/src/Api/IdentifiersExtractor.php @@ -72,10 +72,11 @@ private function getIdentifiersFromOperation(object $item, Operation $operation, } $identifiers = []; - foreach ($links ?? [] as $link) { - if (1 < (is_countable($link->getIdentifiers()) ? \count($link->getIdentifiers()) : 0)) { + foreach ($links ?? [] as $k => $link) { + $linkIdentifiers = $link->getIdentifiers() ?? [$k]; + if (1 < \count($linkIdentifiers)) { $compositeIdentifiers = []; - foreach ($link->getIdentifiers() as $identifier) { + foreach ($linkIdentifiers as $identifier) { $compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $identifier, $link->getParameterName()); } @@ -84,7 +85,7 @@ private function getIdentifiersFromOperation(object $item, Operation $operation, } $parameterName = $link->getParameterName(); - $identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $link->getIdentifiers()[0], $parameterName, $link->getToProperty()); + $identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $linkIdentifiers[0], $parameterName, $link->getToProperty()); } return $identifiers; diff --git a/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php index de23c4a6e8e..403fa7da532 100644 --- a/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php @@ -71,7 +71,7 @@ public function create(string $resourceClass): ResourceMetadataCollection // No item operation has been found on all resources for resource class: generate one on the last resource // Helpful to generate an IRI for a resource without declaring the Get operation /** @var HttpOperation $operation */ - [$key, $operation] = $this->getOperationWithDefaults($resource, new NotExposed(), true, ['uriTemplate']); // @phpstan-ignore-line $resource is defined if count > 0 + [$key, $operation] = $this->getOperationWithDefaults(resource: $resource, operation: new NotExposed(), generated: true, ignoredOptions: ['uriTemplate', 'uriVariables']); // @phpstan-ignore-line $resource is defined if count > 0 if (!$this->linkFactory->createLinksFromIdentifiers($operation)) { $operation = $operation->withUriTemplate(self::$skolemUriTemplate); diff --git a/src/Metadata/Tests/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactoryTest.php index 964efa63721..90f729f1a8c 100644 --- a/src/Metadata/Tests/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactoryTest.php @@ -208,6 +208,7 @@ class: AttributeResource::class shortName: 'AttributeResource', types: ['https://schema.org/Book'], uriTemplate: '/custom_api_resources', // uriTemplate should not be inherited on NotExposed operation + uriVariables: ['slug'], // same as it is used to generate the uriTemplate of our NotExposed operation operations: [ '_api_AttributeResource_get_collection' => new GetCollection(controller: 'api_platform.action.placeholder', shortName: 'AttributeResource', class: AttributeResource::class), ], @@ -227,6 +228,7 @@ class: AttributeResource::class new ApiResource( shortName: 'AttributeResource', uriTemplate: '/custom_api_resources', + uriVariables: ['slug'], types: ['https://schema.org/Book'], operations: [ '_api_AttributeResource_get_collection' => new GetCollection(controller: 'api_platform.action.placeholder', shortName: 'AttributeResource', class: AttributeResource::class), diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index dd510039fd8..4b1949531a3 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -142,6 +142,8 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InitializeInput; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InternalUser; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\IriOnlyDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\Event; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\ItemLog; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsRelatedDummy; @@ -2147,6 +2149,23 @@ public function thereIsAResourceUsingEntityClassAndDateTime(): void $this->manager->flush(); } + /** + * @Given there are logs on an event + */ + public function thereAreLogsOnAnEvent(): void + { + $entity = new Event(); + $entity->logs = new ArrayCollection([new ItemLog(), new ItemLog()]); + $entity->uuid = Uuid::fromString('03af3507-271e-4cca-8eee-6244fb06e95b'); + $this->manager->persist($entity); + foreach ($entity->logs as $log) { + $log->item = $entity; + $this->manager->persist($log); + } + + $this->manager->flush(); + } + private function isOrm(): bool { return null !== $this->schemaTool; diff --git a/tests/Fixtures/TestBundle/Entity/Issue5722/Event.php b/tests/Fixtures/TestBundle/Entity/Issue5722/Event.php new file mode 100644 index 00000000000..159445c22c5 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5722/Event.php @@ -0,0 +1,53 @@ + + * + * 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\Issue5722; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Ramsey\Uuid\Uuid; + +#[ApiResource( + shortName: 'EventIssue5722', + operations: [ + new GetCollection(), + new Get(), + ], +)] +#[ORM\Entity] +class Event +{ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[ApiProperty(readable: false, writable: false, identifier: false)] + public $id; + + #[ApiProperty(writable: false, identifier: true)] + #[ORM\Column(type: 'uuid', unique: true)] + public $uuid; + + #[ORM\OneToMany(targetEntity: ItemLog::class, cascade: ['persist'], orphanRemoval: false, mappedBy: 'item')] + public Collection $logs; + + public function __construct() + { + $this->logs = new ArrayCollection(); + $this->uuid = Uuid::uuid4(); + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue5722/ItemLog.php b/tests/Fixtures/TestBundle/Entity/Issue5722/ItemLog.php new file mode 100644 index 00000000000..699a561666a --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue5722/ItemLog.php @@ -0,0 +1,70 @@ + + * + * 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\Issue5722; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use Doctrine\ORM\Mapping as ORM; +use Ramsey\Uuid\Uuid; + +#[ApiResource( + shortName: 'ItemLogIssue5722', + operations: [ + new GetCollection(), + ], +)] +#[ApiResource( + uriTemplate: '/events/{uuid}/logs{._format}', + operations: [ + new GetCollection(), + ], + uriVariables: [ + 'uuid' => new Link(fromProperty: 'logs', fromClass: Event::class), + ], +)] +#[ORM\Entity] +class ItemLog +{ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[ApiProperty(readable: false, writable: false, identifier: false)] // identifier was false before 3.1.14, changing this to true fixed some errors + private ?int $id = null; + + #[ApiProperty(writable: false, identifier: true)] + #[ORM\Column(type: 'uuid', unique: true)] + public $uuid; + + #[ORM\ManyToOne(targetEntity: Event::class, inversedBy: 'logs')] + public ?Event $item = null; + + #[ApiProperty(required: true)] + #[ORM\Column] + public string $action = 'insert'; + + private \DateTimeInterface $createdAt; + + public function __construct() + { + $this->createdAt = new \DateTime(); + $this->uuid = Uuid::uuid4(); + } + + public function getId(): ?int + { + return $this->id; + } +}