Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(state): add headers to comply with LDP specification #6917

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

LaurentHuzard
Copy link

Q A
Branch? main
Tickets -
License MIT
Doc PR api-platform/docs#

To ensure compliance with the LDP specification (https://www.w3.org/TR/ldp/):

  • Added the "Accept-Post" header with the value "text/turtle, application/ld+json".
  • Added the "Allow" header with values based on the allowed operations on the queried resources.

To ensure compliance with the LDP specification (https://www.w3.org/TR/ldp/):
- Added the "Accept-Post" header with the value "text/turtle, application/ld+json".
- Added the "Allow" header with values based on the allowed operations on the queried resources.
@LaurentHuzard LaurentHuzard marked this pull request as draft January 14, 2025 15:41
When I add "Content-Type" header equal to "application/ld+json"
And I send a "GET" request to "/dummy_get_post_delete_operations"
Then the header "Accept-Post" should be equal to "text/turtle, application/ld+json"
And the header "Allow" should be equal to "OPTIONS, HEAD, GET, POST, DELETE"
Copy link
Member

@dunglas dunglas Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the value be different for item and collection URLs?

Also, Allow-Post should only be defined for URLs where POST is enabled (usually collections, but not always).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right, I overlooked that. I’ll make the changes and push them soon.

@@ -88,6 +93,9 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
$headers['Accept-Patch'] = $acceptPatch;
}

$headers['Accept-Post'] = 'text/turtle, application/ld+json';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value cannot be hardcoded. For instance Turtle is not supported natively by API Platform and JSON-LD may or may not depending on the config.

You should inject the enabled formats here. You can retrieve this from metadata. Listing all allowed media types (not only Turtle and JSON-LD) is probably OK (and a best practice).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds much better to list all the allowed media types. I'll take care of it.


private function getAllowedMethods(?string $resourceClass): string
{
$allowedMethods = self::DEFAULT_ALLOWED_METHOD;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should filter here the operations with a different URI Template than the one of the current operation to fix the issue described in my 1st comment.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point—I’ll sort this out.

@@ -88,6 +93,16 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
$headers['Accept-Patch'] = $acceptPatch;
}

if (!$exception) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid returning incorrect values—such as in the case of a 401 error when requesting a resource—I added this condition, although I’m not entirely sure it’s the best approach. The issue arises because, in the case of a 401 error, the allowed methods and the currentUriTemplate correspond to the error itself rather than the requested resource.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach looks good to me.

}
}

return array_unique($allowedMethods);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's necessary because it's not possible to register two different routes for the same URL and the same method.

It would remove this as we are on the hot path.

To be honest, I would inline this code and remove this method. This will also allow you to set a variable (instead of running an O(n) operation) to check if POST is defined or not for Accept-Post, and this would remove a function call.

(Yes, these are mostly a micro optimization, but this also simplify the code IMHO).

private function getAllowedMethods(?string $resourceClass, ?string $currentUriTemplate): array
{
$allowedMethods = self::DEFAULT_ALLOWED_METHOD;
if (null !== $currentUriTemplate && null !== $resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (null !== $currentUriTemplate && null !== $resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass)) {
if (null !== $currentUriTemplate && null !== $resourceClass && $this->resourceClassResolver?->isResourceClass($resourceClass)) {

{
$allowedMethods = self::DEFAULT_ALLOWED_METHOD;
if (null !== $currentUriTemplate && null !== $resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass)) {
$resourceMetadataCollection = $this->resourceCollectionMetadataFactory ? $this->resourceCollectionMetadataFactory->create($resourceClass) : new ResourceMetadataCollection($resourceClass);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$resourceMetadataCollection = $this->resourceCollectionMetadataFactory ? $this->resourceCollectionMetadataFactory->create($resourceClass) : new ResourceMetadataCollection($resourceClass);
$resourceMetadataCollection = $this->resourceCollectionMetadataFactory ? $this->resourceCollectionMetadataFactory->create($resourceClass) : new ResourceMetadataCollection($resourceClass);

Better not set the extra headers at all if the factory is missing.

It's an optional feature after all.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it's an optional feature, should I add a configuration variable to enable it?

}
$headers['Allow'] = implode(', ', $allowedMethods);

if ($isPostAllowed && \is_array($outputFormats = ($outputFormats = $operation->getOutputFormats())) && [] !== $outputFormats) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ($isPostAllowed && \is_array($outputFormats = ($outputFormats = $operation->getOutputFormats())) && [] !== $outputFormats) {
if ($isPostAllowed && \is_array($outputFormats = $operation->getOutputFormats()) && [] !== $outputFormats) {

public function __construct(
private ?IriConverterInterface $iriConverter = null,
private readonly ?ResourceClassResolverInterface $resourceClassResolver = null,
private readonly ?OperationMetadataFactoryInterface $operationMetadataFactory = null,
private readonly ?ResourceMetadataCollectionFactoryInterface $resourceCollectionMetadataFactory = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably create an LinkedDataPlatformProcessor?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! A LinkedDataPlatformProcessor sounds like a good idea, I'll consider implementing it.

@@ -88,6 +92,27 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
$headers['Accept-Patch'] = $acceptPatch;
}

if (!$exception) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ($operation instanceof ApiPlatform\Metadata\Error)

foreach ($resource->getOperations() as $resourceOperation) {
if ($resourceOperation->getUriTemplate() === $currentUriTemplate) {
$allowedMethods[] = $operationMethod = $resourceOperation->getMethod();
$isPostAllowed = $isPostAllowed || ('POST' === $operationMethod);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could collect outputFormats here directly? The operation below may not be the one where the formats are defined.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Thanks.

}
$headers['Allow'] = implode(', ', $allowedMethods);

if ($isPostAllowed && \is_array($outputFormats = ($outputFormats = $operation->getOutputFormats())) && [] !== $outputFormats) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ($isPostAllowed && \is_array($outputFormats = ($outputFormats = $operation->getOutputFormats())) && [] !== $outputFormats) {
if ($isPostAllowed && ($outputFormats = $operation->getOutputFormats())) {

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class RespondProcessorTest extends TestCase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a RespondProcessorTest already exists it must be in tests forgot to move it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, i will merge them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing RespondProcessorTest use the old way of doing test with prophecy instead of PHPunit mock system, should i refactor it ?

@@ -21,6 +21,7 @@
<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" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on-invalid=ignore

use ApiPlatform\Metadata\Post;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

#[ApiResource(operations: [new Get(), new GetCollection(), new Post(), new Delete()])]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you really need an entity/document for this? I think only the DummyGetPostDeleteOperation is enough, if you need a dummy provider you can create a static one.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, i probably did that when i was struggling with behat or trying to fix some MongoDB test on CI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes with Behat by default they both run, as this feature doesn't impact doctrine we don't need to setup any entities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants