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')); + } +}