Completed
Pull Request — master (#80)
by Christoffer
02:18
created

DefinitionBuilder   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 34
dl 0
loc 324
rs 9.2
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A buildInterfaceType() 0 9 1
C buildNamedType() 0 22 7
A buildFields() 0 12 2
A setTypeDefinitionMap() 0 4 1
A buildField() 0 8 2
A buildArguments() 0 14 1
A buildInputObjectType() 0 20 2
A buildScalarType() 0 9 1
A buildWrappedType() 0 4 1
A buildEnumType() 0 19 2
A __construct() 0 17 2
A resolveType() 0 3 1
A getTypeDefinition() 0 3 1
A buildType() 0 18 4
A buildDirective() 0 10 2
A buildObjectType() 0 12 2
A buildUnionType() 0 9 2
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
56
    /**
57
     * @var ?callable
58
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment ?callable at position 0 could not be parsed: Unknown type name '?callable' at position 0 in ?callable.
Loading history...
59
    protected $resolveTypeFunction;
60
61
    /**
62
     * @var CacheInterface
63
     */
64
    protected $cache;
65
66
    /**
67
     * @var array
68
     */
69
    protected $typeDefinitionsMap;
70
71
    /**
72
     * DefinitionBuilder constructor.
73
     *
74
     * @param callable       $resolveTypeFunction
75
     * @param CacheInterface $cache
76
     * @throws \Psr\SimpleCache\InvalidArgumentException
77
     */
78
    public function __construct(callable $resolveTypeFunction, CacheInterface $cache)
79
    {
80
        $this->typeDefinitionsMap  = [];
81
        $this->resolveTypeFunction = $resolveTypeFunction;
82
83
        $builtInTypes = keyMap(
84
            array_merge(specifiedScalarTypes(), introspectionTypes()),
85
            function (NamedTypeInterface $type) {
86
                return $type->getName();
87
            }
88
        );
89
90
        foreach ($builtInTypes as $name => $type) {
91
            $cache->set($name, $type);
92
        }
93
94
        $this->cache = $cache;
95
    }
96
97
    /**
98
     * @inheritdoc
99
     */
100
    public function setTypeDefinitionMap(array $typeDefinitionMap)
101
    {
102
        $this->typeDefinitionsMap = $typeDefinitionMap;
103
        return $this;
104
    }
105
106
    /**
107
     * @param NamedTypeNode|TypeDefinitionNodeInterface $node
108
     * @inheritdoc
109
     */
110
    public function buildType(NodeInterface $node): TypeInterface
111
    {
112
        $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\Node\EnumTypeDefinitionNode 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

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

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

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