Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/Symfony/Routing/IriConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,16 @@ public function getIriFromResource(object|string $resource, int $referenceType =
!$operation->getName()
|| ($operation instanceof HttpOperation && 'POST' === $operation->getMethod())
) {
$forceCollection = $operation instanceof CollectionOperationInterface;
try {
$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, $forceCollection, true);
$identifiersExtractorOperation = $operation;
} catch (OperationNotFoundException) {
if (isset($this->localOperationCache[$localOperationCacheKey])) {
$operation = $this->localOperationCache[$localOperationCacheKey];
$identifiersExtractorOperation = $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] ?? $operation;
} else {
$forceCollection = $operation instanceof CollectionOperationInterface;
try {
$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, $forceCollection, true);
$identifiersExtractorOperation = $operation;
} catch (OperationNotFoundException) {
}
}
}

Expand Down
86 changes: 86 additions & 0 deletions tests/Symfony/Routing/IriConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,92 @@ public function testGetIriFromItemWithContextOperation(): void
$this->assertSame('/dummies/1', $iriConverter->getIriFromResource($item, UrlGeneratorInterface::ABS_URL, $operation));
}

public function testGetIriFromItemWithoutOperationUsesTheLocalOperationCache(): void
{
$item = new Dummy();
$item->setId(1);

$operationName = 'operation_name';
$operation = (new Get())->withName($operationName);

$routerProphecy = $this->prophesize(RouterInterface::class);
$routerProphecy->generate($operationName, ['id' => 1], UrlGeneratorInterface::ABS_PATH)->shouldBeCalledTimes(2)->willReturn('/dummies/1');

$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
$identifiersExtractorProphecy->getIdentifiersFromItem($item, $operation, Argument::any())->shouldBeCalledTimes(2)->willReturn(['id' => 1]);

$resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->shouldBeCalledOnce()->willReturn(new ResourceMetadataCollection(Dummy::class, [
(new ApiResource())->withOperations(new Operations([$operationName => $operation])),
]));

$iriConverter = $this->getIriConverter(null, $routerProphecy, $identifiersExtractorProphecy, $resourceMetadataCollectionFactoryProphecy);

$this->assertSame('/dummies/1', $iriConverter->getIriFromResource($item));
$this->assertSame('/dummies/1', $iriConverter->getIriFromResource($item));
}

public function testGetIriFromItemWithoutOperationReusesTheCachedOperation(): void
{
$item = new Dummy();
$item->setId(1);

$cachedOperation = (new Get())->withName('cached_operation');
$staleOperation = (new Get())->withName('stale_operation');

$routerProphecy = $this->prophesize(RouterInterface::class);
$routerProphecy->generate('cached_operation', ['id' => 1], UrlGeneratorInterface::ABS_PATH)->willReturn('/dummies/1');
$routerProphecy->generate('stale_operation', ['id' => 1], UrlGeneratorInterface::ABS_PATH)->willReturn('/stale/1');

$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
$identifiersExtractorProphecy->getIdentifiersFromItem($item, Argument::cetera())->willReturn(['id' => 1]);

// Prophecy returns these in order across consecutive calls, repeating the last.
$resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn(
new ResourceMetadataCollection(Dummy::class, [(new ApiResource())->withOperations(new Operations(['cached_operation' => $cachedOperation]))]),
new ResourceMetadataCollection(Dummy::class, [(new ApiResource())->withOperations(new Operations(['stale_operation' => $staleOperation]))]),
);

$iriConverter = $this->getIriConverter(null, $routerProphecy, $identifiersExtractorProphecy, $resourceMetadataCollectionFactoryProphecy);

$this->assertSame('/dummies/1', $iriConverter->getIriFromResource($item));
$this->assertSame('/dummies/1', $iriConverter->getIriFromResource($item));
}

public function testLocalOperationCacheDistinguishesItemAndCollectionIris(): void
{
$item = new Dummy();
$item->setId(1);

$itemOperation = (new Get())->withName('item_operation')->withClass(Dummy::class);
$collectionOperation = (new GetCollection())->withName('collection_operation')->withClass(Dummy::class);

$routerProphecy = $this->prophesize(RouterInterface::class);
$routerProphecy->generate('item_operation', ['id' => 1], UrlGeneratorInterface::ABS_PATH)->shouldBeCalledTimes(2)->willReturn('/dummies/1');
$routerProphecy->generate('collection_operation', [], UrlGeneratorInterface::ABS_PATH)->shouldBeCalledTimes(2)->willReturn('/dummies');

$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
$identifiersExtractorProphecy->getIdentifiersFromItem($item, Argument::cetera())->willReturn(['id' => 1]);

// getOperation(null, $forceCollection, true) picks between the two according to $forceCollection.
$resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->shouldBeCalledTimes(2)->willReturn(new ResourceMetadataCollection(Dummy::class, [
(new ApiResource())->withOperations(new Operations([
'item_operation' => $itemOperation,
'collection_operation' => $collectionOperation,
])),
]));

$iriConverter = $this->getIriConverter(null, $routerProphecy, $identifiersExtractorProphecy, $resourceMetadataCollectionFactoryProphecy);

// Interleaved on purpose: the third and fourth calls must read the cache entry the first two wrote.
$this->assertSame('/dummies/1', $iriConverter->getIriFromResource($item));
$this->assertSame('/dummies', $iriConverter->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, new GetCollection()));
$this->assertSame('/dummies/1', $iriConverter->getIriFromResource($item));
$this->assertSame('/dummies', $iriConverter->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, new GetCollection()));
}

public function testGetIriFromItemWithNoOperations(): void
{
$this->expectExceptionMessage(\sprintf('Unable to generate an IRI for the item of type "%s"', Dummy::class));
Expand Down
Loading