Completed
Pull Request — master (#116)
by Christoffer
02:29
created

DefinitionBuilder::setResolverMap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
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 array
64
     */
65
    protected $typeDefinitionsMap;
66
67
    /**
68
     * @var array
69
     */
70
    protected $resolverMap;
71
72
    /**
73
     * @var callable
74
     */
75
    protected $resolveTypeFunction;
76
77
    /**
78
     * DefinitionBuilder constructor.
79
     *
80
     * @param callable       $resolveTypeFunction
81
     * @param CacheInterface $cache
82
     * @throws \Psr\SimpleCache\InvalidArgumentException
83
     */
84
    public function __construct(CacheInterface $cache, ?callable $resolveTypeFunction = null)
85
    {
86
        $this->typeDefinitionsMap  = [];
87
        $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...
88
89
        $builtInTypes = keyMap(
90
            array_merge(specifiedScalarTypes(), introspectionTypes()),
91
            function (NamedTypeInterface $type) {
92
                return $type->getName();
93
            }
94
        );
95
96
        foreach ($builtInTypes as $name => $type) {
97
            $cache->set($this->getCacheKey($name), $type);
98
        }
99
100
        $this->cache = $cache;
101
    }
102
103
    /**
104
     * @inheritdoc
105
     */
106
    public function setTypeDefinitionMap(array $typeDefinitionMap): DefinitionBuilder
107
    {
108
        $this->typeDefinitionsMap = $typeDefinitionMap;
109
        return $this;
110
    }
111
112
    /**
113
     * @param array $resolverMap
114
     * @return DefinitionBuilder
115
     */
116
    public function setResolverMap(array $resolverMap): DefinitionBuilder
117
    {
118
        $this->resolverMap = $resolverMap;
119
        return $this;
120
    }
121
122
    /**
123
     * @param NamedTypeNode|TypeDefinitionNodeInterface $node
124
     * @inheritdoc
125
     */
126
    public function buildType(NodeInterface $node): TypeInterface
127
    {
128
        $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

128
        /** @scrutinizer ignore-call */ 
129
        $typeName = $node->getNameValue();
Loading history...
129
130
        if (!$this->cache->has($this->getCacheKey($typeName))) {
131
            if ($node instanceof NamedTypeNode) {
132
                $definition = $this->getTypeDefinition($typeName);
133
134
                $this->cache->set(
135
                    $this->getCacheKey($typeName),
136
                    null !== $definition ? $this->buildNamedType($definition) : $this->resolveType($node)
137
                );
138
            } else {
139
                $this->cache->set($this->getCacheKey($typeName), $this->buildNamedType($node));
140
            }
141
        }
142
143
        return $this->cache->get($this->getCacheKey($typeName));
144
    }
145
146
    /**
147
     * @inheritdoc
148
     */
149
    public function buildDirective(DirectiveDefinitionNode $node): DirectiveInterface
150
    {
151
        return GraphQLDirective([
152
            'name'        => $node->getNameValue(),
153
            'description' => $node->getDescriptionValue(),
154
            'locations'   => array_map(function (NameNode $node) {
155
                return $node->getValue();
156
            }, $node->getLocations()),
157
            'arguments'   => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
158
            'astNode'     => $node,
159
        ]);
160
    }
161
162
    /**
163
     * @param TypeNodeInterface $typeNode
164
     * @return TypeInterface
165
     * @throws InvariantException
166
     * @throws InvalidTypeException
167
     */
168
    protected function buildWrappedType(TypeNodeInterface $typeNode): TypeInterface
169
    {
170
        $typeDefinition = $this->buildType($this->getNamedTypeNode($typeNode));
171
        return buildWrappedType($typeDefinition, $typeNode);
172
    }
173
174
    /**
175
     * @param FieldDefinitionNode|InputValueDefinitionNode $node
176
     * @return array
177
     * @throws ExecutionException
178
     * @throws InvalidTypeException
179
     * @throws InvariantException
180
     */
181
    protected function buildField($node, array $resolverMap): array
182
    {
183
        return [
184
            'type'              => $this->buildWrappedType($node->getType()),
185
            'description'       => $node->getDescriptionValue(),
186
            '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

186
            '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

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