Passed
Pull Request — master (#124)
by Christoffer
02:15
created

DefinitionBuilder::buildField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 2
1
<?php
2
3
namespace Digia\GraphQL\SchemaBuilder;
4
5
use Digia\GraphQL\Error\ExecutionException;
6
use Digia\GraphQL\Error\InvalidTypeException;
7
use Digia\GraphQL\Error\InvariantException;
8
use Digia\GraphQL\Error\LanguageException;
9
use Digia\GraphQL\Execution\ValuesResolver;
10
use Digia\GraphQL\Language\Node\DirectiveDefinitionNode;
11
use Digia\GraphQL\Language\Node\EnumTypeDefinitionNode;
12
use Digia\GraphQL\Language\Node\EnumValueDefinitionNode;
13
use Digia\GraphQL\Language\Node\FieldDefinitionNode;
14
use Digia\GraphQL\Language\Node\InputObjectTypeDefinitionNode;
15
use Digia\GraphQL\Language\Node\InputValueDefinitionNode;
16
use Digia\GraphQL\Language\Node\InterfaceTypeDefinitionNode;
17
use Digia\GraphQL\Language\Node\ListTypeNode;
18
use Digia\GraphQL\Language\Node\NamedTypeNode;
19
use Digia\GraphQL\Language\Node\NameNode;
20
use Digia\GraphQL\Language\Node\NodeInterface;
21
use Digia\GraphQL\Language\Node\NonNullTypeNode;
22
use Digia\GraphQL\Language\Node\ObjectTypeDefinitionNode;
23
use Digia\GraphQL\Language\Node\ScalarTypeDefinitionNode;
24
use Digia\GraphQL\Language\Node\TypeDefinitionNodeInterface;
25
use Digia\GraphQL\Language\Node\TypeNodeInterface;
26
use Digia\GraphQL\Language\Node\UnionTypeDefinitionNode;
27
use Digia\GraphQL\Type\Definition\DirectiveInterface;
28
use Digia\GraphQL\Type\Definition\EnumType;
29
use Digia\GraphQL\Type\Definition\InputObjectType;
30
use Digia\GraphQL\Type\Definition\InterfaceType;
31
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
32
use Digia\GraphQL\Type\Definition\ObjectType;
33
use Digia\GraphQL\Type\Definition\ScalarType;
34
use Digia\GraphQL\Type\Definition\TypeInterface;
35
use Digia\GraphQL\Type\Definition\UnionType;
36
use Psr\SimpleCache\CacheInterface;
37
use function Digia\GraphQL\Language\valueFromAST;
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
                $this->cache->set(
144
                    $this->getCacheKey($typeName),
145
                    null !== $definition ? $this->buildNamedType($definition) : $this->resolveType($node)
146
                );
147
            } else {
148
                $this->cache->set($this->getCacheKey($typeName), $this->buildNamedType($node));
149
            }
150
        }
151
152
        return $this->cache->get($this->getCacheKey($typeName));
153
    }
154
155
    /**
156
     * @inheritdoc
157
     */
158
    public function buildDirective(DirectiveDefinitionNode $node): DirectiveInterface
159
    {
160
        return GraphQLDirective([
161
            'name'        => $node->getNameValue(),
162
            'description' => $node->getDescriptionValue(),
163
            'locations'   => array_map(function (NameNode $node) {
164
                return $node->getValue();
165
            }, $node->getLocations()),
166
            'arguments'   => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
167
            'astNode'     => $node,
168
        ]);
169
    }
170
171
    /**
172
     * @param TypeNodeInterface $typeNode
173
     * @return TypeInterface
174
     * @throws InvariantException
175
     * @throws InvalidTypeException
176
     */
177
    protected function buildWrappedType(TypeNodeInterface $typeNode): TypeInterface
178
    {
179
        $typeDefinition = $this->buildType($this->getNamedTypeNode($typeNode));
180
        return buildWrappedType($typeDefinition, $typeNode);
181
    }
182
183
    /**
184
     * @param FieldDefinitionNode|InputValueDefinitionNode $node
185
     * @return array
186
     * @throws ExecutionException
187
     * @throws InvalidTypeException
188
     * @throws InvariantException
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
     */
206
    protected function buildArguments(array $nodes): array
207
    {
208
        return keyValMap(
209
            $nodes,
210
            function (InputValueDefinitionNode $value) {
211
                return $value->getNameValue();
212
            },
213
            function (InputValueDefinitionNode $value): array {
214
                $type = $this->buildWrappedType($value->getType());
215
                return [
216
                    'type'         => $type,
217
                    'description'  => $value->getDescriptionValue(),
218
                    'defaultValue' => valueFromAST($value->getDefaultValue(), $type),
219
                    'astNode'      => $value,
220
                ];
221
            });
222
    }
223
224
    /**
225
     * @param TypeDefinitionNodeInterface $node
226
     * @return NamedTypeInterface
227
     * @throws LanguageException
228
     */
229
    protected function buildNamedType(TypeDefinitionNodeInterface $node): NamedTypeInterface
230
    {
231
        if ($node instanceof ObjectTypeDefinitionNode) {
232
            return $this->buildObjectType($node);
233
        }
234
        if ($node instanceof InterfaceTypeDefinitionNode) {
235
            return $this->buildInterfaceType($node);
236
        }
237
        if ($node instanceof EnumTypeDefinitionNode) {
238
            return $this->buildEnumType($node);
239
        }
240
        if ($node instanceof UnionTypeDefinitionNode) {
241
            return $this->buildUnionType($node);
242
        }
243
        if ($node instanceof ScalarTypeDefinitionNode) {
244
            return $this->buildScalarType($node);
245
        }
246
        if ($node instanceof InputObjectTypeDefinitionNode) {
247
            return $this->buildInputObjectType($node);
248
        }
249
250
        throw new LanguageException(sprintf('Type kind "%s" not supported.', $node->getKind()));
251
    }
252
253
    /**
254
     * @param ObjectTypeDefinitionNode $node
255
     * @return ObjectType
256
     */
257
    protected function buildObjectType(ObjectTypeDefinitionNode $node): ObjectType
258
    {
259
        return GraphQLObjectType([
260
            'name'        => $node->getNameValue(),
261
            'description' => $node->getDescriptionValue(),
262
            'fields'      => function () use ($node) {
263
                return $this->buildFields($node);
264
            },
265
            'interfaces'  => function () use ($node) {
266
                return $node->hasInterfaces() ? array_map(function (NodeInterface $interface) {
267
                    return $this->buildType($interface);
268
                }, $node->getInterfaces()) : [];
269
            },
270
        ]);
271
    }
272
273
    /**
274
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|InputObjectTypeDefinitionNode $node
275
     * @return array
276
     */
277
    protected function buildFields($node): array
278
    {
279
        $resolverMap = $this->resolverMap[$node->getNameValue()] ?? [];
280
281
        return $node->hasFields() ? keyValMap(
282
            $node->getFields(),
283
            function ($value) {
284
                /** @noinspection PhpUndefinedMethodInspection */
285
                return $value->getNameValue();
286
            },
287
            function ($value) use ($resolverMap) {
288
                return $this->buildField($value, $resolverMap);
289
            }
290
        ) : [];
291
    }
292
293
    /**
294
     * @param InterfaceTypeDefinitionNode $node
295
     * @return InterfaceType
296
     */
297
    protected function buildInterfaceType(InterfaceTypeDefinitionNode $node): InterfaceType
298
    {
299
        return GraphQLInterfaceType([
300
            'name'        => $node->getNameValue(),
301
            'description' => $node->getDescriptionValue(),
302
            'fields'      => function () use ($node): array {
303
                return $this->buildFields($node);
304
            },
305
            'astNode'     => $node,
306
        ]);
307
    }
308
309
    /**
310
     * @param EnumTypeDefinitionNode $node
311
     * @return EnumType
312
     */
313
    protected function buildEnumType(EnumTypeDefinitionNode $node): EnumType
314
    {
315
        return GraphQLEnumType([
316
            'name'        => $node->getNameValue(),
317
            'description' => $node->getDescriptionValue(),
318
            'values'      => $node->hasValues() ? keyValMap(
319
                $node->getValues(),
320
                function (EnumValueDefinitionNode $value): string {
321
                    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...
322
                },
323
                function (EnumValueDefinitionNode $value): array {
324
                    return [
325
                        'description'       => $value->getDescriptionValue(),
326
                        'deprecationReason' => $this->getDeprecationReason($value),
327
                        'astNode'           => $value,
328
                    ];
329
                }
330
            ) : [],
331
            'astNode'     => $node,
332
        ]);
333
    }
334
335
    /**
336
     * @param UnionTypeDefinitionNode $node
337
     * @return UnionType
338
     */
339
    protected function buildUnionType(UnionTypeDefinitionNode $node): UnionType
340
    {
341
        return GraphQLUnionType([
342
            'name'        => $node->getNameValue(),
343
            'description' => $node->getDescriptionValue(),
344
            'types'       => $node->hasTypes() ? array_map(function (TypeNodeInterface $type) {
345
                return $this->buildType($type);
346
            }, $node->getTypes()) : [],
347
            'astNode'     => $node,
348
        ]);
349
    }
350
351
    /**
352
     * @param ScalarTypeDefinitionNode $node
353
     * @return ScalarType
354
     */
355
    protected function buildScalarType(ScalarTypeDefinitionNode $node): ScalarType
356
    {
357
        return GraphQLScalarType([
358
            'name'        => $node->getNameValue(),
359
            'description' => $node->getDescriptionValue(),
360
            'serialize'   => function ($value) {
361
                return $value;
362
            },
363
            'astNode'     => $node,
364
        ]);
365
    }
366
367
    /**
368
     * @param InputObjectTypeDefinitionNode $node
369
     * @return InputObjectType
370
     */
371
    protected function buildInputObjectType(InputObjectTypeDefinitionNode $node): InputObjectType
372
    {
373
        return GraphQLInputObjectType([
374
            'name'        => $node->getNameValue(),
375
            'description' => $node->getDescriptionValue(),
376
            'fields'      => $node->hasFields() ? function () use ($node) {
377
                return keyValMap(
378
                    $node->getFields(),
379
                    function (InputValueDefinitionNode $value): string {
380
                        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...
381
                    },
382
                    function (InputValueDefinitionNode $value): array {
383
                        $type = $this->buildWrappedType($value->getType());
384
                        return [
385
                            'type'         => $type,
386
                            'description'  => $value->getDescriptionValue(),
387
                            'defaultValue' => valueFromAST($value->getDefaultValue(), $type),
388
                            'astNode'      => $value,
389
                        ];
390
                    }
391
                );
392
            } : [],
393
            'astNode'     => $node,
394
        ]);
395
    }
396
397
    /**
398
     * @inheritdoc
399
     */
400
    protected function resolveType(NamedTypeNode $node): ?NamedTypeInterface
401
    {
402
        return \call_user_func($this->resolveTypeFunction, $node);
403
    }
404
405
    /**
406
     * @param NamedTypeNode $node
407
     * @return NamedTypeInterface|null
408
     * @throws \Psr\SimpleCache\InvalidArgumentException
409
     */
410
    public function defaultTypeResolver(NamedTypeNode $node): ?NamedTypeInterface
411
    {
412
        return $this->cache->get($this->getCacheKey($node->getNameValue())) ?? null;
413
    }
414
415
    /**
416
     * @param string $typeName
417
     * @return TypeDefinitionNodeInterface|null
418
     */
419
    protected function getTypeDefinition(string $typeName): ?TypeDefinitionNodeInterface
420
    {
421
        return $this->typeDefinitionsMap[$typeName] ?? null;
422
    }
423
424
    /**
425
     * @param TypeNodeInterface $typeNode
426
     * @return NamedTypeNode
427
     */
428
    protected function getNamedTypeNode(TypeNodeInterface $typeNode): NamedTypeNode
429
    {
430
        $namedType = $typeNode;
431
432
        while ($namedType instanceof ListTypeNode || $namedType instanceof NonNullTypeNode) {
433
            $namedType = $namedType->getType();
434
        }
435
436
        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...
437
    }
438
439
    /**
440
     * @param NodeInterface|EnumValueDefinitionNode|FieldDefinitionNode $node
441
     * @return null|string
442
     * @throws InvariantException
443
     * @throws ExecutionException
444
     * @throws InvalidTypeException
445
     */
446
    protected function getDeprecationReason(NodeInterface $node): ?string
447
    {
448
        $deprecated = $this->valuesResolver->getDirectiveValues(GraphQLDeprecatedDirective(), $node);
449
        return $deprecated['reason'] ?? null;
450
    }
451
452
    /**
453
     * @param string $key
454
     * @return string
455
     */
456
    protected function getCacheKey(string $key): string
457
    {
458
        return self::CACHE_PREFIX . $key;
459
    }
460
}
461
462
/**
463
 * @param TypeInterface                        $innerType
464
 * @param NamedTypeInterface|TypeNodeInterface $inputTypeNode
465
 * @return TypeInterface
466
 * @throws InvariantException
467
 * @throws InvalidTypeException
468
 */
469
function buildWrappedType(TypeInterface $innerType, TypeNodeInterface $inputTypeNode): TypeInterface
470
{
471
    if ($inputTypeNode instanceof ListTypeNode) {
472
        return GraphQLList(buildWrappedType($innerType, $inputTypeNode->getType()));
473
    }
474
475
    if ($inputTypeNode instanceof NonNullTypeNode) {
476
        $wrappedType = buildWrappedType($innerType, $inputTypeNode->getType());
477
        return GraphQLNonNull(assertNullableType($wrappedType));
478
    }
479
480
    return $innerType;
481
}
482