Completed
Pull Request — master (#159)
by Christoffer
02:45
created

SchemaExtender::extendImplementedInterfaces()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 9.0534
c 0
b 0
f 0
cc 4
eloc 9
nc 2
nop 1
1
<?php
2
3
namespace Digia\GraphQL\SchemaExtension;
4
5
use Digia\GraphQL\Cache\CacheAwareTrait;
6
use Digia\GraphQL\Error\ExecutionException;
7
use Digia\GraphQL\Error\ExtensionException;
8
use Digia\GraphQL\Error\InvalidTypeException;
9
use Digia\GraphQL\Error\InvariantException;
10
use Digia\GraphQL\Language\Node\DirectiveDefinitionNode;
11
use Digia\GraphQL\Language\Node\DocumentNode;
12
use Digia\GraphQL\Language\Node\EnumTypeExtensionNode;
13
use Digia\GraphQL\Language\Node\InputObjectTypeExtensionNode;
14
use Digia\GraphQL\Language\Node\InterfaceTypeDefinitionNode;
15
use Digia\GraphQL\Language\Node\InterfaceTypeExtensionNode;
16
use Digia\GraphQL\Language\Node\NamedTypeNode;
17
use Digia\GraphQL\Language\Node\NodeInterface;
18
use Digia\GraphQL\Language\Node\ObjectTypeDefinitionNode;
19
use Digia\GraphQL\Language\Node\ObjectTypeExtensionNode;
20
use Digia\GraphQL\Language\Node\ScalarTypeExtensionNode;
21
use Digia\GraphQL\Language\Node\TypeDefinitionNodeInterface;
22
use Digia\GraphQL\Language\Node\UnionTypeExtensionNode;
23
use Digia\GraphQL\SchemaBuilder\DefinitionBuilderCreatorInterface;
24
use Digia\GraphQL\SchemaBuilder\DefinitionBuilderInterface;
25
use Digia\GraphQL\Type\Definition\Argument;
26
use Digia\GraphQL\Type\Definition\InterfaceType;
27
use Digia\GraphQL\Type\Definition\ListType;
28
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
29
use Digia\GraphQL\Type\Definition\NonNullType;
30
use Digia\GraphQL\Type\Definition\ObjectType;
31
use Digia\GraphQL\Type\Definition\TypeInterface;
32
use Digia\GraphQL\Type\Definition\UnionType;
33
use Digia\GraphQL\Type\SchemaInterface;
34
use Psr\SimpleCache\CacheInterface;
35
use Psr\SimpleCache\InvalidArgumentException;
36
use function Digia\GraphQL\Type\GraphQLInterfaceType;
37
use function Digia\GraphQL\Type\GraphQLList;
38
use function Digia\GraphQL\Type\GraphQLNonNull;
39
use function Digia\GraphQL\Type\GraphQLObjectType;
40
use function Digia\GraphQL\Type\GraphQLUnionType;
41
use function Digia\GraphQL\Type\isIntrospectionType;
42
use function Digia\GraphQL\Util\keyMap;
43
use function Digia\GraphQL\Util\toString;
44
45
class SchemaExtender implements SchemaExtenderInterface
46
{
47
    use CacheAwareTrait;
48
49
    private const CACHE_PREFIX = 'GraphQL_SchemaExtender_';
50
51
    /**
52
     * @var DefinitionBuilderCreatorInterface
53
     */
54
    protected $definitionBuilderCreator;
55
56
    /**
57
     * @var DefinitionBuilderInterface
58
     */
59
    protected $definitionBuilder;
60
61
    /**
62
     * @var array
63
     */
64
    protected $typeDefinitionMap;
65
66
    /**
67
     * @var ObjectTypeExtensionNode[][]
68
     */
69
    protected $typeExtensionMap;
70
71
    /**
72
     * @var array
73
     */
74
    protected $directiveDefinitions;
75
76
    /**
77
     * SchemaExtender constructor.
78
     * @param DefinitionBuilderCreatorInterface $definitionBuilderCreator
79
     */
80
    public function __construct(DefinitionBuilderCreatorInterface $definitionBuilderCreator, CacheInterface $cache)
81
    {
82
        $this->definitionBuilderCreator = $definitionBuilderCreator;
83
        $this->cache                    = $cache;
84
    }
85
86
    /**
87
     * @param SchemaInterface $schema
88
     * @param DocumentNode    $document
89
     * @param array           $options
90
     * @return SchemaInterface
91
     * @throws ExecutionException
92
     */
93
    public function extend(SchemaInterface $schema, DocumentNode $document, array $options = []): SchemaInterface
94
    {
95
        $this->typeDefinitionMap    = [];
96
        $this->typeExtensionsMap    = [];
0 ignored issues
show
Bug Best Practice introduced by
The property typeExtensionsMap does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
97
        $this->directiveDefinitions = [];
98
99
        foreach ($document->getDefinitions() as $definition) {
100
            if ($definition instanceof TypeDefinitionNodeInterface) {
101
                // Sanity check that none of the defined types conflict with the schema's existing types.
102
                $typeName     = $definition->getNameValue();
103
                $existingType = $schema->getType($typeName);
104
105
                if (null !== $existingType) {
106
                    throw new ExecutionException(
107
                        \sprintf(
108
                            'Type "%s" already exists in the schema. It cannot also ' .
109
                            'be defined in this type definition.',
110
                            $typeName
111
                        ),
112
                        [$definition]
113
                    );
114
                }
115
116
                $typeDefinitionMap[$typeName] = $definition;
117
118
                continue;
119
            }
120
121
            if ($definition instanceof ObjectTypeDefinitionNode || $definition instanceof InterfaceTypeDefinitionNode) {
122
                // Sanity check that this type extension exists within the schema's existing types.
123
                $extendedTypeName = $definition->getNameValue();
124
                $existingType     = $schema->getType($extendedTypeName);
125
126
                if (null === $existingType) {
127
                    throw new ExecutionException(
128
                        \sprintf(
129
                            'Cannot extend type "%s" because it does not exist in the existing schema.',
130
                            $extendedTypeName
131
                        ),
132
                        [$definition]
133
                    );
134
                }
135
136
                $this->checkExtensionNode($existingType, $definition);
137
138
                $existingTypeExtensions               = $typeExtensionsMap[$extendedTypeName] ?? [];
139
                $typeExtensionsMap[$extendedTypeName] = \array_merge($existingTypeExtensions, [$definition]);
140
141
                continue;
142
            }
143
144
            if ($definition instanceof DirectiveDefinitionNode) {
145
                $directiveName     = $definition->getNameValue();
146
                $existingDirective = $schema->getDirective($directiveName);
147
148
                if (null !== $existingDirective) {
149
                    throw new ExecutionException(
150
                        \sprintf(
151
                            'Directive "%s" already exists in the schema. It cannot be redefined.',
152
                            $directiveName
153
                        ),
154
                        [$definition]
155
                    );
156
                }
157
158
                $directiveDefinitions[] = $definition;
159
160
                continue;
161
            }
162
163
            if ($definition instanceof ScalarTypeExtensionNode ||
164
                $definition instanceof UnionTypeExtensionNode ||
165
                $definition instanceof EnumTypeExtensionNode ||
166
                $definition instanceof InputObjectTypeExtensionNode) {
167
                throw new ExecutionException(
168
                    \sprintf('The %s kind is not yet supported by extendSchema().', $definition->getKind())
169
                );
170
            }
171
        }
172
173
        // If this document contains no new types, extensions, or directives then
174
        // return the same unmodified GraphQLSchema instance.
175
        if (empty($typeDefinitionMap) && empty($typeExtensionsMap) && empty($directiveDefinitions)) {
176
            return $schema;
177
        }
178
179
        $resolveTypeFunction = function (NamedTypeNode $node) use ($schema): ?TypeInterface {
180
            $typeName     = $node->getNameValue();
181
            $existingType = $schema->getType($typeName);
182
183
            if (null !== $existingType) {
184
                /** @noinspection PhpIncompatibleReturnTypeInspection */
185
                /** @noinspection PhpParamsInspection */
186
                return $this->getExtendedType($existingType);
0 ignored issues
show
Bug introduced by
$existingType of type Digia\GraphQL\Type\Definition\TypeInterface is incompatible with the type Digia\GraphQL\Type\Definition\NamedTypeInterface expected by parameter $type of Digia\GraphQL\SchemaExte...nder::getExtendedType(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

186
                return $this->getExtendedType(/** @scrutinizer ignore-type */ $existingType);
Loading history...
Bug Best Practice introduced by
The expression return $this->getExtendedType($existingType) returns the type Digia\GraphQL\Type\Definition\NamedTypeInterface which is incompatible with the type-hinted return null|Digia\GraphQL\Type\Definition\TypeInterface.
Loading history...
187
            }
188
189
            throw new ExecutionException(
190
                \sprintf(
191
                    'Unknown type: "%s". Ensure that this type exists ' .
192
                    'either in the original schema, or is added in a type definition.',
193
                    $typeName
194
                ),
195
                [$node]
196
            );
197
        };
198
199
        $this->definitionBuilder = $this->definitionBuilderCreator->create(
200
            $typeDefinitionMap,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $typeDefinitionMap seems to be defined by a foreach iteration on line 99. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
201
            [],
202
            $resolveTypeFunction
203
        );
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Digia\GraphQL\Type\SchemaInterface. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
204
    }
205
206
    /**
207
     * @param TypeInterface $type
208
     * @param NodeInterface $node
209
     * @throws ExecutionException
210
     */
211
    protected function checkExtensionNode(TypeInterface $type, NodeInterface $node): void
212
    {
213
        if ($node instanceof ObjectTypeExtensionNode && !($type instanceof ObjectType)) {
214
            throw new ExecutionException(
215
                \sprintf('Cannot extend non-object type "%s".', toString($type)),
216
                [$node]
217
            );
218
        }
219
220
        if ($node instanceof InterfaceTypeExtensionNode && !($type instanceof InterfaceType)) {
221
            throw new ExecutionException(
222
                \sprintf('Cannot extend non-interface type "%s".', toString($type)),
223
                [$node]
224
            );
225
        }
226
    }
227
228
    /**
229
     * @param NamedTypeInterface $type
230
     * @return NamedTypeInterface
231
     * @throws InvalidArgumentException
232
     * @throws InvariantException
233
     */
234
    protected function getExtendedType(NamedTypeInterface $type): NamedTypeInterface
235
    {
236
        $typeName = $type->getName();
237
238
        if ($this->isInCache($typeName)) {
239
            $this->setInCache($typeName, $this->extendType($type));
240
        }
241
242
        return $this->getFromCache($typeName);
243
    }
244
245
    /**
246
     * @param NamedTypeInterface $type
247
     * @return NamedTypeInterface
248
     * @throws InvariantException
249
     */
250
    protected function extendType(NamedTypeInterface $type): NamedTypeInterface
251
    {
252
        /** @noinspection PhpParamsInspection */
253
        if (isIntrospectionType($type)) {
254
            // Introspection types are not extended.
255
            return $type;
256
        }
257
258
        if ($type instanceof ObjectType) {
259
            return $this->extendObjectType($type);
260
        }
261
262
        if ($type instanceof InterfaceType) {
263
            return $this->extendInterfaceType($type);
264
        }
265
266
        if ($type instanceof UnionType) {
267
            return $this->extendUnionType($type);
268
        }
269
270
        // This type is not yet extendable.
271
        return $type;
272
    }
273
274
    /**
275
     * @param ObjectType $type
276
     * @return ObjectType
277
     */
278
    protected function extendObjectType(ObjectType $type): ObjectType
279
    {
280
        $typeName          = $type->getName();
281
        $extensionASTNodes = $type->getExtensionAstNodes();
282
283
        if (isset($this->typeExtensionMap[$typeName])) {
284
            $extensionASTNodes = !empty($extensionASTNodes)
285
                ? \array_merge($this->typeExtensionMap[$typeName], $extensionASTNodes)
286
                : $this->typeExtensionMap[$typeName];
287
        }
288
289
        return GraphQLObjectType([
290
            'name'              => $typeName,
291
            'description'       => $type->getDescription(),
292
            'interfaces'        => function () use ($type) {
293
                return $this->extendImplementedInterfaces($type);
294
            },
295
            'fields'            => function () use ($type) {
296
                return $this->extendFieldMap($type);
297
            },
298
            'astNode'           => $type->getAstNode(),
299
            'extensionASTNodes' => $extensionASTNodes,
300
            'isTypeOf'          => $type->getIsTypeOf(),
301
        ]);
302
    }
303
304
    /**
305
     * @param InterfaceType $type
306
     * @return InterfaceType
307
     */
308
    protected function extendInterfaceType(InterfaceType $type): InterfaceType
309
    {
310
        $typeName          = $type->getName();
311
        $extensionASTNodes = $this->typeExtensionMap[$typeName];
312
313
        if (isset($this->typeExtensionMap[$typeName])) {
314
            $extensionASTNodes = !empty($extensionASTNodes)
315
                ? \array_merge($this->typeExtensionMap[$typeName], $extensionASTNodes)
316
                : $this->typeExtensionMap[$typeName];
317
        }
318
319
        return GraphQLInterfaceType([
320
            'name'              => $typeName,
321
            'description'       => $type->getDescription(),
322
            'fields'            => function () use ($type) {
323
                return $this->extendFieldMap($type);
324
            },
325
            'astNode'           => $type->getAstNode(),
326
            'extensionASTNodes' => $extensionASTNodes,
327
            'resolveType'       => $type->getResolveType(),
328
        ]);
329
    }
330
331
    /**
332
     * @param UnionType $type
333
     * @return UnionType
334
     * @throws InvariantException
335
     */
336
    protected function extendUnionType(UnionType $type): UnionType
337
    {
338
        return GraphQLUnionType([
339
            'name'        => $type->getName(),
340
            'description' => $type->getDescription(),
341
            'types'       => \array_map(function ($unionType) {
342
                return $this->getExtendedType($unionType);
343
            }, $type->getTypes()),
344
            'astNode'     => $type->getAstNode(),
345
            'resolveType' => $type->getResolveType(),
346
        ]);
347
    }
348
349
    /**
350
     * @param ObjectType $type
351
     * @return array
352
     * @throws InvariantException
353
     */
354
    protected function extendImplementedInterfaces(ObjectType $type): array
355
    {
356
        $interfaces = \array_map(function (InterfaceType $interface) {
357
            return $this->getExtendedType($interface);
358
        }, $type->getInterfaces());
359
360
        // If there are any extensions to the interfaces, apply those here.
361
        $extensions = $this->typeExtensionMap[$type->getName()] ?? null;
362
363
        if (null !== $extensions) {
364
            foreach ($extensions as $extension) {
365
                foreach ($extension->getInterfaces() as $namedType) {
366
                    // Note: While this could make early assertions to get the correctly
367
                    // typed values, that would throw immediately while type system
368
                    // validation with validateSchema() will produce more actionable results.
369
                    $interfaces[] = $this->definitionBuilder->buildType($namedType);
370
                }
371
            }
372
        }
373
374
        return $interfaces;
375
    }
376
377
    /**
378
     * @param TypeInterface|ObjectType|InterfaceType $type
379
     * @return array
380
     * @throws InvalidTypeException
381
     * @throws InvariantException
382
     * @throws ExtensionException
383
     * @throws InvalidArgumentException
384
     */
385
    protected function extendFieldMap(TypeInterface $type): array
386
    {
387
        $typeName    = $type->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Type\Defin...n\AbstractTypeInterface or Digia\GraphQL\Type\Defin...n\WrappingTypeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

387
        /** @scrutinizer ignore-call */ 
388
        $typeName    = $type->getName();
Loading history...
388
        $newFieldMap = [];
389
        $oldFieldMap = $type->getFields();
0 ignored issues
show
Bug introduced by
The method getFields() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of Digia\GraphQL\Type\Definition\TypeInterface such as Digia\GraphQL\Type\Definition\InputObjectType or Digia\GraphQL\Type\Definition\ObjectType or Digia\GraphQL\Type\Definition\InterfaceType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

389
        /** @scrutinizer ignore-call */ 
390
        $oldFieldMap = $type->getFields();
Loading history...
390
391
        foreach (\array_keys($oldFieldMap) as $fieldName) {
392
            $field = $oldFieldMap[$fieldName];
393
394
            $newFieldMap[$fieldName] = [
395
                'description'       => $field->getDescription(),
396
                'deprecationReason' => $field->getDeprecationReason(),
397
                'type'              => $this->extendFieldType($field->getType()),
398
                'args'              => keyMap($field->getArguments(), function (Argument $argument) {
399
                    return $argument->getName();
400
                }),
401
                'astNode'           => $field->getAstNode(),
402
                'resolve'           => $field->getResolve(),
403
            ];
404
        }
405
406
        // If there are any extensions to the fields, apply those here.
407
        $extensions = $this->typeExtensionMap[$typeName] ?? null;
408
409
        if (null !== $extensions) {
410
            foreach ($extensions as $extension) {
411
                foreach ($extension->getFields() as $field) {
412
                    $fieldName = $field->getName();
413
414
                    if (isset($oldFieldMap[$fieldName])) {
415
                        throw new ExtensionException(
416
                            \sprintf(
417
                                'Field "%s.%s" already exists in the schema. ' .
418
                                'It cannot also be defined in this type extension.',
419
                                $typeName, $fieldName
420
                            ),
421
                            [$field]
422
                        );
423
                    }
424
425
                    $newFieldMap[$fieldName] = $this->definitionBuilder->buildField($field);
426
                }
427
            }
428
        }
429
430
        return $newFieldMap;
431
    }
432
433
    /**
434
     * @param TypeInterface $typeDefinition
435
     * @return TypeInterface
436
     * @throws InvalidArgumentException
437
     * @throws InvalidTypeException
438
     * @throws InvariantException
439
     */
440
    protected function extendFieldType(TypeInterface $typeDefinition): TypeInterface
441
    {
442
        if ($typeDefinition instanceof ListType) {
443
            return GraphQLList($this->extendFieldType($typeDefinition->getOfType()));
444
        }
445
446
        if ($typeDefinition instanceof NonNullType) {
447
            return GraphQLNonNull($this->extendFieldType($typeDefinition->getOfType()));
448
        }
449
450
        /** @noinspection PhpIncompatibleReturnTypeInspection */
451
        /** @noinspection PhpParamsInspection */
452
        return $this->getExtendedType($typeDefinition);
0 ignored issues
show
Bug introduced by
$typeDefinition of type Digia\GraphQL\Type\Definition\TypeInterface is incompatible with the type Digia\GraphQL\Type\Definition\NamedTypeInterface expected by parameter $type of Digia\GraphQL\SchemaExte...nder::getExtendedType(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

452
        return $this->getExtendedType(/** @scrutinizer ignore-type */ $typeDefinition);
Loading history...
Bug Best Practice introduced by
The expression return $this->getExtendedType($typeDefinition) returns the type Digia\GraphQL\Type\Definition\NamedTypeInterface which is incompatible with the type-hinted return Digia\GraphQL\Type\Definition\TypeInterface.
Loading history...
453
    }
454
455
    /**
456
     * @inheritdoc
457
     */
458
    protected function getCachePrefix(): string
459
    {
460
        return self::CACHE_PREFIX;
461
    }
462
}
463