Completed
Push — master ( df07f9...c6bcad )
by Christoffer
08:53 queued 06:33
created

DefinitionBuilder::buildWrappedType()   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\SchemaBuilder;
4
5
use Digia\GraphQL\Cache\CacheAwareTrait;
6
use Digia\GraphQL\Error\CoercingException;
7
use Digia\GraphQL\Error\ExecutionException;
8
use Digia\GraphQL\Error\InvalidTypeException;
9
use Digia\GraphQL\Error\InvariantException;
10
use Digia\GraphQL\Error\LanguageException;
11
use Digia\GraphQL\Execution\ValuesResolver;
12
use Digia\GraphQL\Language\Node\DirectiveDefinitionNode;
13
use Digia\GraphQL\Language\Node\EnumTypeDefinitionNode;
14
use Digia\GraphQL\Language\Node\EnumValueDefinitionNode;
15
use Digia\GraphQL\Language\Node\FieldDefinitionNode;
16
use Digia\GraphQL\Language\Node\InputObjectTypeDefinitionNode;
17
use Digia\GraphQL\Language\Node\InputValueDefinitionNode;
18
use Digia\GraphQL\Language\Node\InterfaceTypeDefinitionNode;
19
use Digia\GraphQL\Language\Node\ListTypeNode;
20
use Digia\GraphQL\Language\Node\NamedTypeNode;
21
use Digia\GraphQL\Language\Node\NameNode;
22
use Digia\GraphQL\Language\Node\NodeInterface;
23
use Digia\GraphQL\Language\Node\NonNullTypeNode;
24
use Digia\GraphQL\Language\Node\ObjectTypeDefinitionNode;
25
use Digia\GraphQL\Language\Node\ScalarTypeDefinitionNode;
26
use Digia\GraphQL\Language\Node\TypeDefinitionNodeInterface;
27
use Digia\GraphQL\Language\Node\TypeNodeInterface;
28
use Digia\GraphQL\Language\Node\UnionTypeDefinitionNode;
29
use Digia\GraphQL\Type\Definition\DirectiveInterface;
30
use Digia\GraphQL\Type\Definition\EnumType;
31
use Digia\GraphQL\Type\Definition\InputObjectType;
32
use Digia\GraphQL\Type\Definition\InterfaceType;
33
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
34
use Digia\GraphQL\Type\Definition\ObjectType;
35
use Digia\GraphQL\Type\Definition\ScalarType;
36
use Digia\GraphQL\Type\Definition\TypeInterface;
37
use Digia\GraphQL\Type\Definition\UnionType;
38
use Psr\SimpleCache\CacheInterface;
39
use function Digia\GraphQL\Type\assertNullableType;
40
use function Digia\GraphQL\Type\GraphQLDirective;
41
use function Digia\GraphQL\Type\GraphQLEnumType;
42
use function Digia\GraphQL\Type\GraphQLInputObjectType;
43
use function Digia\GraphQL\Type\GraphQLInterfaceType;
44
use function Digia\GraphQL\Type\GraphQLList;
45
use function Digia\GraphQL\Type\GraphQLNonNull;
46
use function Digia\GraphQL\Type\GraphQLObjectType;
47
use function Digia\GraphQL\Type\GraphQLScalarType;
48
use function Digia\GraphQL\Type\GraphQLUnionType;
49
use function Digia\GraphQL\Type\introspectionTypes;
50
use function Digia\GraphQL\Type\specifiedScalarTypes;
51
use function Digia\GraphQL\Util\keyMap;
52
use function Digia\GraphQL\Util\keyValueMap;
53
54
class DefinitionBuilder implements DefinitionBuilderInterface
55
{
56
    use CacheAwareTrait;
57
58
    private const CACHE_PREFIX = 'GraphQL_DefinitionBuilder_';
59
60
    /**
61
     * @var ValuesResolver
62
     */
63
    protected $valuesResolver;
64
65
    /**
66
     * @var callable
67
     */
68
    protected $resolveTypeFunction;
69
70
    /**
71
     * @var array
72
     */
73
    protected $resolverMap;
74
75
    /**
76
     * @var array
77
     */
78
    protected $typeDefinitionsMap;
79
80
    /**
81
     * DefinitionBuilder constructor.
82
     *
83
     * @param callable       $resolveTypeFunction
84
     * @param CacheInterface $cache
85
     * @throws \Psr\SimpleCache\InvalidArgumentException
86
     */
87
    public function __construct(
88
        CacheInterface $cache,
89
        ValuesResolver $valuesResolver,
90
        ?callable $resolveTypeFunction = null
91
    ) {
92
        $this->valuesResolver      = $valuesResolver;
93
        $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...
94
        $this->typeDefinitionsMap  = [];
95
96
        $builtInTypes = keyMap(
97
            \array_merge(specifiedScalarTypes(), introspectionTypes()),
98
            function (NamedTypeInterface $type) {
99
                return $type->getName();
100
            }
101
        );
102
103
        $this->cache = $cache;
104
105
        foreach ($builtInTypes as $name => $type) {
106
            $this->setInCache($name, $type);
107
        }
108
    }
109
110
    /**
111
     * @inheritdoc
112
     */
113
    public function setTypeDefinitionMap(array $typeDefinitionMap): DefinitionBuilder
114
    {
115
        $this->typeDefinitionsMap = $typeDefinitionMap;
116
        return $this;
117
    }
118
119
    /**
120
     * @param array $resolverMap
121
     * @return DefinitionBuilder
122
     */
123
    public function setResolverMap(array $resolverMap): DefinitionBuilder
124
    {
125
        $this->resolverMap = $resolverMap;
126
        return $this;
127
    }
128
129
    /**
130
     * @param NamedTypeNode|TypeDefinitionNodeInterface $node
131
     * @inheritdoc
132
     */
133
    public function buildType(NodeInterface $node): TypeInterface
134
    {
135
        $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

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

193
            'args'              => $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...
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

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