-
-
Notifications
You must be signed in to change notification settings - Fork 966
Expand file tree
/
Copy pathReadProvider.php
More file actions
155 lines (128 loc) · 6.23 KB
/
Copy pathReadProvider.php
File metadata and controls
155 lines (128 loc) · 6.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<?php
/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace ApiPlatform\GraphQl\State\Provider;
use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait;
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface;
use ApiPlatform\GraphQl\Util\ArrayTrait;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\GraphQl\Subscription;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\State\ProviderInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Reads data of the provided args, it is also called when reading relations in the same query.
*/
final class ReadProvider implements ProviderInterface
{
use ArrayTrait;
use ClassInfoTrait;
use IdentifierTrait;
public function __construct(private readonly ProviderInterface $provider, private readonly IriConverterInterface $iriConverter, private readonly ?SerializerContextBuilderInterface $serializerContextBuilder, private readonly string $nestingSeparator)
{
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
if (!$operation instanceof GraphQlOperation || !($operation->canRead() ?? true)) {
return $operation instanceof QueryCollection ? [] : null;
}
$args = $context['args'] ?? [];
if ($this->serializerContextBuilder) {
// Builtin data providers are able to use the serialization context to automatically add join clauses
$context += $this->serializerContextBuilder->create($operation->getClass(), $operation, $context, true);
}
if (!$operation instanceof CollectionOperationInterface) {
$identifier = $this->getIdentifierFromOperation($operation, $args);
if (!$identifier) {
return null;
}
try {
// For item Query carrying its own provider, dispatch through that provider.
// Mutation/Subscription keep the route-matched HTTP op so ReadProvider's class-mismatch
// diagnostics below stay accurate.
$dispatchOperation = ($operation instanceof Query && null !== $operation->getProvider()) ? $operation : null;
$item = $this->iriConverter->getResourceFromIri($identifier, $context, $dispatchOperation);
} catch (ItemNotFoundException) {
$item = null;
}
if ($operation instanceof Subscription || $operation instanceof Mutation) {
if (null === $item) {
throw new NotFoundHttpException(\sprintf('Item "%s" not found.', $args['input']['id']));
}
if ($operation->getClass() !== $this->getObjectClass($item)) {
throw new \UnexpectedValueException(\sprintf('Item "%s" did not match expected type "%s".', $args['input']['id'], $operation->getShortName()));
}
}
if (null === $item) {
return $item;
}
if (isset($context['graphql_context']) && !enum_exists($item::class)) {
$context['graphql_context']['previous_object'] = clone $item;
}
return $item;
}
if (null === ($context['root_class'] ?? null)) {
return [];
}
$uriVariables = [];
$context['filters'] = $this->getNormalizedFilters($args);
// This is how we resolve graphql links see ApiPlatform\Doctrine\Common\State\LinksHandlerTrait, I'm wondering if we couldn't do that in an UriVariables
// resolver within our ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory, this would mimic what's happening in the HTTP controller and simplify some code.
$source = $context['source'];
/** @var \GraphQL\Type\Definition\ResolveInfo $info */
$info = $context['info'];
if (isset($source[$info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY], $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
$uriVariables = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY];
$context['linkClass'] = $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY];
$context['linkProperty'] = $info->fieldName;
}
return $this->provider->provide($operation, $uriVariables, $context);
}
/**
* @param array<string, string|array> $args
*
* @return array<string, string>
*/
private function getNormalizedFilters(array $args): array
{
$filters = $args;
foreach ($filters as $name => $value) {
// Prevent numeric keys like `'1'`
$name = (string) $name;
if (\is_array($value)) {
if (strpos($name, '_list')) {
$name = substr($name, 0, \strlen($name) - \strlen('_list'));
}
// If the value contains arrays, we need to merge them for the filters to understand this syntax, proper to GraphQL to preserve the order of the arguments.
if ($this->isSequentialArrayOfArrays($value)) {
$value = array_merge(...$value);
}
$filters[$name] = $this->getNormalizedFilters($value);
}
if (strpos($name, $this->nestingSeparator)) {
// Gives a chance to relations/nested fields.
$index = array_search($name, array_keys($filters), true);
$filters =
\array_slice($filters, 0, $index + 1) +
[str_replace($this->nestingSeparator, '.', $name) => $value] +
\array_slice($filters, $index + 1);
}
}
return $filters;
}
}