Completed
Pull Request — master (#187)
by Christoffer
04:45 queued 02:07
created

DefinitionBuilder::getFieldResolver()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 2
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\Schema\Resolver\ResolverRegistryInterface;
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\Execution\coerceDirectiveValues;
41
use function Digia\GraphQL\Type\assertNullableType;
42
use function Digia\GraphQL\Type\introspectionTypes;
43
use function Digia\GraphQL\Type\newDirective;
44
use function Digia\GraphQL\Type\newEnumType;
45
use function Digia\GraphQL\Type\newInputObjectType;
46
use function Digia\GraphQL\Type\newInterfaceType;
47
use function Digia\GraphQL\Type\newList;
48
use function Digia\GraphQL\Type\newNonNull;
49
use function Digia\GraphQL\Type\newObjectType;
50
use function Digia\GraphQL\Type\newScalarType;
51
use function Digia\GraphQL\Type\newUnionType;
52
use function Digia\GraphQL\Type\specifiedScalarTypes;
53
use function Digia\GraphQL\Util\keyMap;
54
use function Digia\GraphQL\Util\keyValueMap;
55
use function Digia\GraphQL\Util\valueFromAST;
56
57
class DefinitionBuilder implements DefinitionBuilderInterface
58
{
59
    use CacheAwareTrait;
60
61
    private const CACHE_PREFIX = 'GraphQL_DefinitionBuilder_';
62
63
    /**
64
     * @var array
65
     */
66
    protected $typeDefinitionsMap;
67
68
    /**
69
     * @var ResolverRegistryInterface
70
     */
71
    protected $resolverRegistry;
72
73
    /**
74
     * @var callable
75
     */
76
    protected $resolveTypeFunction;
77
78
    /**
79
     * DefinitionBuilder constructor.
80
     * @param array                          $typeDefinitionsMap
81
     * @param ResolverRegistryInterface|null $resolverRegistry
82
     * @param callable|null                  $resolveTypeFunction
83
     * @param CacheInterface                 $cache
84
     * @throws InvalidArgumentException
85
     */
86
    public function __construct(
87
        array $typeDefinitionsMap,
88
        ?ResolverRegistryInterface $resolverRegistry = null,
89
        ?callable $resolveTypeFunction = null,
90
        CacheInterface $cache
91
    ) {
92
        $this->typeDefinitionsMap  = $typeDefinitionsMap;
93
        $this->resolverRegistry    = $resolverRegistry;
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
        $this->cache               = $cache;
96
97
        $builtInTypes = keyMap(
98
            \array_merge(specifiedScalarTypes(), introspectionTypes()),
99
            function (NamedTypeInterface $type) {
100
                return $type->getName();
101
            }
102
        );
103
104
        foreach ($builtInTypes as $name => $type) {
105
            $this->setInCache($name, $type);
106
        }
107
    }
108
109
    /**
110
     * @inheritdoc
111
     */
112
    public function buildTypes(array $nodes): array
113
    {
114
        return \array_map(function (NodeInterface $node) {
115
            return $this->buildType($node);
116
        }, $nodes);
117
    }
118
119
    /**
120
     * @param NamedTypeNode|TypeDefinitionNodeInterface $node
121
     * @inheritdoc
122
     */
123
    public function buildType(NodeInterface $node): TypeInterface
124
    {
125
        $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

125
        /** @scrutinizer ignore-call */ 
126
        $typeName = $node->getNameValue();
Loading history...
126
127
        if (!$this->isInCache($typeName)) {
128
            if ($node instanceof NamedTypeNode) {
129
                $definition = $this->getTypeDefinition($typeName);
130
131
                $type = null !== $definition ? $this->buildNamedType($definition) : $this->resolveType($node);
132
133
                $this->setInCache($typeName, $type);
134
            } else {
135
                $this->setInCache($typeName, $this->buildNamedType($node));
136
            }
137
        }
138
139
        return $this->getFromCache($typeName);
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function buildDirective(DirectiveDefinitionNode $node): DirectiveInterface
146
    {
147
        return newDirective([
148
            'name'        => $node->getNameValue(),
149
            'description' => $node->getDescriptionValue(),
150
            'locations'   => \array_map(function (NameNode $node) {
151
                return $node->getValue();
152
            }, $node->getLocations()),
153
            'args'        => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
154
            'astNode'     => $node,
155
        ]);
156
    }
157
158
    /**
159
     * @inheritdoc
160
     */
161
    public function buildField($node, ?callable $resolve = null): array
162
    {
163
        return [
164
            'type'              => $this->buildWrappedType($node->getType()),
165
            'description'       => $node->getDescriptionValue(),
166
            'args'              => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
167
            'deprecationReason' => $this->getDeprecationReason($node),
168
            'resolve'           => $resolve,
169
            'astNode'           => $node,
170
        ];
171
    }
172
173
    /**
174
     * @param TypeNodeInterface $typeNode
175
     * @return TypeInterface
176
     * @throws InvariantException
177
     * @throws InvalidTypeException
178
     */
179
    protected function buildWrappedType(TypeNodeInterface $typeNode): TypeInterface
180
    {
181
        $typeDefinition = $this->buildType($this->getNamedTypeNode($typeNode));
182
        return buildWrappedType($typeDefinition, $typeNode);
183
    }
184
185
    /**
186
     * @param array $nodes
187
     * @return array
188
     * @throws CoercingException
189
     */
190
    protected function buildArguments(array $nodes): array
191
    {
192
        return keyValueMap(
193
            $nodes,
194
            function (InputValueDefinitionNode $value) {
195
                return $value->getNameValue();
196
            },
197
            function (InputValueDefinitionNode $value): array {
198
                $type         = $this->buildWrappedType($value->getType());
199
                $defaultValue = $value->getDefaultValue();
200
                return [
201
                    'type'         => $type,
202
                    'description'  => $value->getDescriptionValue(),
203
                    'defaultValue' => null !== $defaultValue
204
                        ? valueFromAST($defaultValue, $type)
205
                        : null,
206
                    'astNode'      => $value,
207
                ];
208
            });
209
    }
210
211
    /**
212
     * @param TypeDefinitionNodeInterface $node
213
     * @return NamedTypeInterface
214
     * @throws LanguageException
215
     */
216
    protected function buildNamedType(TypeDefinitionNodeInterface $node): NamedTypeInterface
217
    {
218
        if ($node instanceof ObjectTypeDefinitionNode) {
219
            return $this->buildObjectType($node);
220
        }
221
        if ($node instanceof InterfaceTypeDefinitionNode) {
222
            return $this->buildInterfaceType($node);
223
        }
224
        if ($node instanceof EnumTypeDefinitionNode) {
225
            return $this->buildEnumType($node);
226
        }
227
        if ($node instanceof UnionTypeDefinitionNode) {
228
            return $this->buildUnionType($node);
229
        }
230
        if ($node instanceof ScalarTypeDefinitionNode) {
231
            return $this->buildScalarType($node);
232
        }
233
        if ($node instanceof InputObjectTypeDefinitionNode) {
234
            return $this->buildInputObjectType($node);
235
        }
236
237
        throw new LanguageException(\sprintf('Type kind "%s" not supported.', $node->getKind()));
238
    }
239
240
    /**
241
     * @param ObjectTypeDefinitionNode $node
242
     * @return ObjectType
243
     */
244
    protected function buildObjectType(ObjectTypeDefinitionNode $node): ObjectType
245
    {
246
        return newObjectType([
247
            'name'        => $node->getNameValue(),
248
            'description' => $node->getDescriptionValue(),
249
            'fields'      => $node->hasFields() ? function () use ($node) {
250
                return $this->buildFields($node);
251
            } : [],
252
            // Note: While this could make early assertions to get the correctly
253
            // typed values, that would throw immediately while type system
254
            // validation with validateSchema() will produce more actionable results.
255
            'interfaces'  => function () use ($node) {
256
                return $node->hasInterfaces() ? \array_map(function (NodeInterface $interface) {
257
                    return $this->buildType($interface);
258
                }, $node->getInterfaces()) : [];
259
            },
260
            'astNode'     => $node,
261
        ]);
262
    }
263
264
    /**
265
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|InputObjectTypeDefinitionNode $node
266
     * @return array
267
     */
268
    protected function buildFields($node): array
269
    {
270
        return keyValueMap(
271
            $node->getFields(),
272
            function ($value) {
273
                /** @var FieldDefinitionNode|InputValueDefinitionNode $value */
274
                return $value->getNameValue();
275
            },
276
            function ($value) use ($node) {
277
                /** @var FieldDefinitionNode|InputValueDefinitionNode $value */
278
                return $this->buildField($value, $this->getFieldResolver($node->getNameValue(), $value->getNameValue()));
279
            }
280
        );
281
    }
282
283
    /**
284
     * @param string $typeName
285
     * @param string $fieldName
286
     * @return callable|null
287
     */
288
    protected function getFieldResolver(string $typeName, string $fieldName): ?callable
289
    {
290
        return null !== $this->resolverRegistry
291
            ? $this->resolverRegistry->getFieldResolver($typeName, $fieldName)
292
            : null;
293
    }
294
295
    /**
296
     * @param InterfaceTypeDefinitionNode $node
297
     * @return InterfaceType
298
     */
299
    protected function buildInterfaceType(InterfaceTypeDefinitionNode $node): InterfaceType
300
    {
301
        return newInterfaceType([
302
            'name'        => $node->getNameValue(),
303
            'description' => $node->getDescriptionValue(),
304
            'fields'      => $node->hasFields() ? function () use ($node): array {
305
                return $this->buildFields($node);
306
            } : [],
307
            'resolveType' => $this->getTypeResolver($node->getNameValue()),
308
            'astNode'     => $node,
309
        ]);
310
    }
311
312
    /**
313
     * @param EnumTypeDefinitionNode $node
314
     * @return EnumType
315
     */
316
    protected function buildEnumType(EnumTypeDefinitionNode $node): EnumType
317
    {
318
        return newEnumType([
319
            'name'        => $node->getNameValue(),
320
            'description' => $node->getDescriptionValue(),
321
            'values'      => $node->hasValues() ? keyValueMap(
322
                $node->getValues(),
323
                function (EnumValueDefinitionNode $value): string {
324
                    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...
325
                },
326
                function (EnumValueDefinitionNode $value): array {
327
                    return [
328
                        'description'       => $value->getDescriptionValue(),
329
                        'deprecationReason' => $this->getDeprecationReason($value),
330
                        'astNode'           => $value,
331
                    ];
332
                }
333
            ) : [],
334
            'astNode'     => $node,
335
        ]);
336
    }
337
338
    /**
339
     * @param UnionTypeDefinitionNode $node
340
     * @return UnionType
341
     */
342
    protected function buildUnionType(UnionTypeDefinitionNode $node): UnionType
343
    {
344
        return newUnionType([
345
            'name'        => $node->getNameValue(),
346
            'description' => $node->getDescriptionValue(),
347
            'types'       => $node->hasTypes() ? \array_map(function (TypeNodeInterface $type) {
348
                return $this->buildType($type);
349
            }, $node->getTypes()) : [],
350
            'resolveType' => $this->getTypeResolver($node->getNameValue()),
351
            'astNode'     => $node,
352
        ]);
353
    }
354
355
    /**
356
     * @param string $typeName
357
     * @return callable|null
358
     */
359
    protected function getTypeResolver(string $typeName): ?callable
360
    {
361
        return null !== $this->resolverRegistry
362
            ? $this->resolverRegistry->getTypeResolver($typeName)
363
            : null;
364
    }
365
366
    /**
367
     * @param ScalarTypeDefinitionNode $node
368
     * @return ScalarType
369
     */
370
    protected function buildScalarType(ScalarTypeDefinitionNode $node): ScalarType
371
    {
372
        return newScalarType([
373
            'name'        => $node->getNameValue(),
374
            'description' => $node->getDescriptionValue(),
375
            'serialize'   => function ($value) {
376
                return $value;
377
            },
378
            'astNode'     => $node,
379
        ]);
380
    }
381
382
    /**
383
     * @param InputObjectTypeDefinitionNode $node
384
     * @return InputObjectType
385
     */
386
    protected function buildInputObjectType(InputObjectTypeDefinitionNode $node): InputObjectType
387
    {
388
        return newInputObjectType([
389
            'name'        => $node->getNameValue(),
390
            'description' => $node->getDescriptionValue(),
391
            'fields'      => $node->hasFields() ? function () use ($node) {
392
                return keyValueMap(
393
                    $node->getFields(),
394
                    function (InputValueDefinitionNode $value): string {
395
                        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...
396
                    },
397
                    function (InputValueDefinitionNode $value): array {
398
                        $type         = $this->buildWrappedType($value->getType());
399
                        $defaultValue = $value->getDefaultValue();
400
                        return [
401
                            'type'         => $type,
402
                            'description'  => $value->getDescriptionValue(),
403
                            'defaultValue' => null !== $defaultValue
404
                                ? valueFromAST($defaultValue, $type)
405
                                : null,
406
                            'astNode'      => $value,
407
                        ];
408
                    }
409
                );
410
            } : [],
411
            'astNode'     => $node,
412
        ]);
413
    }
414
415
    /**
416
     * @inheritdoc
417
     */
418
    protected function resolveType(NamedTypeNode $node): ?NamedTypeInterface
419
    {
420
        return \call_user_func($this->resolveTypeFunction, $node);
421
    }
422
423
    /**
424
     * @param NamedTypeNode $node
425
     * @return NamedTypeInterface|null
426
     * @throws \Psr\SimpleCache\InvalidArgumentException
427
     */
428
    public function defaultTypeResolver(NamedTypeNode $node): ?NamedTypeInterface
429
    {
430
        return $this->getFromCache($node->getNameValue()) ?? null;
431
    }
432
433
    /**
434
     * @param string $typeName
435
     * @return TypeDefinitionNodeInterface|null
436
     */
437
    protected function getTypeDefinition(string $typeName): ?TypeDefinitionNodeInterface
438
    {
439
        return $this->typeDefinitionsMap[$typeName] ?? null;
440
    }
441
442
    /**
443
     * @param TypeNodeInterface $typeNode
444
     * @return NamedTypeNode
445
     */
446
    protected function getNamedTypeNode(TypeNodeInterface $typeNode): NamedTypeNode
447
    {
448
        $namedType = $typeNode;
449
450
        while ($namedType instanceof ListTypeNode || $namedType instanceof NonNullTypeNode) {
451
            $namedType = $namedType->getType();
452
        }
453
454
        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...
455
    }
456
457
    /**
458
     * @param NodeInterface|EnumValueDefinitionNode|FieldDefinitionNode $node
459
     * @return null|string
460
     * @throws InvariantException
461
     * @throws ExecutionException
462
     * @throws InvalidTypeException
463
     */
464
    protected function getDeprecationReason(NodeInterface $node): ?string
465
    {
466
        $deprecated = coerceDirectiveValues(DeprecatedDirective(), $node);
467
        return $deprecated['reason'] ?? null;
468
    }
469
470
    /**
471
     * @return string
472
     */
473
    protected function getCachePrefix(): string
474
    {
475
        return self::CACHE_PREFIX;
476
    }
477
}
478
479
/**
480
 * @param TypeInterface                        $innerType
481
 * @param NamedTypeInterface|TypeNodeInterface $inputTypeNode
482
 * @return TypeInterface
483
 * @throws InvariantException
484
 * @throws InvalidTypeException
485
 */
486
function buildWrappedType(TypeInterface $innerType, TypeNodeInterface $inputTypeNode): TypeInterface
487
{
488
    if ($inputTypeNode instanceof ListTypeNode) {
489
        return newList(buildWrappedType($innerType, $inputTypeNode->getType()));
490
    }
491
492
    if ($inputTypeNode instanceof NonNullTypeNode) {
493
        $wrappedType = buildWrappedType($innerType, $inputTypeNode->getType());
494
        return newNonNull(assertNullableType($wrappedType));
495
    }
496
497
    return $innerType;
498
}
499