Completed
Pull Request — master (#129)
by Christoffer
02:14
created

DefinitionBuilder::getNamedTypeNode()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 4
nc 2
nop 1
1
<?php
2
3
namespace Digia\GraphQL\SchemaBuilder;
4
5
use Digia\GraphQL\Error\CoercingException;
6
use Digia\GraphQL\Error\ExecutionException;
7
use Digia\GraphQL\Error\InvalidTypeException;
8
use Digia\GraphQL\Error\InvariantException;
9
use Digia\GraphQL\Error\LanguageException;
10
use Digia\GraphQL\Execution\ValuesResolver;
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\Type\Definition\DirectiveInterface;
29
use Digia\GraphQL\Type\Definition\EnumType;
30
use Digia\GraphQL\Type\Definition\InputObjectType;
31
use Digia\GraphQL\Type\Definition\InterfaceType;
32
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
33
use Digia\GraphQL\Type\Definition\ObjectType;
34
use Digia\GraphQL\Type\Definition\ScalarType;
35
use Digia\GraphQL\Type\Definition\TypeInterface;
36
use Digia\GraphQL\Type\Definition\UnionType;
37
use Psr\SimpleCache\CacheInterface;
38
use function Digia\GraphQL\Type\assertNullableType;
39
use function Digia\GraphQL\Type\GraphQLDirective;
40
use function Digia\GraphQL\Type\GraphQLEnumType;
41
use function Digia\GraphQL\Type\GraphQLInputObjectType;
42
use function Digia\GraphQL\Type\GraphQLInterfaceType;
43
use function Digia\GraphQL\Type\GraphQLList;
44
use function Digia\GraphQL\Type\GraphQLNonNull;
45
use function Digia\GraphQL\Type\GraphQLObjectType;
46
use function Digia\GraphQL\Type\GraphQLScalarType;
47
use function Digia\GraphQL\Type\GraphQLUnionType;
48
use function Digia\GraphQL\Type\introspectionTypes;
49
use function Digia\GraphQL\Type\specifiedScalarTypes;
50
use function Digia\GraphQL\Util\keyMap;
51
use function Digia\GraphQL\Util\keyValMap;
52
53
class DefinitionBuilder implements DefinitionBuilderInterface
54
{
55
    private const CACHE_PREFIX = 'GraphQL_DefinitionBuilder_';
56
57
    /**
58
     * @var CacheInterface
59
     */
60
    protected $cache;
61
62
    /**
63
     * @var ValuesResolver
64
     */
65
    protected $valuesResolver;
66
67
    /**
68
     * @var callable
69
     */
70
    protected $resolveTypeFunction;
71
72
    /**
73
     * @var array
74
     */
75
    protected $resolverMap;
76
77
    /**
78
     * @var array
79
     */
80
    protected $typeDefinitionsMap;
81
82
    /**
83
     * DefinitionBuilder constructor.
84
     *
85
     * @param callable       $resolveTypeFunction
86
     * @param CacheInterface $cache
87
     * @throws \Psr\SimpleCache\InvalidArgumentException
88
     */
89
    public function __construct(
90
        CacheInterface $cache,
91
        ValuesResolver $valuesResolver,
92
        ?callable $resolveTypeFunction = null
93
    ) {
94
        $this->valuesResolver      = $valuesResolver;
95
        $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,string|Dig...lder\DefinitionBuilder>. 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...
96
        $this->typeDefinitionsMap  = [];
97
98
        $builtInTypes = keyMap(
99
            \array_merge(specifiedScalarTypes(), introspectionTypes()),
100
            function (NamedTypeInterface $type) {
101
                return $type->getName();
102
            }
103
        );
104
105
        foreach ($builtInTypes as $name => $type) {
106
            $cache->set($this->getCacheKey($name), $type);
107
        }
108
109
        $this->cache = $cache;
110
    }
111
112
    /**
113
     * @inheritdoc
114
     */
115
    public function setTypeDefinitionMap(array $typeDefinitionMap): DefinitionBuilder
116
    {
117
        $this->typeDefinitionsMap = $typeDefinitionMap;
118
        return $this;
119
    }
120
121
    /**
122
     * @param array $resolverMap
123
     * @return DefinitionBuilder
124
     */
125
    public function setResolverMap(array $resolverMap): DefinitionBuilder
126
    {
127
        $this->resolverMap = $resolverMap;
128
        return $this;
129
    }
130
131
    /**
132
     * @param NamedTypeNode|TypeDefinitionNodeInterface $node
133
     * @inheritdoc
134
     */
135
    public function buildType(NodeInterface $node): TypeInterface
136
    {
137
        $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\FragmentSpreadNode 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\Node\VariableDefinitionNode 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\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

137
        /** @scrutinizer ignore-call */ 
138
        $typeName = $node->getNameValue();
Loading history...
138
139
        if (!$this->cache->has($this->getCacheKey($typeName))) {
140
            if ($node instanceof NamedTypeNode) {
141
                $definition = $this->getTypeDefinition($typeName);
142
143
                $type = null !== $definition ? $this->buildNamedType($definition) : $this->resolveType($node);
144
145
                $this->cache->set($this->getCacheKey($typeName), $type);
146
            } else {
147
                $this->cache->set($this->getCacheKey($typeName), $this->buildNamedType($node));
148
            }
149
        }
150
151
        return $this->cache->get($this->getCacheKey($typeName));
152
    }
153
154
    /**
155
     * @inheritdoc
156
     */
157
    public function buildDirective(DirectiveDefinitionNode $node): DirectiveInterface
158
    {
159
        return GraphQLDirective([
160
            'name'        => $node->getNameValue(),
161
            'description' => $node->getDescriptionValue(),
162
            'locations'   => \array_map(function (NameNode $node) {
163
                return $node->getValue();
164
            }, $node->getLocations()),
165
            'arguments'   => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
166
            'astNode'     => $node,
167
        ]);
168
    }
169
170
    /**
171
     * @param TypeNodeInterface $typeNode
172
     * @return TypeInterface
173
     * @throws InvariantException
174
     * @throws InvalidTypeException
175
     */
176
    protected function buildWrappedType(TypeNodeInterface $typeNode): TypeInterface
177
    {
178
        $typeDefinition = $this->buildType($this->getNamedTypeNode($typeNode));
179
        return buildWrappedType($typeDefinition, $typeNode);
180
    }
181
182
    /**
183
     * @param FieldDefinitionNode|InputValueDefinitionNode $node
184
     * @return array
185
     * @throws ExecutionException
186
     * @throws InvalidTypeException
187
     * @throws InvariantException
188
     * @throws CoercingException
189
     */
190
    protected function buildField($node, array $resolverMap): array
191
    {
192
        return [
193
            'type'              => $this->buildWrappedType($node->getType()),
194
            'description'       => $node->getDescriptionValue(),
195
            'arguments'         => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
0 ignored issues
show
Bug introduced by
The method hasArguments() does not exist on Digia\GraphQL\Language\N...nputValueDefinitionNode. ( Ignorable by Annotation )

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

195
            'arguments'         => $node->/** @scrutinizer ignore-call */ hasArguments() ? $this->buildArguments($node->getArguments()) : [],

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getArguments() does not exist on Digia\GraphQL\Language\N...nputValueDefinitionNode. ( Ignorable by Annotation )

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

195
            'arguments'         => $node->hasArguments() ? $this->buildArguments($node->/** @scrutinizer ignore-call */ getArguments()) : [],

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
196
            'deprecationReason' => $this->getDeprecationReason($node),
197
            'resolve'           => $resolverMap[$node->getNameValue()] ?? null,
198
            'astNode'           => $node,
199
        ];
200
    }
201
202
    /**
203
     * @param array $nodes
204
     * @return array
205
     * @throws CoercingException
206
     */
207
    protected function buildArguments(array $nodes): array
208
    {
209
        return keyValMap(
210
            $nodes,
211
            function (InputValueDefinitionNode $value) {
212
                return $value->getNameValue();
213
            },
214
            function (InputValueDefinitionNode $value): array {
215
                $type         = $this->buildWrappedType($value->getType());
216
                $defaultValue = $value->getDefaultValue();
217
                return [
218
                    'type'         => $type,
219
                    'description'  => $value->getDescriptionValue(),
220
                    'defaultValue' => null !== $defaultValue
221
                        ? $this->valuesResolver->coerceValueFromAST($defaultValue, $type)
222
                        : null,
223
                    'astNode'      => $value,
224
                ];
225
            });
226
    }
227
228
    /**
229
     * @param TypeDefinitionNodeInterface $node
230
     * @return NamedTypeInterface
231
     * @throws LanguageException
232
     */
233
    protected function buildNamedType(TypeDefinitionNodeInterface $node): NamedTypeInterface
234
    {
235
        if ($node instanceof ObjectTypeDefinitionNode) {
236
            return $this->buildObjectType($node);
237
        }
238
        if ($node instanceof InterfaceTypeDefinitionNode) {
239
            return $this->buildInterfaceType($node);
240
        }
241
        if ($node instanceof EnumTypeDefinitionNode) {
242
            return $this->buildEnumType($node);
243
        }
244
        if ($node instanceof UnionTypeDefinitionNode) {
245
            return $this->buildUnionType($node);
246
        }
247
        if ($node instanceof ScalarTypeDefinitionNode) {
248
            return $this->buildScalarType($node);
249
        }
250
        if ($node instanceof InputObjectTypeDefinitionNode) {
251
            return $this->buildInputObjectType($node);
252
        }
253
254
        throw new LanguageException(\sprintf('Type kind "%s" not supported.', $node->getKind()));
255
    }
256
257
    /**
258
     * @param ObjectTypeDefinitionNode $node
259
     * @return ObjectType
260
     */
261
    protected function buildObjectType(ObjectTypeDefinitionNode $node): ObjectType
262
    {
263
        return GraphQLObjectType([
264
            'name'        => $node->getNameValue(),
265
            'description' => $node->getDescriptionValue(),
266
            'fields'      => function () use ($node) {
267
                return $this->buildFields($node);
268
            },
269
            'interfaces'  => function () use ($node) {
270
                return $node->hasInterfaces() ? \array_map(function (NodeInterface $interface) {
271
                    return $this->buildType($interface);
272
                }, $node->getInterfaces()) : [];
273
            },
274
        ]);
275
    }
276
277
    /**
278
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|InputObjectTypeDefinitionNode $node
279
     * @return array
280
     */
281
    protected function buildFields($node): array
282
    {
283
        $resolverMap = $this->resolverMap[$node->getNameValue()] ?? [];
284
285
        return $node->hasFields() ? keyValMap(
286
            $node->getFields(),
287
            function ($value) {
288
                /** @noinspection PhpUndefinedMethodInspection */
289
                return $value->getNameValue();
290
            },
291
            function ($value) use ($resolverMap) {
292
                return $this->buildField($value, $resolverMap);
293
            }
294
        ) : [];
295
    }
296
297
    /**
298
     * @param InterfaceTypeDefinitionNode $node
299
     * @return InterfaceType
300
     */
301
    protected function buildInterfaceType(InterfaceTypeDefinitionNode $node): InterfaceType
302
    {
303
        return GraphQLInterfaceType([
304
            'name'        => $node->getNameValue(),
305
            'description' => $node->getDescriptionValue(),
306
            'fields'      => function () use ($node): array {
307
                return $this->buildFields($node);
308
            },
309
            'astNode'     => $node,
310
        ]);
311
    }
312
313
    /**
314
     * @param EnumTypeDefinitionNode $node
315
     * @return EnumType
316
     */
317
    protected function buildEnumType(EnumTypeDefinitionNode $node): EnumType
318
    {
319
        return GraphQLEnumType([
320
            'name'        => $node->getNameValue(),
321
            'description' => $node->getDescriptionValue(),
322
            'values'      => $node->hasValues() ? keyValMap(
323
                $node->getValues(),
324
                function (EnumValueDefinitionNode $value): string {
325
                    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...
326
                },
327
                function (EnumValueDefinitionNode $value): array {
328
                    return [
329
                        'description'       => $value->getDescriptionValue(),
330
                        'deprecationReason' => $this->getDeprecationReason($value),
331
                        'astNode'           => $value,
332
                    ];
333
                }
334
            ) : [],
335
            'astNode'     => $node,
336
        ]);
337
    }
338
339
    /**
340
     * @param UnionTypeDefinitionNode $node
341
     * @return UnionType
342
     */
343
    protected function buildUnionType(UnionTypeDefinitionNode $node): UnionType
344
    {
345
        return GraphQLUnionType([
346
            'name'        => $node->getNameValue(),
347
            'description' => $node->getDescriptionValue(),
348
            'types'       => $node->hasTypes() ? \array_map(function (TypeNodeInterface $type) {
349
                return $this->buildType($type);
350
            }, $node->getTypes()) : [],
351
            'astNode'     => $node,
352
        ]);
353
    }
354
355
    /**
356
     * @param ScalarTypeDefinitionNode $node
357
     * @return ScalarType
358
     */
359
    protected function buildScalarType(ScalarTypeDefinitionNode $node): ScalarType
360
    {
361
        return GraphQLScalarType([
362
            'name'        => $node->getNameValue(),
363
            'description' => $node->getDescriptionValue(),
364
            'serialize'   => function ($value) {
365
                return $value;
366
            },
367
            'astNode'     => $node,
368
        ]);
369
    }
370
371
    /**
372
     * @param InputObjectTypeDefinitionNode $node
373
     * @return InputObjectType
374
     */
375
    protected function buildInputObjectType(InputObjectTypeDefinitionNode $node): InputObjectType
376
    {
377
        return GraphQLInputObjectType([
378
            'name'        => $node->getNameValue(),
379
            'description' => $node->getDescriptionValue(),
380
            'fields'      => $node->hasFields() ? function () use ($node) {
381
                return keyValMap(
382
                    $node->getFields(),
383
                    function (InputValueDefinitionNode $value): string {
384
                        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...
385
                    },
386
                    function (InputValueDefinitionNode $value): array {
387
                        $type = $this->buildWrappedType($value->getType());
388
                        return [
389
                            'type'         => $type,
390
                            'description'  => $value->getDescriptionValue(),
391
                            'defaultValue' => $this->valuesResolver->coerceValueFromAST($value->getDefaultValue(),
392
                                $type),
393
                            'astNode'      => $value,
394
                        ];
395
                    }
396
                );
397
            } : [],
398
            'astNode'     => $node,
399
        ]);
400
    }
401
402
    /**
403
     * @inheritdoc
404
     */
405
    protected function resolveType(NamedTypeNode $node): ?NamedTypeInterface
406
    {
407
        return \call_user_func($this->resolveTypeFunction, $node);
408
    }
409
410
    /**
411
     * @param NamedTypeNode $node
412
     * @return NamedTypeInterface|null
413
     * @throws \Psr\SimpleCache\InvalidArgumentException
414
     */
415
    public function defaultTypeResolver(NamedTypeNode $node): ?NamedTypeInterface
416
    {
417
        return $this->cache->get($this->getCacheKey($node->getNameValue())) ?? null;
418
    }
419
420
    /**
421
     * @param string $typeName
422
     * @return TypeDefinitionNodeInterface|null
423
     */
424
    protected function getTypeDefinition(string $typeName): ?TypeDefinitionNodeInterface
425
    {
426
        return $this->typeDefinitionsMap[$typeName] ?? null;
427
    }
428
429
    /**
430
     * @param TypeNodeInterface $typeNode
431
     * @return NamedTypeNode
432
     */
433
    protected function getNamedTypeNode(TypeNodeInterface $typeNode): NamedTypeNode
434
    {
435
        $namedType = $typeNode;
436
437
        while ($namedType instanceof ListTypeNode || $namedType instanceof NonNullTypeNode) {
438
            $namedType = $namedType->getType();
439
        }
440
441
        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...
442
    }
443
444
    /**
445
     * @param NodeInterface|EnumValueDefinitionNode|FieldDefinitionNode $node
446
     * @return null|string
447
     * @throws InvariantException
448
     * @throws ExecutionException
449
     * @throws InvalidTypeException
450
     */
451
    protected function getDeprecationReason(NodeInterface $node): ?string
452
    {
453
        $deprecated = $this->valuesResolver->getDirectiveValues(GraphQLDeprecatedDirective(), $node);
454
        return $deprecated['reason'] ?? null;
455
    }
456
457
    /**
458
     * @param string $key
459
     * @return string
460
     */
461
    protected function getCacheKey(string $key): string
462
    {
463
        return self::CACHE_PREFIX . $key;
464
    }
465
}
466
467
/**
468
 * @param TypeInterface                        $innerType
469
 * @param NamedTypeInterface|TypeNodeInterface $inputTypeNode
470
 * @return TypeInterface
471
 * @throws InvariantException
472
 * @throws InvalidTypeException
473
 */
474
function buildWrappedType(TypeInterface $innerType, TypeNodeInterface $inputTypeNode): TypeInterface
475
{
476
    if ($inputTypeNode instanceof ListTypeNode) {
477
        return GraphQLList(buildWrappedType($innerType, $inputTypeNode->getType()));
478
    }
479
480
    if ($inputTypeNode instanceof NonNullTypeNode) {
481
        $wrappedType = buildWrappedType($innerType, $inputTypeNode->getType());
482
        return GraphQLNonNull(assertNullableType($wrappedType));
483
    }
484
485
    return $innerType;
486
}
487