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 0
    public function overwrite(Type $type): self
139
    {
140 0
        $this->types[$type->name] = $type;
141

142 0
        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 0
                    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 0
                throw new InvariantViolation(
246 0
                    "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 0
            throw new DefinitionException(
300 0
                "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 .

Loading