Completed
Push — master ( 30514f...e0ab72 )
by Christoffer
02:37
created

DefinitionBuilder::buildObjectType()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 14
Code Lines 10

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 10
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 Psr\SimpleCache\InvalidArgumentException;
40
use function Digia\GraphQL\Type\assertNullableType;
41
use function Digia\GraphQL\Type\GraphQLDirective;
42
use function Digia\GraphQL\Type\GraphQLEnumType;
43
use function Digia\GraphQL\Type\GraphQLInputObjectType;
44
use function Digia\GraphQL\Type\GraphQLInterfaceType;
45
use function Digia\GraphQL\Type\GraphQLList;
46
use function Digia\GraphQL\Type\GraphQLNonNull;
47
use function Digia\GraphQL\Type\GraphQLObjectType;
48
use function Digia\GraphQL\Type\GraphQLScalarType;
49
use function Digia\GraphQL\Type\GraphQLUnionType;
50
use function Digia\GraphQL\Type\introspectionTypes;
51
use function Digia\GraphQL\Type\specifiedScalarTypes;
52
use function Digia\GraphQL\Util\keyMap;
53
use function Digia\GraphQL\Util\keyValueMap;
54
55
class DefinitionBuilder implements DefinitionBuilderInterface
56
{
57
    use CacheAwareTrait;
58
59
    private const CACHE_PREFIX = 'GraphQL_DefinitionBuilder_';
60
61
    /**
62
     * @var array
63
     */
64
    protected $typeDefinitionsMap;
65
66
    /**
67
     * @var array
68
     */
69
    protected $resolverMap;
70
71
    /**
72
     * @var callable
73
     */
74
    protected $resolveTypeFunction;
75
76
    /**
77
     * @var ValuesResolver
78
     */
79
    protected $valuesResolver;
80
81
    /**
82
     * DefinitionBuilder constructor.
83
     * @param array          $typeDefinitionsMap
84
     * @param array          $resolverMap
85
     * @param CacheInterface $cache
86
     * @param ValuesResolver $valuesResolver
87
     * @param callable|null  $resolveTypeFunction
88
     * @throws InvalidArgumentException
89
     */
90
    public function __construct(
91
        array $typeDefinitionsMap,
92
        array $resolverMap = [],
93
        ?callable $resolveTypeFunction = null,
94
        CacheInterface $cache,
95
        ValuesResolver $valuesResolver
96
    ) {
97
        $this->typeDefinitionsMap  = $typeDefinitionsMap;
98
        $this->resolverMap         = $resolverMap;
99
        $this->cache               = $cache;
100
        $this->valuesResolver      = $valuesResolver;
101
        $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...
102
103
        $builtInTypes = keyMap(
104
            \array_merge(specifiedScalarTypes(), introspectionTypes()),
105
            function (NamedTypeInterface $type) {
106
                return $type->getName();
107
            }
108
        );
109
110
        foreach ($builtInTypes as $name => $type) {
111
            $this->setInCache($name, $type);
112
        }
113
    }
114
115
    /**
116
     * @param NamedTypeNode|TypeDefinitionNodeInterface $node
117
     * @inheritdoc
118
     */
119
    public function buildType(NodeInterface $node): TypeInterface
120
    {
121
        $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

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

179
            '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...
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

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