Completed
Pull Request — master (#119)
by Quang
05:19
created

DefinitionBuilder::buildFields()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
1
<?php
2
3
namespace Digia\GraphQL\Language\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() ? keyValMap(
377
                $node->getFields(),
378
                function (InputValueDefinitionNode $value): string {
379
                    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...
380
                },
381
                function (InputValueDefinitionNode $value): array {
382
                    $type = $this->buildWrappedType($value->getType());
383
                    return [
384
                        'type'         => $type,
385
                        'description'  => $value->getDescriptionValue(),
386
                        'defaultValue' => valueFromAST($value->getDefaultValue(), $type),
387
                        'astNode'      => $value,
388
                    ];
389
                }) : [],
390
            'astNode'     => $node,
391
        ]);
392
    }
393
394
    /**
395
     * @inheritdoc
396
     */
397
    protected function resolveType(NamedTypeNode $node): ?NamedTypeInterface
398
    {
399
        return \call_user_func($this->resolveTypeFunction, $node);
400
    }
401
402
    /**
403
     * @param NamedTypeNode $node
404
     * @return NamedTypeInterface|null
405
     * @throws \Psr\SimpleCache\InvalidArgumentException
406
     */
407
    public function defaultTypeResolver(NamedTypeNode $node): ?NamedTypeInterface
408
    {
409
        return $this->cache->get($this->getCacheKey($node->getNameValue())) ?? null;
410
    }
411
412
    /**
413
     * @param string $typeName
414
     * @return TypeDefinitionNodeInterface|null
415
     */
416
    protected function getTypeDefinition(string $typeName): ?TypeDefinitionNodeInterface
417
    {
418
        return $this->typeDefinitionsMap[$typeName] ?? null;
419
    }
420
421
    /**
422
     * @param TypeNodeInterface $typeNode
423
     * @return NamedTypeNode
424
     */
425
    protected function getNamedTypeNode(TypeNodeInterface $typeNode): NamedTypeNode
426
    {
427
        $namedType = $typeNode;
428
429
        while ($namedType instanceof ListTypeNode || $namedType instanceof NonNullTypeNode) {
430
            $namedType = $namedType->getType();
431
        }
432
433
        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...
434
    }
435
436
    /**
437
     * @param NodeInterface|EnumValueDefinitionNode|FieldDefinitionNode $node
438
     * @return null|string
439
     * @throws InvariantException
440
     * @throws ExecutionException
441
     * @throws InvalidTypeException
442
     */
443
    protected function getDeprecationReason(NodeInterface $node): ?string
444
    {
445
        $deprecated = $this->valuesResolver->getDirectiveValues(GraphQLDeprecatedDirective(), $node);
446
        return $deprecated['reason'] ?? null;
447
    }
448
449
    /**
450
     * @param string $key
451
     * @return string
452
     */
453
    protected function getCacheKey(string $key): string
454
    {
455
        return self::CACHE_PREFIX . $key;
456
    }
457
}
458
459
/**
460
 * @param TypeInterface                        $innerType
461
 * @param NamedTypeInterface|TypeNodeInterface $inputTypeNode
462
 * @return TypeInterface
463
 * @throws InvariantException
464
 * @throws InvalidTypeException
465
 */
466
function buildWrappedType(TypeInterface $innerType, TypeNodeInterface $inputTypeNode): TypeInterface
467
{
468
    if ($inputTypeNode instanceof ListTypeNode) {
469
        return GraphQLList(buildWrappedType($innerType, $inputTypeNode->getType()));
470
    }
471
    if ($inputTypeNode instanceof NonNullTypeNode) {
472
        $wrappedType = buildWrappedType($innerType, $inputTypeNode->getType());
473
        return GraphQLNonNull(assertNullableType($wrappedType));
474
    }
475
476
    return $innerType;
477
}
478