diff --git a/src/State/Processor/RespondProcessor.php b/src/State/Processor/RespondProcessor.php
index 5017591cf78..99e68360e03 100644
--- a/src/State/Processor/RespondProcessor.php
+++ b/src/State/Processor/RespondProcessor.php
@@ -16,6 +16,7 @@
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
@@ -39,8 +40,11 @@ final class RespondProcessor implements ProcessorInterface
'DELETE' => Response::HTTP_NO_CONTENT,
];
- public function __construct(private ?IriConverterInterface $iriConverter = null, private readonly ?ResourceClassResolverInterface $resourceClassResolver = null)
- {
+ public function __construct(
+ private ?IriConverterInterface $iriConverter = null,
+ private readonly ?ResourceClassResolverInterface $resourceClassResolver = null,
+ private readonly ?OperationMetadataFactoryInterface $operationMetadataFactory = null,
+ ) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
@@ -75,11 +79,15 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
if ($hasData = ($this->resourceClassResolver && $originalData && \is_object($originalData) && $this->resourceClassResolver->isResourceClass($this->getObjectClass($originalData))) && $this->iriConverter) {
if (
- ($operation->getExtraProperties()['is_alternate_resource_metadata'] ?? false)
- && 301 === $operation->getStatus()
+ 300 <= $status && $status < 400
+ && (($operation->getExtraProperties()['is_alternate_resource_metadata'] ?? false) || ($operation->getExtraProperties()['canonical_uri_template'] ?? null))
) {
- $status = 301;
- $headers['Location'] = $this->iriConverter->getIriFromResource($originalData, UrlGeneratorInterface::ABS_PATH, $operation);
+ $canonicalOperation = $operation;
+ if ($this->operationMetadataFactory && null !== ($operation->getExtraProperties()['canonical_uri_template'] ?? null)) {
+ $canonicalOperation = $this->operationMetadataFactory->create($operation->getExtraProperties()['canonical_uri_template'], $context);
+ }
+
+ $headers['Location'] = $this->iriConverter->getIriFromResource($originalData, UrlGeneratorInterface::ABS_PATH, $canonicalOperation);
} elseif ('PUT' === $method && !$request->attributes->get('previous_data') && null === $status && ($operation instanceof Put && ($operation->getAllowCreate() ?? false))) {
$status = 201;
}
diff --git a/src/Symfony/Bundle/Resources/config/state.xml b/src/Symfony/Bundle/Resources/config/state.xml
index 473aab31f6f..4c0f96bbb47 100644
--- a/src/Symfony/Bundle/Resources/config/state.xml
+++ b/src/Symfony/Bundle/Resources/config/state.xml
@@ -37,6 +37,7 @@
+
diff --git a/tests/State/RespondProcessorTest.php b/tests/State/RespondProcessorTest.php
new file mode 100644
index 00000000000..4ccb52445a4
--- /dev/null
+++ b/tests/State/RespondProcessorTest.php
@@ -0,0 +1,100 @@
+
+ *
+ * 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\State;
+
+use ApiPlatform\Api\IriConverterInterface;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
+use ApiPlatform\Metadata\ResourceClassResolverInterface;
+use ApiPlatform\State\Processor\RespondProcessor;
+use ApiPlatform\State\ProcessorInterface;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Employee;
+use PHPUnit\Framework\TestCase;
+use Prophecy\Argument;
+use Prophecy\PhpUnit\ProphecyTrait;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+class RespondProcessorTest extends TestCase
+{
+ use ProphecyTrait;
+
+ public function testRedirectToOperation(): void
+ {
+ $canonicalUriTemplateRedirectingOperation = new Get(
+ status: 302,
+ extraProperties: [
+ 'canonical_uri_template' => '/canonical',
+ ]
+ );
+
+ $alternateRedirectingResourceOperation = new Get(
+ status: 308,
+ extraProperties: [
+ 'is_alternate_resource_metadata' => true,
+ ]
+ );
+
+ $alternateResourceOperation = new Get(
+ extraProperties: [
+ 'is_alternate_resource_metadata' => true,
+ ]
+ );
+
+ $operationMetadataFactory = $this->prophesize(OperationMetadataFactoryInterface::class);
+ $operationMetadataFactory
+ ->create('/canonical', Argument::type('array'))
+ ->shouldBeCalledOnce()
+ ->willReturn(new Get(uriTemplate: '/canonical'));
+
+ $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
+ $resourceClassResolver
+ ->isResourceClass(Employee::class)
+ ->willReturn(true);
+
+ $iriConverter = $this->prophesize(IriConverterInterface::class);
+ $iriConverter
+ ->getIriFromResource(Argument::cetera())
+ ->will(static function (array $args): ?string {
+ return ($args[2] ?? null)?->getUriTemplate() ?? '/default';
+ });
+
+ /** @var ProcessorInterface $respondProcessor */
+ $respondProcessor = new RespondProcessor($iriConverter->reveal(), $resourceClassResolver->reveal(), $operationMetadataFactory->reveal());
+
+ $response = $respondProcessor->process('content', $canonicalUriTemplateRedirectingOperation, context: [
+ 'request' => new Request(),
+ 'original_data' => new Employee(),
+ ]);
+
+ $this->assertSame(302, $response->getStatusCode());
+ $this->assertSame('/canonical', $response->headers->get('Location'));
+
+ $response = $respondProcessor->process('content', $alternateRedirectingResourceOperation, context: [
+ 'request' => new Request(),
+ 'original_data' => new Employee(),
+ ]);
+
+ $this->assertSame(308, $response->getStatusCode());
+ $this->assertSame('/default', $response->headers->get('Location'));
+
+ $response = $respondProcessor->process('content', $alternateResourceOperation, context: [
+ 'request' => new Request(),
+ 'original_data' => new Employee(),
+ ]);
+
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertNull($response->headers->get('Location'));
+ }
+}