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\Core\GraphQl\Type; |
||||
15 | |||||
16 | use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer; |
||||
17 | use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; |
||||
18 | use GraphQL\Type\Definition\InputObjectType; |
||||
19 | use GraphQL\Type\Definition\InterfaceType; |
||||
20 | use GraphQL\Type\Definition\NonNull; |
||||
21 | use GraphQL\Type\Definition\ObjectType; |
||||
22 | use GraphQL\Type\Definition\Type as GraphQLType; |
||||
23 | use Psr\Container\ContainerInterface; |
||||
24 | use Symfony\Component\PropertyInfo\Type; |
||||
25 | |||||
26 | /** |
||||
27 | * Builds the GraphQL types. |
||||
28 | * |
||||
29 | * @experimental |
||||
30 | * |
||||
31 | * @author Alan Poulain <[email protected]> |
||||
32 | */ |
||||
33 | final class TypeBuilder implements TypeBuilderInterface |
||||
34 | { |
||||
35 | private $typesContainer; |
||||
36 | private $defaultFieldResolver; |
||||
37 | private $fieldsBuilderLocator; |
||||
38 | |||||
39 | public function __construct(TypesContainerInterface $typesContainer, callable $defaultFieldResolver, ContainerInterface $fieldsBuilderLocator) |
||||
40 | { |
||||
41 | $this->typesContainer = $typesContainer; |
||||
42 | $this->defaultFieldResolver = $defaultFieldResolver; |
||||
43 | $this->fieldsBuilderLocator = $fieldsBuilderLocator; |
||||
44 | } |
||||
45 | |||||
46 | /** |
||||
47 | * {@inheritdoc} |
||||
48 | */ |
||||
49 | public function getResourceObjectType(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input, ?string $queryName, ?string $mutationName, bool $wrapped = false, int $depth = 0): GraphQLType |
||||
50 | { |
||||
51 | $shortName = $resourceMetadata->getShortName(); |
||||
52 | |||||
53 | if (null !== $mutationName) { |
||||
54 | $shortName = $mutationName.ucfirst($shortName); |
||||
55 | } |
||||
56 | if ($input) { |
||||
57 | $shortName .= 'Input'; |
||||
58 | } elseif (null !== $mutationName) { |
||||
59 | if ($depth > 0) { |
||||
60 | $shortName .= 'Nested'; |
||||
61 | } |
||||
62 | $shortName .= 'Payload'; |
||||
63 | } |
||||
64 | if ('item_query' === $queryName) { |
||||
65 | $shortName .= 'Item'; |
||||
66 | } |
||||
67 | if ('collection_query' === $queryName) { |
||||
68 | $shortName .= 'Collection'; |
||||
69 | } |
||||
70 | if ($wrapped && null !== $mutationName) { |
||||
71 | $shortName .= 'Data'; |
||||
72 | } |
||||
73 | |||||
74 | if ($this->typesContainer->has($shortName)) { |
||||
75 | $resourceObjectType = $this->typesContainer->get($shortName); |
||||
76 | if (!($resourceObjectType instanceof ObjectType || $resourceObjectType instanceof NonNull)) { |
||||
77 | throw new \UnexpectedValueException(sprintf( |
||||
78 | 'Expected GraphQL type "%s" to be %s.', |
||||
79 | $shortName, |
||||
80 | implode('|', [ObjectType::class, NonNull::class]) |
||||
81 | )); |
||||
82 | } |
||||
83 | |||||
84 | return $resourceObjectType; |
||||
85 | } |
||||
86 | |||||
87 | $ioMetadata = $resourceMetadata->getGraphqlAttribute($mutationName ?? $queryName, $input ? 'input' : 'output', null, true); |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
88 | if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) { |
||||
89 | $resourceClass = $ioMetadata['class']; |
||||
90 | } |
||||
91 | |||||
92 | $wrapData = !$wrapped && null !== $mutationName && !$input && $depth < 1; |
||||
93 | |||||
94 | $configuration = [ |
||||
95 | 'name' => $shortName, |
||||
96 | 'description' => $resourceMetadata->getDescription(), |
||||
97 | 'resolveField' => $this->defaultFieldResolver, |
||||
98 | 'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName, $queryName, $wrapData, $depth, $ioMetadata) { |
||||
99 | if ($wrapData) { |
||||
100 | $queryNormalizationContext = $resourceMetadata->getGraphqlAttribute($queryName ?? '', 'normalization_context', [], true); |
||||
101 | $mutationNormalizationContext = $resourceMetadata->getGraphqlAttribute($mutationName ?? '', 'normalization_context', [], true); |
||||
102 | // Use a new type for the wrapped object only if there is a specific normalization context for the mutation. |
||||
103 | // If not, use the query type in order to ensure the client cache could be used. |
||||
104 | $useWrappedType = $queryNormalizationContext !== $mutationNormalizationContext; |
||||
105 | |||||
106 | return [ |
||||
107 | lcfirst($resourceMetadata->getShortName()) => $useWrappedType ? |
||||
108 | $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, true, $depth) : |
||||
109 | $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $queryName ?? 'item_query', null, true, $depth), |
||||
110 | 'clientMutationId' => GraphQLType::string(), |
||||
111 | ]; |
||||
112 | } |
||||
113 | |||||
114 | $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder'); |
||||
115 | |||||
116 | $fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $queryName, $mutationName, $depth, $ioMetadata); |
||||
117 | |||||
118 | if ($input && null !== $mutationName && null !== $mutationArgs = $resourceMetadata->getGraphql()[$mutationName]['args'] ?? null) { |
||||
119 | return $fieldsBuilder->resolveResourceArgs($mutationArgs, $mutationName, $resourceMetadata->getShortName()) + ['clientMutationId' => $fields['clientMutationId']]; |
||||
120 | } |
||||
121 | |||||
122 | return $fields; |
||||
123 | }, |
||||
124 | 'interfaces' => $wrapData ? [] : [$this->getNodeInterface()], |
||||
125 | ]; |
||||
126 | |||||
127 | $resourceObjectType = $input ? GraphQLType::nonNull(new InputObjectType($configuration)) : new ObjectType($configuration); |
||||
128 | $this->typesContainer->set($shortName, $resourceObjectType); |
||||
0 ignored issues
–
show
It seems like
$shortName can also be of type null ; however, parameter $id of ApiPlatform\Core\GraphQl...ntainerInterface::set() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
129 | |||||
130 | return $resourceObjectType; |
||||
131 | } |
||||
132 | |||||
133 | /** |
||||
134 | * {@inheritdoc} |
||||
135 | */ |
||||
136 | public function getNodeInterface(): InterfaceType |
||||
137 | { |
||||
138 | if ($this->typesContainer->has('Node')) { |
||||
139 | $nodeInterface = $this->typesContainer->get('Node'); |
||||
140 | if (!$nodeInterface instanceof InterfaceType) { |
||||
141 | throw new \UnexpectedValueException(sprintf('Expected GraphQL type "Node" to be %s.', InterfaceType::class)); |
||||
142 | } |
||||
143 | |||||
144 | return $nodeInterface; |
||||
145 | } |
||||
146 | |||||
147 | $nodeInterface = new InterfaceType([ |
||||
148 | 'name' => 'Node', |
||||
149 | 'description' => 'A node, according to the Relay specification.', |
||||
150 | 'fields' => [ |
||||
151 | 'id' => [ |
||||
152 | 'type' => GraphQLType::nonNull(GraphQLType::id()), |
||||
153 | 'description' => 'The id of this node.', |
||||
154 | ], |
||||
155 | ], |
||||
156 | 'resolveType' => function ($value) { |
||||
157 | if (!isset($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) { |
||||
158 | return null; |
||||
159 | } |
||||
160 | |||||
161 | $shortName = (new \ReflectionClass($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]))->getShortName().'Item'; |
||||
162 | |||||
163 | return $this->typesContainer->has($shortName) ? $this->typesContainer->get($shortName) : null; |
||||
164 | }, |
||||
165 | ]); |
||||
166 | |||||
167 | $this->typesContainer->set('Node', $nodeInterface); |
||||
168 | |||||
169 | return $nodeInterface; |
||||
170 | } |
||||
171 | |||||
172 | /** |
||||
173 | * {@inheritdoc} |
||||
174 | */ |
||||
175 | public function getResourcePaginatedCollectionType(GraphQLType $resourceType): GraphQLType |
||||
176 | { |
||||
177 | $shortName = $resourceType->name; |
||||
178 | |||||
179 | if ($this->typesContainer->has("{$shortName}Connection")) { |
||||
180 | return $this->typesContainer->get("{$shortName}Connection"); |
||||
181 | } |
||||
182 | |||||
183 | $edgeObjectTypeConfiguration = [ |
||||
184 | 'name' => "{$shortName}Edge", |
||||
185 | 'description' => "Edge of $shortName.", |
||||
186 | 'fields' => [ |
||||
187 | 'node' => $resourceType, |
||||
188 | 'cursor' => GraphQLType::nonNull(GraphQLType::string()), |
||||
189 | ], |
||||
190 | ]; |
||||
191 | $edgeObjectType = new ObjectType($edgeObjectTypeConfiguration); |
||||
192 | $this->typesContainer->set("{$shortName}Edge", $edgeObjectType); |
||||
193 | |||||
194 | $pageInfoObjectTypeConfiguration = [ |
||||
195 | 'name' => "{$shortName}PageInfo", |
||||
196 | 'description' => 'Information about the current page.', |
||||
197 | 'fields' => [ |
||||
198 | 'endCursor' => GraphQLType::string(), |
||||
199 | 'startCursor' => GraphQLType::string(), |
||||
200 | 'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()), |
||||
201 | 'hasPreviousPage' => GraphQLType::nonNull(GraphQLType::boolean()), |
||||
202 | ], |
||||
203 | ]; |
||||
204 | $pageInfoObjectType = new ObjectType($pageInfoObjectTypeConfiguration); |
||||
205 | $this->typesContainer->set("{$shortName}PageInfo", $pageInfoObjectType); |
||||
206 | |||||
207 | $configuration = [ |
||||
208 | 'name' => "{$shortName}Connection", |
||||
209 | 'description' => "Connection for $shortName.", |
||||
210 | 'fields' => [ |
||||
211 | 'edges' => GraphQLType::listOf($edgeObjectType), |
||||
212 | 'pageInfo' => GraphQLType::nonNull($pageInfoObjectType), |
||||
213 | 'totalCount' => GraphQLType::nonNull(GraphQLType::int()), |
||||
214 | ], |
||||
215 | ]; |
||||
216 | |||||
217 | $resourcePaginatedCollectionType = new ObjectType($configuration); |
||||
218 | $this->typesContainer->set("{$shortName}Connection", $resourcePaginatedCollectionType); |
||||
219 | |||||
220 | return $resourcePaginatedCollectionType; |
||||
221 | } |
||||
222 | |||||
223 | /** |
||||
224 | * {@inheritdoc} |
||||
225 | */ |
||||
226 | public function isCollection(Type $type): bool |
||||
227 | { |
||||
228 | return $type->isCollection() && Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType(); |
||||
229 | } |
||||
230 | } |
||||
231 |