Passed
Push — master ( 11461b...d9477e )
by Christoffer
02:32
created

DefinitionBuilder::buildNamedType()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 6.9811
c 0
b 0
f 0
cc 7
eloc 13
nc 7
nop 1
1
<?php
2
3
namespace Digia\GraphQL\Schema;
4
5
use Digia\GraphQL\Cache\CacheAwareTrait;
6
use Digia\GraphQL\Error\CoercingException;
7
use Digia\GraphQL\Error\ExecutionException;
8
use Digia\GraphQL\Error\InvalidTypeException;
9
use Digia\GraphQL\Error\InvariantException;
10
use Digia\GraphQL\Error\LanguageException;
11
use Digia\GraphQL\Language\Node\DirectiveDefinitionNode;
12
use Digia\GraphQL\Language\Node\EnumTypeDefinitionNode;
13
use Digia\GraphQL\Language\Node\EnumValueDefinitionNode;
14
use Digia\GraphQL\Language\Node\FieldDefinitionNode;
15
use Digia\GraphQL\Language\Node\InputObjectTypeDefinitionNode;
16
use Digia\GraphQL\Language\Node\InputValueDefinitionNode;
17
use Digia\GraphQL\Language\Node\InterfaceTypeDefinitionNode;
18
use Digia\GraphQL\Language\Node\ListTypeNode;
19
use Digia\GraphQL\Language\Node\NamedTypeNode;
20
use Digia\GraphQL\Language\Node\NameNode;
21
use Digia\GraphQL\Language\Node\NodeInterface;
22
use Digia\GraphQL\Language\Node\NonNullTypeNode;
23
use Digia\GraphQL\Language\Node\ObjectTypeDefinitionNode;
24
use Digia\GraphQL\Language\Node\ScalarTypeDefinitionNode;
25
use Digia\GraphQL\Language\Node\TypeDefinitionNodeInterface;
26
use Digia\GraphQL\Language\Node\TypeNodeInterface;
27
use Digia\GraphQL\Language\Node\UnionTypeDefinitionNode;
28
use Digia\GraphQL\Schema\Resolver\ResolverRegistryInterface;
29
use Digia\GraphQL\Type\Definition\Directive;
30
use Digia\GraphQL\Type\Definition\EnumType;
31
use Digia\GraphQL\Type\Definition\InputObjectType;
32
use Digia\GraphQL\Type\Definition\InterfaceType;
33
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
34
use Digia\GraphQL\Type\Definition\ObjectType;
35
use Digia\GraphQL\Type\Definition\ScalarType;
36
use Digia\GraphQL\Type\Definition\TypeInterface;
37
use Digia\GraphQL\Type\Definition\UnionType;
38
use Psr\SimpleCache\CacheInterface;
39
use Psr\SimpleCache\InvalidArgumentException;
40
use function Digia\GraphQL\Execution\coerceDirectiveValues;
41
use function Digia\GraphQL\Type\assertNullableType;
42
use function Digia\GraphQL\Type\introspectionTypes;
43
use function Digia\GraphQL\Type\newDirective;
44
use function Digia\GraphQL\Type\newEnumType;
45
use function Digia\GraphQL\Type\newInputObjectType;
46
use function Digia\GraphQL\Type\newInterfaceType;
47
use function Digia\GraphQL\Type\newList;
48
use function Digia\GraphQL\Type\newNonNull;
49
use function Digia\GraphQL\Type\newObjectType;
50
use function Digia\GraphQL\Type\newScalarType;
51
use function Digia\GraphQL\Type\newUnionType;
52
use function Digia\GraphQL\Type\specifiedScalarTypes;
53
use function Digia\GraphQL\Util\keyMap;
54
use function Digia\GraphQL\Util\keyValueMap;
55
use function Digia\GraphQL\Util\valueFromAST;
56
57
class DefinitionBuilder implements DefinitionBuilderInterface
58
{
59
    /**
60
     * @var array
61
     */
62
    protected $typeDefinitionsMap;
63
64
    /**
65
     * @var ResolverRegistryInterface
66
     */
67
    protected $resolverRegistry;
68
69
    /**
70
     * @var callable
71
     */
72
    protected $resolveTypeFunction;
73
74
    /**
75
     * @var NamedTypeInterface[]
76
     */
77
    protected $types;
78
79
    /**
80
     * @var Directive[]
81
     */
82
    protected $directives;
83
84
    /**
85
     * DefinitionBuilder constructor.
86
     * @param array                          $typeDefinitionsMap
87
     * @param ResolverRegistryInterface|null $resolverRegistry
88
     * @param callable|null                  $resolveTypeFunction
89
     * @param CacheInterface                 $cache
90
     * @throws InvalidArgumentException
91
     */
92
    public function __construct(
93
        array $typeDefinitionsMap,
94
        ?ResolverRegistryInterface $resolverRegistry = null,
95
        array $types = [],
96
        array $directives = [],
97
        ?callable $resolveTypeFunction = null
98
    ) {
99
        $this->typeDefinitionsMap  = $typeDefinitionsMap;
100
        $this->resolverRegistry    = $resolverRegistry;
101
        $this->resolveTypeFunction = $resolveTypeFunction ?? [$this, 'defaultTypeResolver'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $resolveTypeFunction ?? ... 'defaultTypeResolver') can also be of type array<integer,Digia\Grap...finitionBuilder|string>. However, the property $resolveTypeFunction is declared as type callable. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
102
103
        $this->registerTypes($types);
104
        $this->registerDirectives($directives);
105
    }
106
107
    /**
108
     * @inheritdoc
109
     */
110
    public function buildTypes(array $nodes): array
111
    {
112
        return \array_map(function (NodeInterface $node) {
113
            return $this->buildType($node);
114
        }, $nodes);
115
    }
116
117
    /**
118
     * @inheritdoc
119
     * @param NamedTypeNode|TypeDefinitionNodeInterface $node
120
     */
121
    public function buildType(NodeInterface $node): NamedTypeInterface
122
    {
123
        $typeName = $node->getNameValue();
0 ignored issues
show
Bug introduced by
The method getNameValue() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of Digia\GraphQL\Language\Node\NodeInterface such as Digia\GraphQL\Language\Node\DirectiveNode or Digia\GraphQL\Language\Node\ArgumentNode or Digia\GraphQL\Language\Node\ObjectFieldNode or Digia\GraphQL\Language\Node\FieldNode or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...DefinitionNodeInterface or Digia\GraphQL\Language\N...EnumValueDefinitionNode or Digia\GraphQL\Language\N...DirectiveDefinitionNode or Digia\GraphQL\Language\N...nputValueDefinitionNode or Digia\GraphQL\Language\Node\FieldDefinitionNode or Digia\GraphQL\Language\Node\FragmentSpreadNode or Digia\GraphQL\Language\Node\VariableNode or Digia\GraphQL\Language\Node\NamedTypeNode or Digia\GraphQL\Language\N...ObjectTypeExtensionNode or Digia\GraphQL\Language\N...ScalarTypeExtensionNode or Digia\GraphQL\Language\Node\EnumTypeExtensionNode or Digia\GraphQL\Language\N...ObjectTypeExtensionNode or Digia\GraphQL\Language\N...erfaceTypeExtensionNode or Digia\GraphQL\Language\Node\UnionTypeExtensionNode. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

123
        /** @scrutinizer ignore-call */ 
124
        $typeName = $node->getNameValue();
Loading history...
124
125
        if (isset($this->types[$typeName])) {
126
            return $this->types[$typeName];
127
        }
128
129
        if ($node instanceof NamedTypeNode) {
130
            $definition = $this->getTypeDefinition($typeName);
131
132
            $type = null !== $definition
133
                ? $this->buildNamedType($definition)
134
                : $this->resolveType($node);
135
        } else {
136
            $type = $this->buildNamedType($node);
137
        }
138
139
        return $this->types[$typeName] = $type;
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function buildDirective(DirectiveDefinitionNode $node): Directive
146
    {
147
        $directiveName = $node->getNameValue();
148
149
        if (isset($this->directives[$directiveName])) {
150
            return $this->directives[$directiveName];
151
        }
152
153
        $directive = newDirective([
154
            'name'        => $node->getNameValue(),
155
            'description' => $node->getDescriptionValue(),
156
            'locations'   => \array_map(function (NameNode $node) {
157
                return $node->getValue();
158
            }, $node->getLocations()),
159
            'args'        => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
160
            'astNode'     => $node,
161
        ]);
162
163
        return $this->directives[$directiveName] = $directive;
164
    }
165
166
    /**
167
     * @inheritdoc
168
     */
169
    public function buildField($node, ?callable $resolve = null): array
170
    {
171
        return [
172
            'type'              => $this->buildWrappedType($node->getType()),
173
            'description'       => $node->getDescriptionValue(),
174
            'args'              => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
175
            'deprecationReason' => $this->getDeprecationReason($node),
176
            'resolve'           => $resolve,
177
            'astNode'           => $node,
178
        ];
179
    }
180
181
    /**
182
     * @param TypeNodeInterface $typeNode
183
     * @return TypeInterface
184
     * @throws InvariantException
185
     * @throws InvalidTypeException
186
     */
187
    protected function buildWrappedType(TypeNodeInterface $typeNode): TypeInterface
188
    {
189
        $typeDefinition = $this->buildType($this->getNamedTypeNode($typeNode));
190
        return $this->buildWrappedTypeRecursive($typeDefinition, $typeNode);
191
    }
192
193
    /**
194
     * @param TypeInterface      $innerType
195
     * @param NamedTypeInterface $inputTypeNode
196
     * @return TypeInterface
197
     * @throws InvariantException
198
     * @throws InvalidTypeException
199
     */
200
    protected function buildWrappedTypeRecursive(
201
        NamedTypeInterface $innerType,
202
        TypeNodeInterface $inputTypeNode
203
    ): TypeInterface {
204
        if ($inputTypeNode instanceof ListTypeNode) {
205
            return newList($this->buildWrappedTypeRecursive($innerType, $inputTypeNode->getType()));
206
        }
207
208
        if ($inputTypeNode instanceof NonNullTypeNode) {
209
            $wrappedType = $this->buildWrappedTypeRecursive($innerType, $inputTypeNode->getType());
210
            return newNonNull(assertNullableType($wrappedType));
211
        }
212
213
        return $innerType;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $innerType returns the type Digia\GraphQL\Type\Definition\NamedTypeInterface which is incompatible with the type-hinted return Digia\GraphQL\Type\Definition\TypeInterface.
Loading history...
214
    }
215
216
    /**
217
     * @param array $types
218
     * @throws InvalidArgumentException
219
     */
220
    protected function registerTypes(array $customTypes)
221
    {
222
        $typesMap = keyMap(
223
            \array_merge($customTypes, specifiedScalarTypes(), introspectionTypes()),
224
            function (NamedTypeInterface $type) {
225
                return $type->getName();
226
            }
227
        );
228
229
        foreach ($typesMap as $typeName => $type) {
230
            $this->types[$typeName] = $type;
231
        }
232
    }
233
234
    /**
235
     * @param array $directives
236
     * @throws InvalidArgumentException
237
     */
238
    protected function registerDirectives(array $customDirectives)
239
    {
240
        $directivesMap = keyMap(
241
            \array_merge($customDirectives, specifiedDirectives()),
242
            function (Directive $directive) {
243
                return $directive->getName();
244
            }
245
        );
246
247
        foreach ($directivesMap as $directiveName => $directive) {
248
            $this->directives[$directiveName] = $directive;
249
        }
250
    }
251
252
    /**
253
     * @param array $nodes
254
     * @return array
255
     * @throws CoercingException
256
     */
257
    protected function buildArguments(array $nodes): array
258
    {
259
        return keyValueMap(
260
            $nodes,
261
            function (InputValueDefinitionNode $value) {
262
                return $value->getNameValue();
263
            },
264
            function (InputValueDefinitionNode $value): array {
265
                $type         = $this->buildWrappedType($value->getType());
266
                $defaultValue = $value->getDefaultValue();
267
                return [
268
                    'type'         => $type,
269
                    'description'  => $value->getDescriptionValue(),
270
                    'defaultValue' => null !== $defaultValue
271
                        ? valueFromAST($defaultValue, $type)
272
                        : null,
273
                    'astNode'      => $value,
274
                ];
275
            });
276
    }
277
278
    /**
279
     * @param TypeDefinitionNodeInterface $node
280
     * @return NamedTypeInterface
281
     * @throws LanguageException
282
     */
283
    protected function buildNamedType(TypeDefinitionNodeInterface $node): NamedTypeInterface
284
    {
285
        if ($node instanceof ObjectTypeDefinitionNode) {
286
            return $this->buildObjectType($node);
287
        }
288
        if ($node instanceof InterfaceTypeDefinitionNode) {
289
            return $this->buildInterfaceType($node);
290
        }
291
        if ($node instanceof EnumTypeDefinitionNode) {
292
            return $this->buildEnumType($node);
293
        }
294
        if ($node instanceof UnionTypeDefinitionNode) {
295
            return $this->buildUnionType($node);
296
        }
297
        if ($node instanceof ScalarTypeDefinitionNode) {
298
            return $this->buildScalarType($node);
299
        }
300
        if ($node instanceof InputObjectTypeDefinitionNode) {
301
            return $this->buildInputObjectType($node);
302
        }
303
304
        throw new LanguageException(\sprintf('Type kind "%s" not supported.', $node->getKind()));
305
    }
306
307
    /**
308
     * @param ObjectTypeDefinitionNode $node
309
     * @return ObjectType
310
     */
311
    protected function buildObjectType(ObjectTypeDefinitionNode $node): ObjectType
312
    {
313
        return newObjectType([
314
            'name'        => $node->getNameValue(),
315
            'description' => $node->getDescriptionValue(),
316
            'fields'      => $node->hasFields() ? function () use ($node) {
317
                return $this->buildFields($node);
318
            } : [],
319
            // Note: While this could make early assertions to get the correctly
320
            // typed values, that would throw immediately while type system
321
            // validation with validateSchema() will produce more actionable results.
322
            'interfaces'  => function () use ($node) {
323
                return $node->hasInterfaces() ? \array_map(function (NodeInterface $interface) {
324
                    return $this->buildType($interface);
325
                }, $node->getInterfaces()) : [];
326
            },
327
            'astNode'     => $node,
328
        ]);
329
    }
330
331
    /**
332
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|InputObjectTypeDefinitionNode $node
333
     * @return array
334
     */
335
    protected function buildFields($node): array
336
    {
337
        return keyValueMap(
338
            $node->getFields(),
339
            function ($value) {
340
                /** @var FieldDefinitionNode|InputValueDefinitionNode $value */
341
                return $value->getNameValue();
342
            },
343
            function ($value) use ($node) {
344
                /** @var FieldDefinitionNode|InputValueDefinitionNode $value */
345
                return $this->buildField($value,
346
                    $this->getFieldResolver($node->getNameValue(), $value->getNameValue()));
347
            }
348
        );
349
    }
350
351
    /**
352
     * @param string $typeName
353
     * @param string $fieldName
354
     * @return callable|null
355
     */
356
    protected function getFieldResolver(string $typeName, string $fieldName): ?callable
357
    {
358
        return null !== $this->resolverRegistry
359
            ? $this->resolverRegistry->getFieldResolver($typeName, $fieldName)
360
            : null;
361
    }
362
363
    /**
364
     * @param InterfaceTypeDefinitionNode $node
365
     * @return InterfaceType
366
     */
367
    protected function buildInterfaceType(InterfaceTypeDefinitionNode $node): InterfaceType
368
    {
369
        return newInterfaceType([
370
            'name'        => $node->getNameValue(),
371
            'description' => $node->getDescriptionValue(),
372
            'fields'      => $node->hasFields() ? function () use ($node): array {
373
                return $this->buildFields($node);
374
            } : [],
375
            'resolveType' => $this->getTypeResolver($node->getNameValue()),
376
            'astNode'     => $node,
377
        ]);
378
    }
379
380
    /**
381
     * @param EnumTypeDefinitionNode $node
382
     * @return EnumType
383
     */
384
    protected function buildEnumType(EnumTypeDefinitionNode $node): EnumType
385
    {
386
        return newEnumType([
387
            'name'        => $node->getNameValue(),
388
            'description' => $node->getDescriptionValue(),
389
            'values'      => $node->hasValues() ? keyValueMap(
390
                $node->getValues(),
391
                function (EnumValueDefinitionNode $value): string {
392
                    return $value->getNameValue();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $value->getNameValue() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
393
                },
394
                function (EnumValueDefinitionNode $value): array {
395
                    return [
396
                        'description'       => $value->getDescriptionValue(),
397
                        'deprecationReason' => $this->getDeprecationReason($value),
398
                        'astNode'           => $value,
399
                    ];
400
                }
401
            ) : [],
402
            'astNode'     => $node,
403
        ]);
404
    }
405
406
    /**
407
     * @param UnionTypeDefinitionNode $node
408
     * @return UnionType
409
     */
410
    protected function buildUnionType(UnionTypeDefinitionNode $node): UnionType
411
    {
412
        return newUnionType([
413
            'name'        => $node->getNameValue(),
414
            'description' => $node->getDescriptionValue(),
415
            'types'       => $node->hasTypes() ? \array_map(function (TypeNodeInterface $type) {
416
                return $this->buildType($type);
417
            }, $node->getTypes()) : [],
418
            'resolveType' => $this->getTypeResolver($node->getNameValue()),
419
            'astNode'     => $node,
420
        ]);
421
    }
422
423
    /**
424
     * @param string $typeName
425
     * @return callable|null
426
     */
427
    protected function getTypeResolver(string $typeName): ?callable
428
    {
429
        return null !== $this->resolverRegistry
430
            ? $this->resolverRegistry->getTypeResolver($typeName)
431
            : null;
432
    }
433
434
    /**
435
     * @param ScalarTypeDefinitionNode $node
436
     * @return ScalarType
437
     */
438
    protected function buildScalarType(ScalarTypeDefinitionNode $node): ScalarType
439
    {
440
        return newScalarType([
441
            'name'        => $node->getNameValue(),
442
            'description' => $node->getDescriptionValue(),
443
            'serialize'   => function ($value) {
444
                return $value;
445
            },
446
            'astNode'     => $node,
447
        ]);
448
    }
449
450
    /**
451
     * @param InputObjectTypeDefinitionNode $node
452
     * @return InputObjectType
453
     */
454
    protected function buildInputObjectType(InputObjectTypeDefinitionNode $node): InputObjectType
455
    {
456
        return newInputObjectType([
457
            'name'        => $node->getNameValue(),
458
            'description' => $node->getDescriptionValue(),
459
            'fields'      => $node->hasFields() ? function () use ($node) {
460
                return keyValueMap(
461
                    $node->getFields(),
462
                    function (InputValueDefinitionNode $value): string {
463
                        return $value->getNameValue();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $value->getNameValue() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
464
                    },
465
                    function (InputValueDefinitionNode $value): array {
466
                        $type         = $this->buildWrappedType($value->getType());
467
                        $defaultValue = $value->getDefaultValue();
468
                        return [
469
                            'type'         => $type,
470
                            'description'  => $value->getDescriptionValue(),
471
                            'defaultValue' => null !== $defaultValue
472
                                ? valueFromAST($defaultValue, $type)
473
                                : null,
474
                            'astNode'      => $value,
475
                        ];
476
                    }
477
                );
478
            } : [],
479
            'astNode'     => $node,
480
        ]);
481
    }
482
483
    /**
484
     * @param NamedTypeNode $node
485
     * @return NamedTypeInterface
486
     */
487
    protected function resolveType(NamedTypeNode $node): NamedTypeInterface
488
    {
489
        return \call_user_func($this->resolveTypeFunction, $node);
490
    }
491
492
    /**
493
     * @param NamedTypeNode $node
494
     * @return NamedTypeInterface|null
495
     * @throws InvalidArgumentException
496
     */
497
    public function defaultTypeResolver(NamedTypeNode $node): ?NamedTypeInterface
498
    {
499
        return $this->types[$node->getNameValue()] ?? null;
500
    }
501
502
    /**
503
     * @param string $typeName
504
     * @return TypeDefinitionNodeInterface|null
505
     */
506
    protected function getTypeDefinition(string $typeName): ?TypeDefinitionNodeInterface
507
    {
508
        return $this->typeDefinitionsMap[$typeName] ?? null;
509
    }
510
511
    /**
512
     * @param TypeNodeInterface $typeNode
513
     * @return NamedTypeNode
514
     */
515
    protected function getNamedTypeNode(TypeNodeInterface $typeNode): NamedTypeNode
516
    {
517
        $namedType = $typeNode;
518
519
        while ($namedType instanceof ListTypeNode || $namedType instanceof NonNullTypeNode) {
520
            $namedType = $namedType->getType();
521
        }
522
523
        return $namedType;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $namedType returns the type Digia\GraphQL\Language\Node\TypeNodeInterface which includes types incompatible with the type-hinted return Digia\GraphQL\Language\Node\NamedTypeNode.
Loading history...
524
    }
525
526
    /**
527
     * @param NodeInterface|EnumValueDefinitionNode|FieldDefinitionNode $node
528
     * @return null|string
529
     * @throws InvariantException
530
     * @throws ExecutionException
531
     * @throws InvalidTypeException
532
     */
533
    protected function getDeprecationReason(NodeInterface $node): ?string
534
    {
535
        $deprecated = coerceDirectiveValues(DeprecatedDirective(), $node);
536
        return $deprecated['reason'] ?? null;
537
    }
538
}
539