Skip to content

Commit efe7d06

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

File tree

3 files changed

+121
-6
lines changed

3 files changed

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

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
/** @group wip */
34+
public function testRedirectToOperation(): void
35+
{
36+
$canonicalUriTemplateRedirectingOperation = new Get(
37+
status: 302,
38+
extraProperties: [
39+
'canonical_uri_template' => '/canonical',
40+
]
41+
);
42+
43+
$alternateRedirectingResourceOperation = new Get(
44+
status: 308,
45+
extraProperties: [
46+
'is_alternate_resource_metadata' => true,
47+
]
48+
);
49+
50+
$alternateResourceOperation = new Get(
51+
extraProperties: [
52+
'is_alternate_resource_metadata' => true,
53+
]
54+
);
55+
56+
/** @var OperationMetadataFactoryInterface&ObjectProphecy $operationMetadataFactory */
57+
$operationMetadataFactory = $this->prophesize(OperationMetadataFactoryInterface::class);
58+
$operationMetadataFactory
59+
->create('/canonical', Argument::type('array'))
60+
->shouldBeCalledOnce()
61+
->willReturn(new Get(uriTemplate: '/canonical'));
62+
63+
/** @var ResourceClassResolverInterface&ObjectProphecy $resourceClassResolver */
64+
$resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
65+
$resourceClassResolver
66+
->isResourceClass(Employee::class)
67+
->willReturn(true);
68+
69+
/** @var IriConverterInterface&ObjectProphecy $iriConverter */
70+
$iriConverter = $this->prophesize(IriConverterInterface::class);
71+
$iriConverter
72+
->getIriFromResource(Argument::cetera())
73+
->will(static function (array $args): ?string {
74+
return ($args[2] ?? null)?->getUriTemplate() ?? '/default';
75+
});
76+
77+
$respondProcessor = new RespondProcessor($iriConverter->reveal(), $resourceClassResolver->reveal(), $operationMetadataFactory->reveal());
78+
79+
/** @var Response $response */
80+
$response = $respondProcessor->process('content', $canonicalUriTemplateRedirectingOperation, context: [
81+
'request' => new Request(),
82+
'original_data' => new Employee(),
83+
]);
84+
85+
$this->assertSame(302, $response->getStatusCode());
86+
$this->assertSame('/canonical', $response->headers->get('Location'));
87+
88+
/** @var Response $response */
89+
$response = $respondProcessor->process('content', $alternateRedirectingResourceOperation, context: [
90+
'request' => new Request(),
91+
'original_data' => new Employee(),
92+
]);
93+
94+
$this->assertSame(308, $response->getStatusCode());
95+
$this->assertSame('/default', $response->headers->get('Location'));
96+
97+
/** @var Response $response */
98+
$response = $respondProcessor->process('content', $alternateResourceOperation, context: [
99+
'request' => new Request(),
100+
'original_data' => new Employee(),
101+
]);
102+
103+
$this->assertSame(200, $response->getStatusCode());
104+
$this->assertNull($response->headers->get('Location'));
105+
}
106+
}

0 commit comments

Comments
 (0)