core icon indicating copy to clipboard operation
core copied to clipboard

Having multiple QueryParameters with different providers does not work as expected

Open Ilyes512 opened this issue 4 months ago • 8 comments

API Platform version(s) affected: 3+

Description

If we have 2 different ParameterProviders set to 2 different query parameters, only the last provider has effect on the context/operation. So if I add a new value to the context in the first provider, that change is then not visible in the next provider.

How to reproduce

I have a DTO with a GetCollection attribute. In that attribute I have added an array of two QueryParameters to the parameters-parameter of the GetCollection attribute.

So something like this:

#[GetCollection(
    uriTemplate: 'foobar',
    parameters: [
        'a' => new QueryParameter(
            provider: ParameterOneProvider::class,
        ),
        'b' => new QueryParameter(
            provider: ParameterTwoProvider::class,
        ),
    ]
)]
final readonly class FoobarDto
{
}

Both ParameterOneProvider and ParameterTwoProvider implement the ParameterProviderInterface. In both parameters I do something like this:

class ParameterOneProvider implements ParameterProviderInterface
{
    /**
     * @param array<string, mixed>                                                                         $parameters
     * @param array<string, mixed>|array{request?: Request, resource_class?: string, operation: Operation} $context
     */
    public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
    {
        $aQuery = $this->getSearchQuery($parameter);

        if ($searchQuery === null) {
            return null;
        }

        $operation = $this->getOperation($context);
        $context = $operation->getNormalizationContext() ?? [];
        $context['a'] = $searchQuery; // <-- the key in the ParameterTwoProvider would be `b`

        return $operation->withNormalizationContext($context);
    }

    private function getOperation(array $context): Operation
    {
        $operation = $context['operation'];

        Assert::isInstanceOf($operation, Operation::class);

        return $operation;
    }

    private function getAQuery(Parameter $parameter): ?string
    {
        $searchQuery = $parameter->getValue();

        if ($searchQuery instanceof ParameterNotFound) {
            return null;
        }

        Assert::string($searchQuery);

        return trim($searchQuery);
    }
}

If we now take a look at the ApiPlatform\State\Provider\ParameterProvider we can see that the operation that is returned from the first loop is not given to the next provider. See https://github.com/api-platform/core/blob/cead16a02592e8a2446f72286a6e9d2c3503e2eb/src/State/Provider/ParameterProvider.php#L86-L90

The new operation ($op) that is returned from the $providerInstance->provide() call is assigned to the $operator variable. Using my providers the $provider instance will always be a new instance since we called withNormalizationContext that uses clone to return a new instance.

Within the loop nothing is really done with the $operator except for possibly overwriting it again with a second call to a provider. Only after leaving the loop are we then assigning it to the context https://github.com/api-platform/core/blob/cead16a02592e8a2446f72286a6e9d2c3503e2eb/src/State/Provider/ParameterProvider.php#L97

So that means we are only seeing any changes made to the operation from the last provider called.

So unless I have totally misunderstood the use cases for using ParameterProviderInterface, what's the point of creating multiple custom providers?

Possible Solution

Not really sure if my use case is what the custom ParameterProvider was meant for.

Additional Context

I work on a project where we use DTO's and don't have api-platform directly tight to Doctrine (so we don't make use of the Doctrine integration in our Symfony project).

So my use case for the custom parameter provider is for example to have a query parameter that can contain a string uuid. And what I do is turn that string uuid into a Uuid instance and make it available to the custom provider for the specific DTO. I thought I would be able to do this by setting a unique value on the context so it becomes available in the DTO's provider.

CC: @soyuka (since it seems like you have added this)

Ilyes512 avatar Sep 25 '24 12:09 Ilyes512