Skip to content

Commit b42e25f

Browse files
authored
fix(state): parameter decorates main chain (#6434)
* fix(state): parameter decorates main chain * fix: property placeholder validation
1 parent af34e72 commit b42e25f

File tree

16 files changed

+187
-61
lines changed

16 files changed

+187
-61
lines changed

src/State/Provider/ParameterProvider.php

+14-30
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,15 @@
1313

1414
namespace ApiPlatform\State\Provider;
1515

16-
use ApiPlatform\Metadata\HeaderParameterInterface;
1716
use ApiPlatform\Metadata\Operation;
1817
use ApiPlatform\Metadata\Parameter;
1918
use ApiPlatform\Metadata\Parameters;
2019
use ApiPlatform\State\Exception\ProviderNotFoundException;
2120
use ApiPlatform\State\ParameterProviderInterface;
2221
use ApiPlatform\State\ProviderInterface;
22+
use ApiPlatform\State\Util\ParameterParserTrait;
2323
use ApiPlatform\State\Util\RequestParser;
2424
use Psr\Container\ContainerInterface;
25-
use Symfony\Component\HttpFoundation\Request;
2625

2726
/**
2827
* Loops over parameters to:
@@ -33,6 +32,8 @@
3332
*/
3433
final class ParameterProvider implements ProviderInterface
3534
{
35+
use ParameterParserTrait;
36+
3637
public function __construct(private readonly ?ProviderInterface $decorated = null, private readonly ?ContainerInterface $locator = null)
3738
{
3839
}
@@ -50,31 +51,27 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5051
}
5152

5253
$context = ['operation' => $operation] + $context;
53-
$parameters = $operation->getParameters() ?? [];
54-
$operationParameters = $parameters instanceof Parameters ? iterator_to_array($parameters) : $parameters;
55-
foreach ($operationParameters as $parameter) {
54+
$p = $operation->getParameters() ?? [];
55+
$parameters = $p instanceof Parameters ? iterator_to_array($p) : $p;
56+
foreach ($parameters as $parameter) {
5657
$key = $parameter->getKey();
57-
$parameters = $this->extractParameterValues($parameter, $request, $context);
58-
$parsedKey = explode('[:property]', $key);
59-
60-
if (isset($parsedKey[0]) && isset($parameters[$parsedKey[0]])) {
61-
$key = $parsedKey[0];
62-
}
58+
$values = $this->extractParameterValues($parameter, $request, $context);
59+
$key = $this->getParameterFlattenKey($key, $values);
6360

64-
if (!isset($parameters[$key])) {
61+
if (!isset($values[$key])) {
6562
continue;
6663
}
6764

68-
$operationParameters[$parameter->getKey()] = $parameter = $parameter->withExtraProperties(
69-
$parameter->getExtraProperties() + ['_api_values' => [$key => $parameters[$key]]]
65+
$parameters[$parameter->getKey()] = $parameter = $parameter->withExtraProperties(
66+
$parameter->getExtraProperties() + ['_api_values' => [$key => $values[$key]]]
7067
);
7168

7269
if (null === ($provider = $parameter->getProvider())) {
7370
continue;
7471
}
7572

7673
if (\is_callable($provider)) {
77-
if (($op = $provider($parameter, $parameters, $context)) instanceof Operation) {
74+
if (($op = $provider($parameter, $values, $context)) instanceof Operation) {
7875
$operation = $op;
7976
}
8077

@@ -87,28 +84,15 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
8784

8885
/** @var ParameterProviderInterface $providerInstance */
8986
$providerInstance = $this->locator->get($provider);
90-
if (($op = $providerInstance->provide($parameter, $parameters, $context)) instanceof Operation) {
87+
if (($op = $providerInstance->provide($parameter, $values, $context)) instanceof Operation) {
9188
$operation = $op;
9289
}
9390
}
9491

95-
$operation = $operation->withParameters(new Parameters($operationParameters));
92+
$operation = $operation->withParameters(new Parameters($parameters));
9693
$request?->attributes->set('_api_operation', $operation);
9794
$context['operation'] = $operation;
9895

9996
return $this->decorated?->provide($operation, $uriVariables, $context);
10097
}
101-
102-
/**
103-
* @param array<string, mixed> $context
104-
*/
105-
private function extractParameterValues(Parameter $parameter, ?Request $request, array $context)
106-
{
107-
if ($request) {
108-
return $parameter instanceof HeaderParameterInterface ? $request->attributes->get('_api_header_parameters') : $request->attributes->get('_api_query_parameters');
109-
}
110-
111-
// GraphQl
112-
return $context['args'] ?? [];
113-
}
11498
}
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\State\Util;
15+
16+
use ApiPlatform\Metadata\HeaderParameterInterface;
17+
use ApiPlatform\Metadata\Parameter;
18+
use Symfony\Component\HttpFoundation\Request;
19+
20+
/**
21+
* @internal
22+
*/
23+
trait ParameterParserTrait
24+
{
25+
/**
26+
* @param array<string, mixed> $values
27+
*/
28+
private function getParameterFlattenKey(string $key, array $values): string
29+
{
30+
$parsedKey = explode('[:property]', $key);
31+
32+
if (isset($parsedKey[0]) && isset($values[$parsedKey[0]])) {
33+
return $parsedKey[0];
34+
}
35+
36+
return $key;
37+
}
38+
39+
/**
40+
* @param array<string, mixed> $context
41+
*
42+
* @return array<string, mixed>
43+
*/
44+
private function extractParameterValues(Parameter $parameter, ?Request $request, array $context): array
45+
{
46+
if ($request) {
47+
return ($parameter instanceof HeaderParameterInterface ? $request->attributes->get('_api_header_parameters') : $request->attributes->get('_api_query_parameters')) ?? [];
48+
}
49+
50+
// GraphQl
51+
return $context['args'] ?? [];
52+
}
53+
}

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

-1
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,6 @@ private function registerValidatorConfiguration(ContainerBuilder $container, arr
830830
$container->setParameter('api_platform.validator.legacy_validation_exception', $config['validator']['legacy_validation_exception'] ?? true);
831831
$loader->load('metadata/validator.xml');
832832
$loader->load('validator/validator.xml');
833-
$loader->load('symfony/parameter_validator.xml');
834833

835834
if ($this->isConfigEnabled($container, $config['graphql'])) {
836835
$loader->load('graphql/validator.xml');

src/Symfony/Bundle/Resources/config/state/provider.xml

+5
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,10 @@
3838
<argument key="$negotiator">null</argument>
3939
<argument key="$problemCompliantErrors">%api_platform.rfc_7807_compliant_errors%</argument>
4040
</service>
41+
42+
<service id="api_platform.state_provider.parameter" class="ApiPlatform\State\Provider\ParameterProvider" decorates="api_platform.state_provider.main" decoration-priority="190">
43+
<argument type="service" id="api_platform.state_provider.parameter.inner" />
44+
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
45+
</service>
4146
</services>
4247
</container>

src/Symfony/Bundle/Resources/config/state/state.xml

-5
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,6 @@
5555
</service>
5656
<service id="ApiPlatform\State\ObjectProvider" alias="api_platform.state_provider.object" />
5757

58-
<service id="api_platform.state_provider.parameter" class="ApiPlatform\State\Provider\ParameterProvider" decorates="api_platform.state_provider.read" decoration-priority="300">
59-
<argument type="service" id="api_platform.state_provider.parameter.inner" />
60-
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
61-
</service>
62-
6358
<service id="ApiPlatform\State\SerializerContextBuilderInterface" alias="api_platform.serializer.context_builder" />
6459
</services>
6560
</container>

src/Symfony/Bundle/Resources/config/symfony/events.xml

+5
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@
102102
<argument key="$problemCompliantErrors">%api_platform.rfc_7807_compliant_errors%</argument>
103103
</service>
104104

105+
<service id="api_platform.state_provider.parameter" class="ApiPlatform\State\Provider\ParameterProvider" decorates="api_platform.state_provider.read" decoration-priority="100">
106+
<argument type="service" id="api_platform.state_provider.parameter.inner" />
107+
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
108+
</service>
109+
105110
<service id="api_platform.state_processor.documentation" alias="api_platform.state_processor.respond" />
106111

107112
<service id="api_platform.state_processor.documentation.serialize" class="ApiPlatform\State\Processor\SerializeProcessor" decorates="api_platform.state_processor.documentation" decoration-priority="200">

src/Symfony/Bundle/Resources/config/symfony/parameter_validator.xml

-11
This file was deleted.

src/Symfony/Bundle/Resources/config/validator/events.xml

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="64" />
1818
</service>
1919

20+
<service id="api_platform.state_provider.parameter_validator" class="ApiPlatform\Symfony\Validator\State\ParameterValidatorProvider" public="true" decorates="api_platform.state_provider.read" decoration-priority="110">
21+
<argument type="service" id="validator" />
22+
<argument type="service" id="api_platform.state_provider.parameter_validator.inner" />
23+
</service>
24+
2025
<service id="api_platform.state_provider.query_parameter_validate" class="ApiPlatform\Symfony\Validator\State\QueryParameterValidateProvider">
2126
<argument>null</argument>
2227
<argument type="service" id="api_platform.validator.query_parameter_validator" />
@@ -26,6 +31,7 @@
2631
<argument type="service" id="api_platform.state_provider.query_parameter_validate" />
2732
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
2833
<argument>%api_platform.validator.query_parameter_validation%</argument>
34+
<argument type="service" id="api_platform.state_provider.parameter_validator" />
2935

3036
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="16" />
3137
</service>

src/Symfony/Bundle/Resources/config/validator/state.xml

+5
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@
1414
<argument type="service" id="api_platform.state_provider.validate.inner" />
1515
<argument type="service" id="api_platform.validator" />
1616
</service>
17+
18+
<service id="api_platform.state_provider.parameter_validator" class="ApiPlatform\Symfony\Validator\State\ParameterValidatorProvider" public="true" decorates="api_platform.state_provider.main" decoration-priority="191">
19+
<argument type="service" id="validator" />
20+
<argument type="service" id="api_platform.state_provider.parameter_validator.inner" />
21+
</service>
1722
</services>
1823
</container>

src/Symfony/Validator/State/ParameterValidatorProvider.php

+13-9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Metadata\Operation;
1717
use ApiPlatform\State\ProviderInterface;
18+
use ApiPlatform\State\Util\ParameterParserTrait;
1819
use ApiPlatform\Validator\Exception\ValidationException;
1920
use Symfony\Component\HttpFoundation\Request;
2021
use Symfony\Component\Validator\ConstraintViolation;
@@ -28,36 +29,39 @@
2829
*/
2930
final class ParameterValidatorProvider implements ProviderInterface
3031
{
32+
use ParameterParserTrait;
33+
3134
public function __construct(
32-
private readonly ProviderInterface $decorated,
33-
private readonly ValidatorInterface $validator
35+
private readonly ValidatorInterface $validator,
36+
private readonly ProviderInterface $decorated
3437
) {
3538
}
3639

3740
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
3841
{
39-
$body = $this->decorated->provide($operation, $uriVariables, $context);
40-
if (!$context['request'] instanceof Request) {
41-
return $body;
42+
if (!($request = $context['request']) instanceof Request) {
43+
return $this->decorated->provide($operation, $uriVariables, $context);
4244
}
4345

44-
$operation = $context['request']->attributes->get('_api_operation');
46+
$operation = $request->attributes->get('_api_operation') ?? $operation;
4547
foreach ($operation->getParameters() ?? [] as $parameter) {
4648
if (!$constraints = $parameter->getConstraints()) {
4749
continue;
4850
}
4951

50-
$value = $parameter->getExtraProperties()['_api_values'][$parameter->getKey()] ?? null;
52+
$key = $this->getParameterFlattenKey($parameter->getKey(), $this->extractParameterValues($parameter, $request, $context));
53+
$value = $parameter->getExtraProperties()['_api_values'][$key] ?? null;
5154
$violations = $this->validator->validate($value, $constraints);
5255
if (0 !== \count($violations)) {
5356
$constraintViolationList = new ConstraintViolationList();
5457
foreach ($violations as $violation) {
58+
$propertyPath = $key !== $parameter->getKey() ? $key.$violation->getPropertyPath() : ($parameter->getProperty() ?? $key);
5559
$constraintViolationList->add(new ConstraintViolation(
5660
$violation->getMessage(),
5761
$violation->getMessageTemplate(),
5862
$violation->getParameters(),
5963
$violation->getRoot(),
60-
$parameter->getProperty() ?? $parameter->getKey(),
64+
$propertyPath,
6165
$violation->getInvalidValue(),
6266
$violation->getPlural(),
6367
$violation->getCode(),
@@ -70,6 +74,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
7074
}
7175
}
7276

73-
return $body;
77+
return $this->decorated->provide($operation, $uriVariables, $context);
7478
}
7579
}

tests/.ignored-deprecations

+2
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@
1414
%The "Symfony\\Bundle\\MakerBundle\\Maker\\MakeAuthenticator" class is deprecated, use any of the Security\\Make\* commands instead%
1515

1616
%Since symfony/validator 7.1: Not passing a value for the "requireTld" option to the Url constraint is deprecated. Its default value will change to "true".%
17+
18+
%$fieldsBuilder argument of SchemaBuilder implementing "ApiPlatform\\GraphQl\\Type\\FieldsBuilderInterface" is deprecated since API Platform 3.1. It has to implement "ApiPlatform\\GraphQl\\Type\\FieldsBuilderEnumInterface" instead.%
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Metadata\GetCollection;
17+
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\Metadata\QueryParameter;
19+
use Symfony\Component\HttpFoundation\JsonResponse;
20+
use Symfony\Component\Validator\Constraints\Choice;
21+
use Symfony\Component\Validator\Constraints\Collection;
22+
use Symfony\Component\Validator\Constraints\NotBlank;
23+
24+
#[GetCollection(
25+
uriTemplate: 'query_parameter_validate_before_read',
26+
parameters: [
27+
'search' => new QueryParameter(constraints: [new NotBlank()]),
28+
'sort[:property]' => new QueryParameter(constraints: [new NotBlank(), new Collection(['id' => new Choice(['asc', 'desc'])], allowMissingFields: true)]),
29+
],
30+
provider: [self::class, 'provide']
31+
)]
32+
class ValidateParameterBeforeProvider
33+
{
34+
public static function provide(Operation $operation, array $uriVariables = [], array $context = [])
35+
{
36+
if (!$context['request']->query->get('search')) {
37+
throw new \RuntimeException('Not supposed to happen');
38+
}
39+
40+
return new JsonResponse(204);
41+
}
42+
}

tests/Functional/Parameters/DoctrineTests.php renamed to tests/Functional/Parameters/DoctrineTest.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
use Doctrine\ORM\EntityManagerInterface;
2020
use Doctrine\ORM\Tools\SchemaTool;
2121

22-
final class DoctrineTests extends ApiTestCase
22+
final class DoctrineTest extends ApiTestCase
2323
{
2424
public function testDoctrineEntitySearchFilter(): void
2525
{
@@ -55,8 +55,15 @@ public function testDoctrineEntitySearchFilter(): void
5555
$this->assertArraySubset(['foo' => 'bar', 'createdAt' => '2024-01-21T00:00:00+00:00'], $members[0]);
5656
}
5757

58+
/**
59+
* @group legacy
60+
*/
5861
public function testGraphQl(): void
5962
{
63+
if ($_SERVER['EVENT_LISTENERS_BACKWARD_COMPATIBILITY_LAYER'] ?? false) {
64+
$this->markTestSkipped('Parameters are not supported in BC mode.');
65+
}
66+
6067
$this->recreateSchema();
6168
$container = static::getContainer();
6269
$object = 'mongodb' === $container->getParameter('kernel.environment') ? 'searchFilterParameterDocuments' : 'searchFilterParameters';

tests/Functional/Parameters/HydraTests.php renamed to tests/Functional/Parameters/HydraTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515

1616
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
1717

18-
final class HydraTests extends ApiTestCase
18+
final class HydraTest extends ApiTestCase
1919
{
2020
public function testHydraTemplate(): void
2121
{
22-
$response = self::createClient()->request('GET', 'with_parameters_collection');
22+
$response = self::createClient()->request('GET', 'with_parameters_collection?hydra=1');
2323
$this->assertArraySubset(['hydra:search' => [
2424
'hydra:template' => '/with_parameters_collection{?hydra}',
2525
'hydra:mapping' => [

tests/Functional/Parameters/ParameterTests.php renamed to tests/Functional/Parameters/ParameterTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
1717

18-
final class ParameterTests extends ApiTestCase
18+
final class ParameterTest extends ApiTestCase
1919
{
2020
public function testWithGroupFilter(): void
2121
{

0 commit comments

Comments
 (0)