Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Hydra/State/JsonStreamerProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\State\Pagination\PaginatorInterface;
Expand Down Expand Up @@ -57,10 +58,12 @@ public function __construct(
private readonly string $pageParameterName = 'page',
private readonly string $enabledParameterName = 'pagination',
private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH,
?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
) {
$this->resourceClassResolver = $resourceClassResolver;
$this->iriConverter = $iriConverter;
$this->operationMetadataFactory = $operationMetadataFactory;
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
Expand Down
8 changes: 7 additions & 1 deletion src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,13 @@ public function register(): void
$this->app->bind(ProviderInterface::class, ContentNegotiationProvider::class);

$this->app->singleton(RespondProcessor::class, function (Application $app) {
$decorated = new RespondProcessor();
$decorated = new RespondProcessor(
$app->make(IriConverterInterface::class),
$app->make(ResourceClassResolverInterface::class),
$app->make(OperationMetadataFactoryInterface::class),
$app->make(ResourceMetadataCollectionFactoryInterface::class)
);

if (class_exists(AddHeadersProcessor::class)) {
/** @var ConfigRepository */
$config = $app['config']->get('api-platform.http_cache') ?? [];
Expand Down
3 changes: 3 additions & 0 deletions src/Serializer/State/JsonStreamerProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\Util\HttpResponseHeadersTrait;
Expand Down Expand Up @@ -46,10 +47,12 @@ public function __construct(
?IriConverterInterface $iriConverter = null,
?ResourceClassResolverInterface $resourceClassResolver = null,
?OperationMetadataFactoryInterface $operationMetadataFactory = null,
?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
) {
$this->resourceClassResolver = $resourceClassResolver;
$this->iriConverter = $iriConverter;
$this->operationMetadataFactory = $operationMetadataFactory;
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
Expand Down
3 changes: 3 additions & 0 deletions src/State/Processor/RespondProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\StopwatchAwareInterface;
Expand All @@ -40,10 +41,12 @@ public function __construct(
?IriConverterInterface $iriConverter = null,
?ResourceClassResolverInterface $resourceClassResolver = null,
?OperationMetadataFactoryInterface $operationMetadataFactory = null,
?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
) {
$this->iriConverter = $iriConverter;
$this->resourceClassResolver = $resourceClassResolver;
$this->operationMetadataFactory = $operationMetadataFactory;
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
Expand Down
22 changes: 22 additions & 0 deletions src/State/Tests/Fixtures/ApiResource/Dummy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\State\Tests\Fixtures\ApiResource;

use ApiPlatform\Metadata\ApiResource;

#[ApiResource()]
class Dummy
{
public int $id;
}
40 changes: 40 additions & 0 deletions src/State/Util/HttpResponseHeadersTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@

namespace ApiPlatform\State\Util;

use ApiPlatform\Metadata\Error;
use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
use ApiPlatform\Metadata\Exception\RuntimeException;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Metadata\Util\CloneTrait;
Expand All @@ -38,6 +41,8 @@ trait HttpResponseHeadersTrait
use CloneTrait;
private ?IriConverterInterface $iriConverter;
private ?OperationMetadataFactoryInterface $operationMetadataFactory;
private ?ResourceClassResolverInterface $resourceClassResolver;
private ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory;

/**
* @param array<string, mixed> $context
Expand Down Expand Up @@ -122,6 +127,41 @@ private function getHeaders(Request $request, HttpOperation $operation, array $c
}
}

if (
!$operation instanceof Error
&& $operation->getUriTemplate()
&& $this->resourceClassResolver?->isResourceClass($operation->getClass())
) {
$this->addLinkedDataPlatformHeaders($headers, $operation);
}

return $headers;
}

private function addLinkedDataPlatformHeaders(array &$headers, HttpOperation $operation): void
{
if (!$this->resourceMetadataCollectionFactory) {
return;
}

$acceptPost = null;
$allowedMethods = ['OPTIONS', 'HEAD'];
$resourceCollection = $this->resourceMetadataCollectionFactory->create($operation->getClass());
foreach ($resourceCollection as $resource) {
foreach ($resource->getOperations() as $op) {
if ($op->getUriTemplate() === $operation->getUriTemplate()) {
$allowedMethods[] = $method = $op->getMethod();
if ('POST' === $method && \is_array($outputFormats = $op->getOutputFormats())) {
$acceptPost = implode(', ', array_merge(...array_values($outputFormats)));
}
}
}
}

if ($acceptPost) {
$headers['Accept-Post'] = $acceptPost;
}

$headers['Allow'] = implode(', ', $allowedMethods);
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/Resources/config/json_streamer/events.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'%api_platform.collection.pagination.page_parameter_name%',
'%api_platform.collection.pagination.enabled_parameter_name%',
'%api_platform.url_generation_strategy%',
service('api_platform.metadata.resource.metadata_collection_factory'),
]);

$services->set('api_platform.jsonld.state_provider.json_streamer', 'ApiPlatform\Hydra\State\JsonStreamerProvider')
Expand All @@ -41,6 +42,7 @@
service('api_platform.iri_converter'),
service('api_platform.resource_class_resolver'),
service('api_platform.metadata.operation.metadata_factory'),
service('api_platform.metadata.resource.metadata_collection_factory'),
]);

$services->set('api_platform.state_provider.json_streamer', 'ApiPlatform\Serializer\State\JsonStreamerProvider')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
'%api_platform.collection.pagination.page_parameter_name%',
'%api_platform.collection.pagination.enabled_parameter_name%',
'%api_platform.url_generation_strategy%',
service('api_platform.metadata.resource.metadata_collection_factory'),
]);

$services->set('api_platform.jsonld.state_provider.json_streamer', 'ApiPlatform\Hydra\State\JsonStreamerProvider')
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/Resources/config/json_streamer/json.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
service('api_platform.iri_converter'),
service('api_platform.resource_class_resolver'),
service('api_platform.metadata.operation.metadata_factory'),
service('api_platform.metadata.resource.metadata_collection_factory'),
]);

$services->set('api_platform.state_provider.json_streamer', 'ApiPlatform\Serializer\State\JsonStreamerProvider')
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/Resources/config/state/processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
service('api_platform.iri_converter'),
service('api_platform.resource_class_resolver'),
service('api_platform.metadata.operation.metadata_factory'),
service('api_platform.metadata.resource.metadata_collection_factory'),
]);

$services->set('api_platform.state_processor.add_link_header', 'ApiPlatform\State\Processor\AddLinkHeaderProcessor')
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/Resources/config/symfony/events.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
service('api_platform.iri_converter'),
service('api_platform.resource_class_resolver'),
service('api_platform.metadata.operation.metadata_factory'),
service('api_platform.metadata.resource.metadata_collection_factory'),
]);

$services->set('api_platform.state_processor.add_link_header', 'ApiPlatform\State\Processor\AddLinkHeaderProcessor')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Fixtures\TestBundle\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Post;

#[ApiResource(operations: [
new Get(
uriTemplate: '/dummy_get_post_delete_operations/{id}',
provider: [self::class, 'provideItem'],
),
new GetCollection(
uriTemplate: '/dummy_get_post_delete_operations',
provider: [self::class, 'provide'],
),
new Post(
uriTemplate: '/dummy_get_post_delete_operations',
provider: [self::class, 'provide'],
),
new Delete(
uriTemplate: '/dummy_get_post_delete_operations/{id}',
provider: [self::class, 'provideItem'],
),
])]
class DummyGetPostDeleteOperation
{
public ?int $id;

public ?string $name = null;

public static function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
$dummyResource = new self();
$dummyResource->id = 1;
$dummyResource->name = 'Dummy name';

return [
$dummyResource,
];
}

public static function provideItem(Operation $operation, array $uriVariables = [], array $context = []): self
{
$dummyResource = new self();
$dummyResource->id = $uriVariables['id'];
$dummyResource->name = 'Dummy name';

return $dummyResource;
}
}
78 changes: 78 additions & 0 deletions tests/Functional/LinkedDataPlatformTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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\Functional;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\DummyGetPostDeleteOperation;
use ApiPlatform\Tests\SetupClassResourcesTrait;

class LinkedDataPlatformTest extends ApiTestCase
{
use SetupClassResourcesTrait;

protected static ?bool $alwaysBootKernel = false;

/**
* @return class-string[]
*/
public static function getResources(): array
{
return [DummyGetPostDeleteOperation::class];
}

public function testAllowHeadersForResourceCollectionReflectsAllowedMethods(): void
{
$client = static::createClient();
$client->request('GET', '/dummy_get_post_delete_operations', [
'headers' => [
'Content-Type' => 'application/ld+json',
],
]);

$this->assertResponseHeaderSame('allow', 'OPTIONS, HEAD, GET, POST');

$client = static::createClient();
$client->request('GET', '/dummy_get_post_delete_operations/1', [
'headers' => [
'Content-Type' => 'application/ld+json',
],
]);

$this->assertResponseHeaderSame('allow', 'OPTIONS, HEAD, GET, DELETE');
}

public function testAcceptPostHeaderForResourceWithPostReflectsAllowedTypes(): void
{
$client = static::createClient();
$client->request('GET', '/dummy_get_post_delete_operations', [
'headers' => [
'Content-Type' => 'application/ld+json',
],
]);

$this->assertResponseHeaderSame('accept-post', 'application/ld+json, application/hal+json, application/vnd.api+json, application/xml, text/xml, application/json, text/html, application/graphql, multipart/form-data');
}

public function testAcceptPostHeaderDoesNotExistForResourceWithoutPost(): void
{
$client = static::createClient();
$client->request('GET', '/dummy_get_post_delete_operations/1', [
'headers' => [
'Content-Type' => 'application/ld+json',
],
]);

$this->assertResponseNotHasHeader('accept-post');
}
}
Loading