api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.45k stars 878 forks source link

'uri_variables' missing from context during denormalization #6491

Open darthf1 opened 4 months ago

darthf1 commented 4 months ago

API Platform version(s) affected: 3.3.11

Description I have a custom Symfony\Component\Serializer\Normalizer\DenormalizerInterface class, which converts the request context to a typed (CQRS) Command class. Based on the uri variables, the Command constructor arguments are set and based on the type, some additional (constructor) logic is executed.

The CommandDenormalizer has the following methods:

<?php

use Application\Shared\MessageBus\CommandInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

final readonly class CommandDenormalizer implements DenormalizerInterface
{
    /**
     * @param array<mixed> $context
     */
    #[\Override]
    public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
    {
        return is_a($type, CommandInterface::class, true) && \is_array($data) && \is_array($context['uri_variables']);
    }

    #[\Override]
    public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): CommandInterface
    {
        // some logic, which also uses the uri_variables
    }
}

However, when updating from 3.3.7 to 3.3.11, the uri_variables seems to be missing from $context.

I guess directly related to https://github.com/api-platform/core/pull/6467

SpartakusMd commented 3 months ago

We're also affected by this change. We use the uri variables for later processing but they stopped coming and it breaks a lot of endpoints.

soyuka commented 3 months ago

They're removed from the context only in case of a relation. The ApiPlatform\State\UriVariablesResolverTrait may help you gather URIs for a given operation if needed. Last but not least, you should be able to get initial uri_variables using Symfony's Request no?

darthf1 commented 3 months ago

They're removed from the context only in case of a relation. The ApiPlatform\State\UriVariablesResolverTrait may help you gather URIs for a given operation if needed.

I just tried with adding ApiPlatform\State\UriVariablesResolverTrait, but the getOperationUriVariables requires the $operation to which I don't have access in the denormalizer (right ?).

Last but not least, you should be able to get initial uri_variables using Symfony's Request no?

Accessing via SF request would be possible I guess, Ill try on monday, but that is a lot of extra logic for something which was easily accessible through the context in a previous patch release?

soyuka commented 3 months ago

Either you have the operation inside the context or you need to use the ResourceMetadataFactory basicaly it's like:

https://github.com/api-platform/core/blob/ad2d5a78f36273b706e180ebdbdf1617d952138d/src/Serializer/AbstractItemNormalizer.php#L799-L801

darthf1 commented 3 months ago

Ok, so I was able to resolve it most easy with:

private RequestStack $requestStack

$request = $this->requestStack->getCurrentRequest();
/** @var array<string, scalar> $uriVariables */
$uriVariables = $request?->attributes->get('_api_uri_variables', []);
soyuka commented 2 months ago

Not sure that this is correct, uri variables are present for the main resource, but when it's a relation we can't know for sure that uri variables are the one from the request. On something else I don't understand why you would need another subsystem for CQRS, API Platform has embedded CQRS (Processor/Provider).

darthf1 commented 1 month ago

Not sure that this is correct, uri variables are present for the main resource, but when it's a relation we can't know for sure that uri variables are the one from the request. On something else I don't understand why you would need another subsystem for CQRS, API Platform has embedded CQRS (Processor/Provider).

Its only a custom denormalizer, not a full subsystem (right?).

Lets say I have the following command:

<?php

final readonly class CreateEngagementCommand
{
  public const string DENORMALIZATION_GROUP = 'engagement_create_denormalize';

  public function __construct(
      public OrganisationId $organisationId,
      #[Groups([self::DENORMALIZATION_GROUP])]
      public string $engagementName,
  ) {}
}

With the following api resource:

<?php

#[ApiResource(
  operations: [
        new Post(
          uriTemplate: '/organisations/{organisationId}/engagements',
          uriVariables: [
              'organisationId' => new Link(
                  fromClass: Organisation::class,
                  fromProperty: 'organsation',
                  security: "is_granted('" . OrganisationVoter::ATTRIBUTE_ORGANISATION_READ . "', organsation)",
              ),
          ],
          requirements: [
              'organisationId' => AbstractDomainId::REQUIREMENT,
          ],
          messenger: 'input',
          input: CreateEngagementCommand::class,
          output: false,
          status: Response::HTTP_ACCEPTED,
          denormalizationContext: [
              'groups' => [CreateEngagementCommand::DENORMALIZATION_GROUP],
          ],
      ),
  ],
)
class Engagement {}

Then I perform the following request:

curl -X POST "http://my-api.com/organisations/{organisationId}/engagements" \
-H "Content-Type: application/json" \
-d '{"engagementName": "my engagement"}'

During denormalization it would throw an error, because the $organisationId property of class CreateEngagementCommand is not an instance of OrganisationId (because no value is given in the payload, and no value is even allowed to be given because of the denormalization group).

What my denormalizer is doing, besides getting all constructor parameter from the request payload in combination with the given denormalization group, is check if there are any unset constructor parameters left, where the property name is equal to one of the provided uri variables (organisationId in this case), and where the property type is one of a domain id. Thats why I need access to those uri variables.

Well, this is the solution I came up with during my migration from v2.7 to v3. If you have a better / simpler solution I would love to hear it :) I'm still getting accustomed to all the specifics of the new operation classes.