Skip to content

Commit 5232204

Browse files
committed
feat(state): provide parameter values
1 parent 683c34c commit 5232204

File tree

3 files changed

+220
-0
lines changed

3 files changed

+220
-0
lines changed
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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;
15+
16+
use ApiPlatform\Metadata\Operation;
17+
use ApiPlatform\Metadata\Parameter;
18+
19+
/**
20+
* Optionnaly transforms request parameters and provides modification to the current Operation.
21+
*
22+
* @experimental
23+
*/
24+
interface ParameterProviderInterface
25+
{
26+
/**
27+
* @param array<string, mixed> $parameters
28+
* @param array<string, mixed>|array{request?: Request, resource_class?: string, operation: Operation} $context
29+
*/
30+
public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation;
31+
}
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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\Provider;
15+
16+
use ApiPlatform\Metadata\HeaderParameterInterface;
17+
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\Metadata\Parameters;
20+
use ApiPlatform\State\Exception\ProviderNotFoundException;
21+
use ApiPlatform\State\ParameterProviderInterface;
22+
use ApiPlatform\State\ProviderInterface;
23+
use ApiPlatform\State\Util\RequestParser;
24+
use Psr\Container\ContainerInterface;
25+
use Symfony\Component\HttpFoundation\Request;
26+
27+
/**
28+
* Loops over parameters to:
29+
* - compute its values set as extra properties from the Parameter object (`_api_values`)
30+
* - call the Parameter::provider if any and updates the Operation
31+
*
32+
* @experimental
33+
*/
34+
final class ParameterProvider implements ProviderInterface
35+
{
36+
public function __construct(private readonly ?ProviderInterface $decorated = null, private readonly ?ContainerInterface $locator = null)
37+
{
38+
}
39+
40+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
41+
{
42+
$request = $context['request'] ?? null;
43+
44+
if ($request && null === $request->attributes->get('_api_query_parameters')) {
45+
$queryString = RequestParser::getQueryString($request);
46+
$request->attributes->set('_api_query_parameters', $queryString ? RequestParser::parseRequestParams($queryString) : []);
47+
}
48+
49+
if ($request && null === $request->attributes->get('_api_header_parameters')) {
50+
$request->attributes->set('_api_header_parameters', $request->headers->all());
51+
}
52+
53+
$context = ['operation' => $operation] + $context;
54+
$parameters = $operation->getParameters() ?? [];
55+
$operationParameters = $parameters instanceof Parameters ? iterator_to_array($parameters) : $parameters;
56+
foreach ($operationParameters as $parameter) {
57+
$key = $parameter->getKey();
58+
$parameters = $this->extractParameterValues($parameter, $request, $context);
59+
$parsedKey = explode('[:property]', $key);
60+
if (isset($parsedKey[0]) && isset($parameters[$parsedKey[0]])) {
61+
$key = $parsedKey[0];
62+
}
63+
64+
if (!isset($parameters[$key])) {
65+
continue;
66+
}
67+
68+
$operationParameters[$parameter->getKey()] = $parameter = $parameter->withExtraProperties(
69+
$parameter->getExtraProperties() + ['_api_values' => [$key => $parameters[$key]]]
70+
);
71+
72+
if (null === ($provider = $parameter->getProvider())) {
73+
continue;
74+
}
75+
76+
if (\is_callable($provider)) {
77+
if (($op = $provider($parameter, $parameters, $context)) instanceof Operation) {
78+
$operation = $op;
79+
}
80+
81+
continue;
82+
}
83+
84+
if (!\is_string($provider) || !$this->locator->has($provider)) {
85+
throw new ProviderNotFoundException(sprintf('Provider "%s" not found on operation "%s"', $provider, $operation->getName()));
86+
}
87+
88+
/** @var ParameterProviderInterface $providerInstance */
89+
$providerInstance = $this->locator->get($provider);
90+
if (($op = $providerInstance->provide($parameter, $parameters, $context)) instanceof Operation) {
91+
$operation = $op;
92+
}
93+
}
94+
95+
$operation = $operation->withParameters(new Parameters($operationParameters));
96+
$request?->attributes->set('_api_operation', $operation);
97+
$context['operation'] = $operation;
98+
99+
return $this->decorated?->provide($operation, $uriVariables, $context);
100+
}
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+
}
114+
}
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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\Tests;
15+
16+
use ApiPlatform\Metadata\Get;
17+
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\Metadata\Parameters;
20+
use ApiPlatform\Metadata\QueryParameter;
21+
use ApiPlatform\State\ParameterProviderInterface;
22+
use ApiPlatform\State\Provider\ParameterProvider;
23+
use PHPUnit\Framework\TestCase;
24+
use Psr\Container\ContainerInterface;
25+
use Symfony\Component\HttpFoundation\Request;
26+
27+
final class ParameterProviderTest extends TestCase
28+
{
29+
public function testExtractValues(): void
30+
{
31+
$locator = new class() implements ContainerInterface {
32+
public function get(string $id)
33+
{
34+
if ('test' === $id) {
35+
return new class() implements ParameterProviderInterface {
36+
public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
37+
{
38+
return new Get(name: 'ok');
39+
}
40+
};
41+
}
42+
}
43+
44+
public function has(string $id): bool
45+
{
46+
return 'test' === $id;
47+
}
48+
};
49+
50+
$operation = new Get(parameters: new Parameters([
51+
'order' => new QueryParameter(key: 'order', provider: 'test'),
52+
'search[:property]' => new QueryParameter(key: 'search[:property]', provider: [self::class, 'provide']),
53+
'foo' => new QueryParameter(key: 'foo', provider: [self::class, 'shouldNotBeCalled']),
54+
]));
55+
$parameterProvider = new ParameterProvider(null, $locator);
56+
$request = new Request(server: ['QUERY_STRING' => 'order[foo]=asc&search[a]=bar']);
57+
$context = ['request' => $request, 'operation' => $operation];
58+
$parameterProvider->provide($operation, [], $context);
59+
$operation = $request->attributes->get('_api_operation');
60+
61+
$this->assertEquals('ok', $operation->getName());
62+
$this->assertEquals(['order' => ['foo' => 'asc']], $operation->getParameters()->get('order')->getExtraProperties()['_api_values']);
63+
$this->assertEquals(['search' => ['a' => 'bar']], $operation->getParameters()->get('search[:property]')->getExtraProperties()['_api_values']);
64+
}
65+
66+
public static function provide(): void
67+
{
68+
static::assertTrue(true);
69+
}
70+
71+
public static function shouldNotBeCalled(): void
72+
{
73+
static::assertTrue(false); // @phpstan-ignore-line
74+
}
75+
}

0 commit comments

Comments
 (0)