Passed
Push — master ( 102634...08910a )
by Christoffer
02:20
created

DefinitionBuilder   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 394
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 42
dl 0
loc 394
rs 8.295
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A buildTypes() 0 5 1
C buildNamedType() 0 22 7
A defaultTypeResolver() 0 3 1
A getCachePrefix() 0 3 1
A buildArguments() 0 17 2
A buildType() 0 17 4
A resolveType() 0 3 1
A buildObjectType() 0 14 2
A getNamedTypeNode() 0 9 3
A buildFields() 0 16 2
A buildScalarType() 0 9 1
A buildField() 0 9 2
A __construct() 0 20 2
A buildDirective() 0 10 2
A buildEnumType() 0 19 2
A buildUnionType() 0 9 2
A getDeprecationReason() 0 4 1
B buildInputObjectType() 0 26 3
A buildInterfaceType() 0 9 1
A buildWrappedType() 0 4 1
A getTypeDefinition() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DefinitionBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DefinitionBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Digia\GraphQL\Schema;
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\Language\Node\DirectiveDefinitionNode;
12
use Digia\GraphQL\Language\Node\EnumTypeDefinitionNode;
13
use Digia\GraphQL\Language\Node\EnumValueDefinitionNode;
14
use Digia\GraphQL\Language\Node\FieldDefinitionNode;
15
use Digia\GraphQL\Language\Node\InputObjectTypeDefinitionNode;
16
use Digia\GraphQL\Language\Node\InputValueDefinitionNode;
17
use Digia\GraphQL\Language\Node\InterfaceTypeDefinitionNode;
18
use Digia\GraphQL\Language\Node\ListTypeNode;
19
use Digia\GraphQL\Language\Node\NamedTypeNode;
20
use Digia\GraphQL\Language\Node\NameNode;
21
use Digia\GraphQL\Language\Node\NodeInterface;
22
use Digia\GraphQL\Language\Node\NonNullTypeNode;
23
use Digia\GraphQL\Language\Node\ObjectTypeDefinitionNode;
24
use Digia\GraphQL\Language\Node\ScalarTypeDefinitionNode;
25
use Digia\GraphQL\Language\Node\TypeDefinitionNodeInterface;
26
use Digia\GraphQL\Language\Node\TypeNodeInterface;
27
use Digia\GraphQL\Language\Node\UnionTypeDefinitionNode;
28
use Digia\GraphQL\Type\Definition\DirectiveInterface;
29
use Digia\GraphQL\Type\Definition\EnumType;
30
use Digia\GraphQL\Type\Definition\InputObjectType;
31
use Digia\GraphQL\Type\Definition\InterfaceType;
32
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
33
use Digia\GraphQL\Type\Definition\ObjectType;
34
use Digia\GraphQL\Type\Definition\ScalarType;
35
use Digia\GraphQL\Type\Definition\TypeInterface;
36
use Digia\GraphQL\Type\Definition\UnionType;
37
use Psr\SimpleCache\CacheInterface;
38
use Psr\SimpleCache\InvalidArgumentException;
39
use function Digia\GraphQL\Execution\coerceDirectiveValues;
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
use function Digia\GraphQL\Util\valueFromAST;
55
56
class DefinitionBuilder implements DefinitionBuilderInterface
57
{
58
    use CacheAwareTrait;
59
60
    private const CACHE_PREFIX = 'GraphQL_DefinitionBuilder_';
61
62
    /**
63
     * @var array
64
     */
65
    protected $typeDefinitionsMap;
66
67
    /**
68
     * @var ResolverRegistryInterface
69
     */
70
    protected $resolverRegistry;
71
72
    /**
73
     * @var callable
74
     */
75
    protected $resolveTypeFunction;
76
77
    /**
78
     * DefinitionBuilder constructor.
79
     * @param array                     $typeDefinitionsMap
80
     * @param ResolverRegistryInterface $resolverRegistry
81
     * @param callable|null             $resolveTypeFunction
82
     * @param CacheInterface            $cache
83
     * @throws InvalidArgumentException
84
     */
85
    public function __construct(
86
        array $typeDefinitionsMap,
87
        ResolverRegistryInterface $resolverRegistry,
88
        ?callable $resolveTypeFunction = null,
89
        CacheInterface $cache
90
    ) {
91
        $this->typeDefinitionsMap  = $typeDefinitionsMap;
92
        $this->resolverRegistry    = $resolverRegistry;
93
        $this->cache               = $cache;
94
        $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,Digia\Grap...finitionBuilder|string>. 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...
95
96
        $builtInTypes = keyMap(
97
            \array_merge(specifiedScalarTypes(), introspectionTypes()),
98
            function (NamedTypeInterface $type) {
99
                return $type->getName();
100
            }
101
        );
102
103
        foreach ($builtInTypes as $name => $type) {
104
            $this->setInCache($name, $type);
105
        }
106
    }
107
108
    /**
109
     * @inheritdoc
110
     */
111
    public function buildTypes(array $nodes): array
112
    {
113
        return \array_map(function (NodeInterface $node) {
114
            return $this->buildType($node);
115
        }, $nodes);
116
    }
117
118
    /**
119
     * @param NamedTypeNode|TypeDefinitionNodeInterface $node
120
     * @inheritdoc
121
     */
122
    public function buildType(NodeInterface $node): TypeInterface
123
    {
124
        $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

124
        /** @scrutinizer ignore-call */ 
125
        $typeName = $node->getNameValue();
Loading history...
125
126
        if (!$this->isInCache($typeName)) {
127
            if ($node instanceof NamedTypeNode) {
128
                $definition = $this->getTypeDefinition($typeName);
129
130
                $type = null !== $definition ? $this->buildNamedType($definition) : $this->resolveType($node);
131
132
                $this->setInCache($typeName, $type);
133
            } else {
134
                $this->setInCache($typeName, $this->buildNamedType($node));
135
            }
136
        }
137
138
        return $this->getFromCache($typeName);
139
    }
140
141
    /**
142
     * @inheritdoc
143
     */
144
    public function buildDirective(DirectiveDefinitionNode $node): DirectiveInterface
145
    {
146
        return GraphQLDirective([
147
            'name'        => $node->getNameValue(),
148
            'description' => $node->getDescriptionValue(),
149
            'locations'   => \array_map(function (NameNode $node) {
150
                return $node->getValue();
151
            }, $node->getLocations()),
152
            'args'        => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
153
            'astNode'     => $node,
154
        ]);
155
    }
156
157
    /**
158
     * @inheritdoc
159
     */
160
    public function buildField($node, ?callable $resolve = null): array
161
    {
162
        return [
163
            'type'              => $this->buildWrappedType($node->getType()),
164
            'description'       => $node->getDescriptionValue(),
165
            'args'              => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
166
            'deprecationReason' => $this->getDeprecationReason($node),
167
            'resolve'           => $resolve,
168
            'astNode'           => $node,
169
        ];
170
    }
171
172
    /**
173
     * @param TypeNodeInterface $typeNode
174
     * @return TypeInterface
175
     * @throws InvariantException
176
     * @throws InvalidTypeException
177
     */
178
    protected function buildWrappedType(TypeNodeInterface $typeNode): TypeInterface
179
    {
180
        $typeDefinition = $this->buildType($this->getNamedTypeNode($typeNode));
181
        return buildWrappedType($typeDefinition, $typeNode);
182
    }
183
184
    /**
185
     * @param array $nodes
186
     * @return array
187
     * @throws CoercingException
188
     */
189
    protected function buildArguments(array $nodes): array
190
    {
191
        return keyValueMap(
192
            $nodes,
193
            function (InputValueDefinitionNode $value) {
194
                return $value->getNameValue();
195
            },
196
            function (InputValueDefinitionNode $value): array {
197
                $type         = $this->buildWrappedType($value->getType());
198
                $defaultValue = $value->getDefaultValue();
199
                return [
200
                    'type'         => $type,
201
                    'description'  => $value->getDescriptionValue(),
202
                    'defaultValue' => null !== $defaultValue
203
                        ? valueFromAST($defaultValue, $type)
204
                        : null,
205
                    'astNode'      => $value,
206
                ];
207
            });
208
    }
209
210
    /**
211
     * @param TypeDefinitionNodeInterface $node
212
     * @return NamedTypeInterface
213
     * @throws LanguageException
214
     */
215
    protected function buildNamedType(TypeDefinitionNodeInterface $node): NamedTypeInterface
216
    {
217
        if ($node instanceof ObjectTypeDefinitionNode) {
218
            return $this->buildObjectType($node);
219
        }
220
        if ($node instanceof InterfaceTypeDefinitionNode) {
221
            return $this->buildInterfaceType($node);
222
        }
223
        if ($node instanceof EnumTypeDefinitionNode) {
224
            return $this->buildEnumType($node);
225
        }
226
        if ($node instanceof UnionTypeDefinitionNode) {
227
            return $this->buildUnionType($node);
228
        }
229
        if ($node instanceof ScalarTypeDefinitionNode) {
230
            return $this->buildScalarType($node);
231
        }
232
        if ($node instanceof InputObjectTypeDefinitionNode) {
233
            return $this->buildInputObjectType($node);
234
        }
235
236
        throw new LanguageException(\sprintf('Type kind "%s" not supported.', $node->getKind()));
237
    }
238
239
    /**
240
     * @param ObjectTypeDefinitionNode $node
241
     * @return ObjectType
242
     */
243
    protected function buildObjectType(ObjectTypeDefinitionNode $node): ObjectType
244
    {
245
        return GraphQLObjectType([
246
            'name'        => $node->getNameValue(),
247
            'description' => $node->getDescriptionValue(),
248
            'fields'      => function () use ($node) {
249
                return $this->buildFields($node);
250
            },
251
            'interfaces'  => function () use ($node) {
252
                return $node->hasInterfaces() ? \array_map(function (NodeInterface $interface) {
253
                    return $this->buildType($interface);
254
                }, $node->getInterfaces()) : [];
255
            },
256
            'astNode'     => $node,
257
        ]);
258
    }
259
260
    /**
261
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|InputObjectTypeDefinitionNode $node
262
     * @return array
263
     */
264
    protected function buildFields($node): array
265
    {
266
        return $node->hasFields() ? keyValueMap(
267
            $node->getFields(),
268
            function ($value) {
269
                /** @var FieldDefinitionNode|InputValueDefinitionNode $value */
270
                return $value->getNameValue();
271
            },
272
            function ($value) use ($node) {
273
                /** @var FieldDefinitionNode|InputValueDefinitionNode $value */
274
                return $this->buildField(
275
                    $value,
276
                    $this->resolverRegistry->lookup($node->getNameValue(), $value->getNameValue())
277
                );
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
                                ? valueFromAST($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 = coerceDirectiveValues(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