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); |
||
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 |