1 |
<?php
|
|
2 |
|
|
3 |
namespace Nuwave\Lighthouse\Schema; |
|
4 |
|
|
5 |
use Closure; |
|
6 |
use GraphQL\Error\InvariantViolation; |
|
7 |
use GraphQL\Language\AST\EnumTypeDefinitionNode; |
|
8 |
use GraphQL\Language\AST\InputObjectTypeDefinitionNode; |
|
9 |
use GraphQL\Language\AST\InterfaceTypeDefinitionNode; |
|
10 |
use GraphQL\Language\AST\ObjectTypeDefinitionNode; |
|
11 |
use GraphQL\Language\AST\ScalarTypeDefinitionNode; |
|
12 |
use GraphQL\Language\AST\TypeDefinitionNode; |
|
13 |
use GraphQL\Language\AST\UnionTypeDefinitionNode; |
|
14 |
use GraphQL\Type\Definition\EnumType; |
|
15 |
use GraphQL\Type\Definition\InputObjectType; |
|
16 |
use GraphQL\Type\Definition\InterfaceType; |
|
17 |
use GraphQL\Type\Definition\ObjectType; |
|
18 |
use GraphQL\Type\Definition\ScalarType; |
|
19 |
use GraphQL\Type\Definition\Type; |
|
20 |
use GraphQL\Type\Definition\UnionType; |
|
21 |
use Illuminate\Pipeline\Pipeline; |
|
22 |
use Nuwave\Lighthouse\Exceptions\DefinitionException; |
|
23 |
use Nuwave\Lighthouse\Schema\AST\ASTHelper; |
|
24 |
use Nuwave\Lighthouse\Schema\AST\DocumentAST; |
|
25 |
use Nuwave\Lighthouse\Schema\Directives\EnumDirective; |
|
26 |
use Nuwave\Lighthouse\Schema\Directives\InterfaceDirective; |
|
27 |
use Nuwave\Lighthouse\Schema\Directives\UnionDirective; |
|
28 |
use Nuwave\Lighthouse\Schema\Factories\ArgumentFactory; |
|
29 |
use Nuwave\Lighthouse\Schema\Factories\FieldFactory; |
|
30 |
use Nuwave\Lighthouse\Schema\Values\FieldValue; |
|
31 |
use Nuwave\Lighthouse\Schema\Values\TypeValue; |
|
32 |
use Nuwave\Lighthouse\Support\Contracts\TypeMiddleware; |
|
33 |
use Nuwave\Lighthouse\Support\Contracts\TypeResolver; |
|
34 |
use Nuwave\Lighthouse\Support\Utils; |
|
35 |
|
|
36 |
class TypeRegistry |
|
37 |
{
|
|
38 |
/**
|
|
39 |
* Resolved types.
|
|
40 |
*
|
|
41 |
* @var array<string, \GraphQL\Type\Definition\Type>
|
|
42 |
*/
|
|
43 |
protected $types = []; |
|
44 |
|
|
45 |
/**
|
|
46 |
* @var \Illuminate\Pipeline\Pipeline
|
|
47 |
*/
|
|
48 |
protected $pipeline; |
|
49 |
|
|
50 |
/**
|
|
51 |
* @var \Nuwave\Lighthouse\Schema\DirectiveLocator
|
|
52 |
*/
|
|
53 |
protected $directiveFactory; |
|
54 |
|
|
55 |
/**
|
|
56 |
* @var \Nuwave\Lighthouse\Schema\Factories\ArgumentFactory
|
|
57 |
*/
|
|
58 |
protected $argumentFactory; |
|
59 |
|
|
60 |
/**
|
|
61 |
* Lazily initialized.
|
|
62 |
*
|
|
63 |
* @var \Nuwave\Lighthouse\Schema\AST\DocumentAST
|
|
64 |
*/
|
|
65 |
protected $documentAST; |
|
66 |
|
|
67 | 1 |
public function __construct( |
68 |
Pipeline $pipeline, |
|
69 |
DirectiveLocator $directiveFactory, |
|
70 |
ArgumentFactory $argumentFactory |
|
71 |
) { |
|
72 | 1 |
$this->pipeline = $pipeline; |
73 | 1 |
$this->directiveFactory = $directiveFactory; |
74 | 1 |
$this->argumentFactory = $argumentFactory; |
75 |
}
|
|
76 |
|
|
77 |
/**
|
|
78 |
* @return $this
|
|
79 |
*/
|
|
80 | 1 |
public function setDocumentAST(DocumentAST $documentAST): self |
81 |
{
|
|
82 | 1 |
$this->documentAST = $documentAST; |
83 |
|
|
84 | 1 |
return $this; |
85 |
}
|
|
86 |
|
|
87 |
/**
|
|
88 |
* Get the given GraphQL type by name.
|
|
89 |
*
|
|
90 |
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
|
|
91 |
*/
|
|
92 | 1 |
public function get(string $name): Type |
93 |
{
|
|
94 | 1 |
if (! $this->has($name)) { |
95 | 1 |
throw new DefinitionException(<<<EOL |
96 | 1 |
Lighthouse failed while trying to load a type: $name
|
97 |
|
|
98 |
Make sure the type is present in your schema definition.
|
|
99 |
|
|
100 |
EOL
|
|
101 |
);
|
|
102 |
}
|
|
103 |
|
|
104 | 1 |
return $this->types[$name]; |
105 |
}
|
|
106 |
|
|
107 |
/**
|
|
108 |
* Is a type with the given name present?
|
|
109 |
*/
|
|
110 | 1 |
public function has(string $name): bool |
111 |
{
|
|
112 | 1 |
return isset($this->types[$name]) |
113 | 1 |
|| $this->fromAST($name) instanceof Type; |
114 |
}
|
|
115 |
|
|
116 |
/**
|
|
117 |
* Register an executable GraphQL type.
|
|
118 |
*
|
|
119 |
* @return $this
|
|
120 |
*/
|
|
121 | 1 |
public function register(Type $type): self |
122 |
{
|
|
123 | 1 |
$name = $type->name; |
124 | 1 |
if ($this->has($name)) { |
125 | 1 |
throw new DefinitionException("Tried to register a type that is already present in the schema: {$name}. Use overwrite() to ignore existing types."); |
126 |
}
|
|
127 |
|
|
128 | 1 |
$this->types[$name] = $type; |
129 |
|
|
130 | 1 |
return $this; |
131 |
}
|
|
132 |
|
|
133 |
/**
|
|
134 |
* Register a type, overwriting if it exists already.
|
|
135 |
*
|
|
136 |
* @return $this
|
|
137 |
*/
|
|
138 |
public function overwrite(Type $type): self |
|
139 |
{
|
|
140 |
$this->types[$type->name] = $type; |
|
141 |
|
|
142 |
return $this; |
|
143 |
}
|
|
144 |
|
|
145 |
/**
|
|
146 |
* Attempt to make a type of the given name from the AST.
|
|
147 |
*/
|
|
148 | 1 |
protected function fromAST(string $name): ?Type |
149 |
{
|
|
150 | 1 |
$typeDefinition = $this->documentAST->types[$name] ?? null; |
151 | 1 |
if ($typeDefinition === null) { |
152 | 1 |
return null; |
153 |
}
|
|
154 |
|
|
155 | 1 |
return $this->types[$name] = $this->handle($typeDefinition); |
156 |
}
|
|
157 |
|
|
158 |
/**
|
|
159 |
* Return all possible types that are registered.
|
|
160 |
*
|
|
161 |
* @return array<string, \GraphQL\Type\Definition\Type>
|
|
162 |
*/
|
|
163 | 1 |
public function possibleTypes(): array |
164 |
{
|
|
165 |
// Make sure all the types from the AST are eagerly converted
|
|
166 |
// to find orphaned types, such as an object type that is only
|
|
167 |
// ever used through its association to an interface
|
|
168 |
/** @var \GraphQL\Language\AST\TypeDefinitionNode&\GraphQL\Language\AST\Node $typeDefinition */
|
|
169 | 1 |
foreach ($this->documentAST->types as $typeDefinition) { |
170 | 1 |
$name = $typeDefinition->name->value; |
171 |
|
|
172 | 1 |
if (! isset($this->types[$name])) { |
173 | 1 |
$this->types[$name] = $this->handle($typeDefinition); |
174 |
}
|
|
175 |
}
|
|
176 |
|
|
177 | 1 |
return $this->types; |
178 |
}
|
|
179 |
|
|
180 |
/**
|
|
181 |
* Get the types that are currently resolved.
|
|
182 |
*
|
|
183 |
* Note that this does not all possible types, only those that
|
|
184 |
* are programmatically registered or already resolved.
|
|
185 |
*
|
|
186 |
* @return array<string, \GraphQL\Type\Definition\Type>
|
|
187 |
*/
|
|
188 | 1 |
public function resolvedTypes(): array |
189 |
{
|
|
190 | 1 |
return $this->types; |
191 |
}
|
|
192 |
|
|
193 |
/**
|
|
194 |
* Transform a definition node to an executable type.
|
|
195 |
*
|
|
196 |
* @param \GraphQL\Language\AST\TypeDefinitionNode&\GraphQL\Language\AST\Node $definition
|
|
197 |
*/
|
|
198 | 1 |
public function handle(TypeDefinitionNode $definition): Type |
199 |
{
|
|
200 | 1 |
return $this->pipeline |
201 | 1 |
->send( |
202 | 1 |
new TypeValue($definition) |
203 |
)
|
|
204 | 1 |
->through( |
205 | 1 |
$this->directiveFactory |
206 | 1 |
->associatedOfType($definition, TypeMiddleware::class) |
207 | 1 |
->all() |
208 |
)
|
|
209 | 1 |
->via('handleNode') |
210 |
->then(function (TypeValue $value) use ($definition): Type { |
|
211 | 1 |
$typeResolver = $this->directiveFactory->exclusiveOfType($definition, TypeResolver::class); |
212 | 1 |
if ($typeResolver !== null) { |
213 |
/** @var \Nuwave\Lighthouse\Support\Contracts\TypeResolver $typeResolver */
|
|
214 |
return $typeResolver->resolveNode($value); |
|
215 |
}
|
|
216 |
|
|
217 | 1 |
return $this->resolveType($definition); |
218 | 1 |
});
|
219 |
}
|
|
220 |
|
|
221 |
/**
|
|
222 |
* The default type transformations.
|
|
223 |
*
|
|
224 |
* @param \GraphQL\Language\AST\TypeDefinitionNode&\GraphQL\Language\AST\Node $typeDefinition
|
|
225 |
*
|
|
226 |
* @throws \GraphQL\Error\InvariantViolation
|
|
227 |
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
|
|
228 |
*/
|
|
229 | 1 |
protected function resolveType(TypeDefinitionNode $typeDefinition): Type |
230 |
{
|
|
231 | 1 |
switch (get_class($typeDefinition)) { |
232 |
case EnumTypeDefinitionNode::class: |
|
233 | 1 |
return $this->resolveEnumType($typeDefinition); |
234 |
case ScalarTypeDefinitionNode::class: |
|
235 | 1 |
return $this->resolveScalarType($typeDefinition); |
236 |
case ObjectTypeDefinitionNode::class: |
|
237 | 1 |
return $this->resolveObjectType($typeDefinition); |
238 |
case InputObjectTypeDefinitionNode::class: |
|
239 | 1 |
return $this->resolveInputObjectType($typeDefinition); |
240 |
case InterfaceTypeDefinitionNode::class: |
|
241 | 1 |
return $this->resolveInterfaceType($typeDefinition); |
242 |
case UnionTypeDefinitionNode::class: |
|
243 | 1 |
return $this->resolveUnionType($typeDefinition); |
244 |
default: |
|
245 |
throw new InvariantViolation( |
|
246 |
"Unknown type for definition [{$typeDefinition->name->value}]" |
|
247 |
);
|
|
248 |
}
|
|
249 |
}
|
|
250 |
|
|
251 | 1 |
protected function resolveEnumType(EnumTypeDefinitionNode $enumDefinition): EnumType |
252 |
{
|
|
253 |
/** @var array<string, array<string, mixed>> $values */
|
|
254 | 1 |
$values = []; |
255 |
|
|
256 | 1 |
foreach ($enumDefinition->values as $enumValue) { |
257 |
/** @var \Nuwave\Lighthouse\Schema\Directives\EnumDirective|null $enumDirective */
|
|
258 | 1 |
$enumDirective = $this->directiveFactory->exclusiveOfType($enumValue, EnumDirective::class); |
259 |
|
|
260 | 1 |
$values[$enumValue->name->value] = [ |
261 |
// If no explicit value is given, we default to the name of the value
|
|
262 | 1 |
'value' => $enumDirective !== null |
263 | 1 |
? $enumDirective->value() |
264 | 1 |
: $enumValue->name->value, |
265 | 1 |
'description' => data_get($enumValue->description, 'value'), |
266 | 1 |
'deprecationReason' => ASTHelper::deprecationReason($enumValue), |
267 |
];
|
|
268 |
}
|
|
269 |
|
|
270 | 1 |
return new EnumType([ |
271 | 1 |
'name' => $enumDefinition->name->value, |
272 | 1 |
'description' => data_get($enumDefinition->description, 'value'), |
273 | 1 |
'values' => $values, |
274 |
]);
|
|
275 |
}
|
|
276 |
|
|
277 |
/**
|
|
278 |
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
|
|
279 |
*/
|
|
280 | 1 |
protected function resolveScalarType(ScalarTypeDefinitionNode $scalarDefinition): ScalarType |
281 |
{
|
|
282 | 1 |
$scalarName = $scalarDefinition->name->value; |
283 |
|
|
284 | 1 |
if (($directive = ASTHelper::directiveDefinition($scalarDefinition, 'scalar')) !== null) { |
285 | 1 |
$className = ASTHelper::directiveArgValue($directive, 'class'); |
286 |
} else { |
|
287 | 1 |
$className = $scalarName; |
288 |
}
|
|
289 |
|
|
290 | 1 |
$className = Utils::namespaceClassname( |
291 |
$className, |
|
292 | 1 |
(array) config('lighthouse.namespaces.scalars'), |
293 |
function (string $className): bool { |
|
294 | 1 |
return is_subclass_of($className, ScalarType::class); |
295 |
}
|
|
296 |
);
|
|
297 |
|
|
298 | 1 |
if (! $className) { |
299 |
throw new DefinitionException( |
|
300 |
"No matching subclass of GraphQL\Type\Definition\ScalarType of found for the scalar {$scalarName}" |
|
301 |
);
|
|
302 |
}
|
|
303 |
|
|
304 | 1 |
return new $className([ |
305 | 1 |
'name' => $scalarName, |
306 | 1 |
'description' => data_get($scalarDefinition->description, 'value'), |
307 |
]);
|
|
308 |
}
|
|
309 |
|
|
310 | 1 |
protected function resolveObjectType(ObjectTypeDefinitionNode $objectDefinition): ObjectType |
311 |
{
|
|
312 | 1 |
return new ObjectType([ |
313 | 1 |
'name' => $objectDefinition->name->value, |
314 | 1 |
'description' => data_get($objectDefinition->description, 'value'), |
315 | 1 |
'fields' => $this->makeFieldsLoader($objectDefinition), |
316 |
'interfaces' => |
|
317 |
/**
|
|
318 |
* @return array<\GraphQL\Type\Definition\Type>
|
|
319 |
*/
|
|
320 |
function () use ($objectDefinition): array { |
|
321 | 1 |
$interfaces = []; |
322 |
|
|
323 |
// Might be a NodeList, so we can not use array_map()
|
|
324 | 1 |
foreach ($objectDefinition->interfaces as $interface) { |
325 | 1 |
$interfaces [] = $this->get($interface->name->value); |
326 |
}
|
|
327 |
|
|
328 | 1 |
return $interfaces; |
329 | 1 |
},
|
330 |
]);
|
|
331 |
}
|
|
332 |
|
|
333 |
/**
|
|
334 |
* Returns a closure that lazy loads the fields for a constructed type.
|
|
335 |
*
|
|
336 |
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode|\GraphQL\Language\AST\InterfaceTypeDefinitionNode $typeDefinition
|
|
337 |
*/
|
|
338 | 1 |
protected function makeFieldsLoader($typeDefinition): Closure |
339 |
{
|
|
340 |
return
|
|
341 |
/**
|
|
342 |
* @return array<string, array>
|
|
343 |
*/
|
|
344 |
function () use ($typeDefinition): array { |
|
345 | 1 |
$typeValue = new TypeValue($typeDefinition); |
346 | 1 |
$fields = []; |
347 |
|
|
348 | 1 |
foreach ($typeDefinition->fields as $fieldDefinition) { |
349 |
/** @var \Nuwave\Lighthouse\Schema\Factories\FieldFactory $fieldFactory */
|
|
350 | 1 |
$fieldFactory = app(FieldFactory::class); |
351 | 1 |
$fieldValue = new FieldValue($typeValue, $fieldDefinition); |
352 |
|
|
353 | 1 |
$fields[$fieldDefinition->name->value] = $fieldFactory->handle($fieldValue); |
354 |
}
|
|
355 |
|
|
356 | 1 |
return $fields; |
357 | 1 |
};
|
358 |
}
|
|
359 |
|
|
360 | 1 |
protected function resolveInputObjectType(InputObjectTypeDefinitionNode $inputDefinition): InputObjectType |
361 |
{
|
|
362 | 1 |
return new InputObjectType([ |
363 | 1 |
'name' => $inputDefinition->name->value, |
364 | 1 |
'description' => data_get($inputDefinition->description, 'value'), |
365 | 1 |
'astNode' => $inputDefinition, |
366 |
'fields' => |
|
367 |
/**
|
|
368 |
* @return array<string, array<string, mixed>>
|
|
369 |
*/
|
|
370 |
function () use ($inputDefinition): array { |
|
371 | 1 |
return $this->argumentFactory->toTypeMap($inputDefinition->fields); |
372 | 1 |
},
|
373 |
]);
|
|
374 |
}
|
|
375 |
|
|
376 | 1 |
protected function resolveInterfaceType(InterfaceTypeDefinitionNode $interfaceDefinition): InterfaceType |
377 |
{
|
|
378 | 1 |
$nodeName = $interfaceDefinition->name->value; |
379 |
|
|
380 | 1 |
if (($directiveNode = ASTHelper::directiveDefinition($interfaceDefinition, 'interface')) !== null) { |
381 | 1 |
$interfaceDirective = (new InterfaceDirective)->hydrate($directiveNode, $interfaceDefinition); |
382 |
|
|
383 | 1 |
$typeResolver = $interfaceDirective->getResolverFromArgument('resolveType'); |
384 |
} else { |
|
385 |
$typeResolver = |
|
386 | 1 |
$this->findTypeResolverClass( |
387 |
$nodeName, |
|
388 | 1 |
(array) config('lighthouse.namespaces.interfaces') |
389 |
)
|
|
390 | 1 |
?: $this->typeResolverFallback(); |
391 |
}
|
|
392 |
|
|
393 | 1 |
return new InterfaceType([ |
394 | 1 |
'name' => $nodeName, |
395 | 1 |
'description' => data_get($interfaceDefinition->description, 'value'), |
396 | 1 |
'fields' => $this->makeFieldsLoader($interfaceDefinition), |
397 | 1 |
'resolveType' => $typeResolver, |
398 |
]);
|
|
399 |
}
|
|
400 |
|
|
401 |
/**
|
|
402 |
* @param array<string> $namespaces
|
|
403 |
*/
|
|
404 | 1 |
protected function findTypeResolverClass(string $nodeName, array $namespaces): ?Closure |
405 |
{
|
|
406 | 1 |
$className = Utils::namespaceClassname( |
407 |
$nodeName, |
|
408 |
$namespaces, |
|
409 |
function (string $className): bool { |
|
410 | 1 |
return method_exists($className, '__invoke'); |
411 |
}
|
|
412 |
);
|
|
413 | 1 |
if ($className) { |
414 | 1 |
return Closure::fromCallable( |
415 | 1 |
[app($className), '__invoke'] |
416 |
);
|
|
417 |
}
|
|
418 |
|
|
419 | 1 |
return null; |
420 |
}
|
|
421 |
|
|
422 |
/**
|
|
423 |
* Default type resolver for resolving interfaces or union types.
|
|
424 |
*
|
|
425 |
* We just assume that the rootValue that shall be returned from the
|
|
426 |
* field is a class that is named just like the concrete Object Type
|
|
427 |
* that is supposed to be returned.
|
|
428 |
*/
|
|
429 | 1 |
protected function typeResolverFallback(): Closure |
430 |
{
|
|
431 |
return function ($rootValue): Type { |
|
432 | 1 |
return $this->get(class_basename($rootValue)); |
433 | 1 |
};
|
434 |
}
|
|
435 |
|
|
436 | 1 |
protected function resolveUnionType(UnionTypeDefinitionNode $unionDefinition): UnionType |
437 |
{
|
|
438 | 1 |
$nodeName = $unionDefinition->name->value; |
439 |
|
|
440 | 1 |
if (($directiveNode = ASTHelper::directiveDefinition($unionDefinition, 'union')) !== null) { |
441 | 1 |
$unionDirective = (new UnionDirective)->hydrate($directiveNode, $unionDefinition); |
442 |
|
|
443 | 1 |
$typeResolver = $unionDirective->getResolverFromArgument('resolveType'); |
444 |
} else { |
|
445 |
$typeResolver = |
|
446 | 1 |
$this->findTypeResolverClass( |
447 |
$nodeName, |
|
448 | 1 |
(array) config('lighthouse.namespaces.unions') |
449 |
)
|
|
450 | 1 |
?: $this->typeResolverFallback(); |
451 |
}
|
|
452 |
|
|
453 | 1 |
return new UnionType([ |
454 | 1 |
'name' => $nodeName, |
455 | 1 |
'description' => data_get($unionDefinition->description, 'value'), |
456 |
'types' => |
|
457 |
/**
|
|
458 |
* @return array<\GraphQL\Type\Definition\Type>
|
|
459 |
*/
|
|
460 |
function () use ($unionDefinition): array { |
|
461 | 1 |
$types = []; |
462 |
|
|
463 | 1 |
foreach ($unionDefinition->types as $type) { |
464 | 1 |
$types[] = $this->get($type->name->value); |
465 |
}
|
|
466 |
|
|
467 | 1 |
return $types; |
468 | 1 |
},
|
469 | 1 |
'resolveType' => $typeResolver, |
470 |
]);
|
|
471 |
}
|
|
472 |
}
|
Read our documentation on viewing source code .