Skip to content

Commit 3fa0176

Browse files
authored
feat(metadata): add canonical_uri_template (#5832)
1 parent 828e429 commit 3fa0176

File tree

3 files changed

+115
-6
lines changed

3 files changed

+115
-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

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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\State\ProcessorInterface;
22+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Employee;
23+
use PHPUnit\Framework\TestCase;
24+
use Prophecy\Argument;
25+
use Prophecy\PhpUnit\ProphecyTrait;
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+
$operationMetadataFactory = $this->prophesize(OperationMetadataFactoryInterface::class);
56+
$operationMetadataFactory
57+
->create('/canonical', Argument::type('array'))
58+
->shouldBeCalledOnce()
59+
->willReturn(new Get(uriTemplate: '/canonical'));
60+
61+
$resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
62+
$resourceClassResolver
63+
->isResourceClass(Employee::class)
64+
->willReturn(true);
65+
66+
$iriConverter = $this->prophesize(IriConverterInterface::class);
67+
$iriConverter
68+
->getIriFromResource(Argument::cetera())
69+
->will(static function (array $args): ?string {
70+
return ($args[2] ?? null)?->getUriTemplate() ?? '/default';
71+
});
72+
73+
/** @var ProcessorInterface<Response> $respondProcessor */
74+
$respondProcessor = new RespondProcessor($iriConverter->reveal(), $resourceClassResolver->reveal(), $operationMetadataFactory->reveal());
75+
76+
$response = $respondProcessor->process('content', $canonicalUriTemplateRedirectingOperation, context: [
77+
'request' => new Request(),
78+
'original_data' => new Employee(),
79+
]);
80+
81+
$this->assertSame(302, $response->getStatusCode());
82+
$this->assertSame('/canonical', $response->headers->get('Location'));
83+
84+
$response = $respondProcessor->process('content', $alternateRedirectingResourceOperation, context: [
85+
'request' => new Request(),
86+
'original_data' => new Employee(),
87+
]);
88+
89+
$this->assertSame(308, $response->getStatusCode());
90+
$this->assertSame('/default', $response->headers->get('Location'));
91+
92+
$response = $respondProcessor->process('content', $alternateResourceOperation, context: [
93+
'request' => new Request(),
94+
'original_data' => new Employee(),
95+
]);
96+
97+
$this->assertSame(200, $response->getStatusCode());
98+
$this->assertNull($response->headers->get('Location'));
99+
}
100+
}

0 commit comments

Comments
 (0)