Skip to content

Commit 8e49ee7

Browse files
committed
feat(metadata): add canonical_uri_template
1 parent 36d87de commit 8e49ee7

File tree

3 files changed

+120
-6
lines changed

3 files changed

+120
-6
lines changed

src/State/Processor/RespondProcessor.php

+14-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Metadata\HttpOperation;
1717
use ApiPlatform\Metadata\IriConverterInterface;
1818
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
1920
use ApiPlatform\Metadata\Put;
2021
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2122
use ApiPlatform\Metadata\UrlGeneratorInterface;
@@ -39,8 +40,11 @@ final class RespondProcessor implements ProcessorInterface
3940
'DELETE' => Response::HTTP_NO_CONTENT,
4041
];
4142

42-
public function __construct(private ?IriConverterInterface $iriConverter = null, private readonly ?ResourceClassResolverInterface $resourceClassResolver = null)
43-
{
43+
public function __construct(
44+
private ?IriConverterInterface $iriConverter = null,
45+
private readonly ?ResourceClassResolverInterface $resourceClassResolver = null,
46+
private readonly ?OperationMetadataFactoryInterface $operationMetadataFactory = null,
47+
) {
4448
}
4549

4650
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
@@ -75,11 +79,15 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
7579

7680
if ($hasData = ($this->resourceClassResolver && $originalData && \is_object($originalData) && $this->resourceClassResolver->isResourceClass($this->getObjectClass($originalData))) && $this->iriConverter) {
7781
if (
78-
($operation->getExtraProperties()['is_alternate_resource_metadata'] ?? false)
79-
&& 301 === $operation->getStatus()
82+
300 <= $status && $status < 400
83+
&& (($operation->getExtraProperties()['is_alternate_resource_metadata'] ?? false) || ($operation->getExtraProperties()['canonical_uri_template'] ?? null))
8084
) {
81-
$status = 301;
82-
$headers['Location'] = $this->iriConverter->getIriFromResource($originalData, UrlGeneratorInterface::ABS_PATH, $operation);
85+
$canonicalOperation = $operation;
86+
if ($this->operationMetadataFactory && null !== ($operation->getExtraProperties()['canonical_uri_template'] ?? null)) {
87+
$canonicalOperation = $this->operationMetadataFactory->create($operation->getExtraProperties()['canonical_uri_template'], $context);
88+
}
89+
90+
$headers['Location'] = $this->iriConverter->getIriFromResource($originalData, UrlGeneratorInterface::ABS_PATH, $canonicalOperation);
8391
} elseif ('PUT' === $method && !$request->attributes->get('previous_data') && null === $status && ($operation instanceof Put && ($operation->getAllowCreate() ?? false))) {
8492
$status = 201;
8593
}

src/Symfony/Bundle/Resources/config/state.xml

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<service id="api_platform.state_processor.respond" class="ApiPlatform\State\Processor\RespondProcessor">
3838
<argument type="service" id="api_platform.iri_converter" />
3939
<argument type="service" id="api_platform.resource_class_resolver" />
40+
<argument type="service" id="api_platform.metadata.operation.metadata_factory" />
4041
</service>
4142
<service id="api_platform.state_processor.main" alias="api_platform.state_processor.respond" />
4243

tests/State/RespondProcessorTest.php

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\State;
15+
16+
use ApiPlatform\Api\IriConverterInterface;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
19+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
20+
use ApiPlatform\State\Processor\RespondProcessor;
21+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Employee;
22+
use PHPUnit\Framework\TestCase;
23+
use Prophecy\Argument;
24+
use Prophecy\PhpUnit\ProphecyTrait;
25+
use Prophecy\Prophecy\ObjectProphecy;
26+
use Symfony\Component\HttpFoundation\Request;
27+
use Symfony\Component\HttpFoundation\Response;
28+
29+
class RespondProcessorTest extends TestCase
30+
{
31+
use ProphecyTrait;
32+
33+
public function testRedirectToOperation(): void
34+
{
35+
$canonicalUriTemplateRedirectingOperation = new Get(
36+
status: 302,
37+
extraProperties: [
38+
'canonical_uri_template' => '/canonical',
39+
]
40+
);
41+
42+
$alternateRedirectingResourceOperation = new Get(
43+
status: 308,
44+
extraProperties: [
45+
'is_alternate_resource_metadata' => true,
46+
]
47+
);
48+
49+
$alternateResourceOperation = new Get(
50+
extraProperties: [
51+
'is_alternate_resource_metadata' => true,
52+
]
53+
);
54+
55+
/** @var OperationMetadataFactoryInterface&ObjectProphecy $operationMetadataFactory */
56+
$operationMetadataFactory = $this->prophesize(OperationMetadataFactoryInterface::class);
57+
$operationMetadataFactory
58+
->create('/canonical', Argument::cetera())
59+
->shouldBeCalledOnce()
60+
->willReturn(new Get(uriTemplate: '/canonical'));
61+
62+
/** @var ResourceClassResolverInterface&ObjectProphecy $resourceClassResolver */
63+
$resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
64+
$resourceClassResolver
65+
->isResourceClass(Employee::class)
66+
->willReturn(true);
67+
68+
/** @var IriConverterInterface&ObjectProphecy $iriConverter */
69+
$iriConverter = $this->prophesize(IriConverterInterface::class);
70+
$iriConverter
71+
->getIriFromResource(Argument::cetera())
72+
->will(static function (array $args): ?string {
73+
return ($args[2] ?? null)?->getUriTemplate() ?? '/default';
74+
});
75+
76+
$respondProcessor = new RespondProcessor($iriConverter->reveal(), $resourceClassResolver->reveal(), $operationMetadataFactory->reveal());
77+
78+
/** @var Response $response */
79+
$response = $respondProcessor->process('content', $canonicalUriTemplateRedirectingOperation, context: [
80+
'request' => new Request(),
81+
'original_data' => new Employee(),
82+
]);
83+
84+
$this->assertSame(302, $response->getStatusCode());
85+
$this->assertSame('/canonical', $response->headers->get('Location'));
86+
87+
/** @var Response $response */
88+
$response = $respondProcessor->process('content', $alternateRedirectingResourceOperation, context: [
89+
'request' => new Request(),
90+
'original_data' => new Employee(),
91+
]);
92+
93+
$this->assertSame(308, $response->getStatusCode());
94+
$this->assertSame('/default', $response->headers->get('Location'));
95+
96+
/** @var Response $response */
97+
$response = $respondProcessor->process('content', $alternateResourceOperation, context: [
98+
'request' => new Request(),
99+
'original_data' => new Employee(),
100+
]);
101+
102+
$this->assertSame(200, $response->getStatusCode());
103+
$this->assertNull($response->headers->get('Location'));
104+
}
105+
}

0 commit comments

Comments
 (0)