Skip to content

Commit

Permalink
feat(metadata): add canonical_uri_template (#5832)
Browse files Browse the repository at this point in the history
  • Loading branch information
mtarld committed Sep 19, 2023
1 parent 828e429 commit 3fa0176
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 6 deletions.
20 changes: 14 additions & 6 deletions src/State/Processor/RespondProcessor.php
Expand Up @@ -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;
Expand All @@ -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 = [])
Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/Resources/config/state.xml
Expand Up @@ -37,6 +37,7 @@
<service id="api_platform.state_processor.respond" class="ApiPlatform\State\Processor\RespondProcessor">
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="api_platform.resource_class_resolver" />
<argument type="service" id="api_platform.metadata.operation.metadata_factory" />
</service>
<service id="api_platform.state_processor.main" alias="api_platform.state_processor.respond" />

Expand Down
100 changes: 100 additions & 0 deletions tests/State/RespondProcessorTest.php
@@ -0,0 +1,100 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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<Response> $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'));
}
}

0 comments on commit 3fa0176

Please sign in to comment.