Passed
Pull Request — master (#173)
by Christoffer
02:44
created

ExtensionContext::extendImplementedInterfaces()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
c 0
b 0
f 0
rs 8.7972
cc 4
eloc 10
nc 2
nop 1
1
<?php
2
3
namespace Digia\GraphQL\Schema\Extension;
4
5
use Digia\GraphQL\Error\ExtensionException;
6
use Digia\GraphQL\Error\InvalidTypeException;
7
use Digia\GraphQL\Error\InvariantException;
8
use Digia\GraphQL\Language\Node\DirectiveDefinitionNode;
9
use Digia\GraphQL\Language\Node\NamedTypeNode;
10
use Digia\GraphQL\Schema\DefinitionBuilderInterface;
11
use Digia\GraphQL\Type\Definition\Argument;
12
use Digia\GraphQL\Type\Definition\Directive;
13
use Digia\GraphQL\Type\Definition\InterfaceType;
14
use Digia\GraphQL\Type\Definition\ListType;
15
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
16
use Digia\GraphQL\Type\Definition\NonNullType;
17
use Digia\GraphQL\Type\Definition\ObjectType;
18
use Digia\GraphQL\Type\Definition\TypeInterface;
19
use Digia\GraphQL\Type\Definition\UnionType;
20
use Psr\SimpleCache\InvalidArgumentException;
21
use function Digia\GraphQL\Type\isIntrospectionType;
22
use function Digia\GraphQL\Type\newInterfaceType;
23
use function Digia\GraphQL\Type\newList;
24
use function Digia\GraphQL\Type\newNonNull;
25
use function Digia\GraphQL\Type\newObjectType;
26
use function Digia\GraphQL\Type\newUnionType;
27
use function Digia\GraphQL\Util\invariant;
28
use function Digia\GraphQL\Util\keyMap;
29
30
class ExtensionContext implements ExtensionContextInterface
31
{
32
    /**
33
     * @var ExtensionInfo
34
     */
35
    protected $info;
36
37
    /**
38
     * @var DefinitionBuilderInterface
39
     */
40
    protected $definitionBuilder;
41
42
    /**
43
     * @var NamedTypeInterface[]
44
     */
45
    protected $extendTypeCache = [];
46
47
    /**
48
     * ExtensionContext constructor.
49
     * @param ExtensionInfo $info
50
     */
51
    public function __construct(ExtensionInfo $info)
52
    {
53
        $this->info = $info;
54
    }
55
56
    /**
57
     * @return bool
58
     */
59
    public function isSchemaExtended(): bool
60
    {
61
        return
62
            $this->info->hasTypeExtensionsMap() ||
63
            $this->info->hasTypeDefinitionMap() ||
64
            $this->info->hasDirectiveDefinitions();
65
    }
66
67
    /**
68
     * @return TypeInterface|null
69
     * @throws InvalidArgumentException
70
     * @throws InvariantException
71
     */
72
    public function getExtendedQueryType(): ?TypeInterface
73
    {
74
        $existingQueryType = $this->info->getSchema()->getQueryType();
75
76
        return null !== $existingQueryType
77
            ? $this->getExtendedType($existingQueryType)
78
            : null;
79
    }
80
81
    /**
82
     * @return TypeInterface|null
83
     * @throws InvalidArgumentException
84
     * @throws InvariantException
85
     */
86
    public function getExtendedMutationType(): ?TypeInterface
87
    {
88
        $existingMutationType = $this->info->getSchema()->getMutationType();
89
90
        return null !== $existingMutationType
91
            ? $this->getExtendedType($existingMutationType)
92
            : null;
93
    }
94
95
    /**
96
     * @return TypeInterface|null
97
     * @throws InvalidArgumentException
98
     * @throws InvariantException
99
     */
100
    public function getExtendedSubscriptionType(): ?TypeInterface
101
    {
102
        $existingSubscriptionType = $this->info->getSchema()->getSubscriptionType();
103
104
        return null !== $existingSubscriptionType
105
            ? $this->getExtendedType($existingSubscriptionType)
106
            : null;
107
    }
108
109
    /**
110
     * @return TypeInterface[]
111
     */
112
    public function getExtendedTypes(): array
113
    {
114
        $extendedTypes = \array_map(function ($type) {
115
            return $this->getExtendedType($type);
116
        }, $this->info->getSchema()->getTypeMap());
117
118
        return \array_merge(
119
            $extendedTypes,
120
            $this->definitionBuilder->buildTypes($this->info->getTypeDefinitionMap())
121
        );
122
    }
123
124
    /**
125
     * @return Directive[]
126
     * @throws InvariantException
127
     */
128
    public function getExtendedDirectives(): array
129
    {
130
        $existingDirectives = $this->info->getSchema()->getDirectives();
131
132
        invariant(!empty($existingDirectives), 'schema must have default directives');
133
134
        return \array_merge(
135
            $existingDirectives,
136
            \array_map(function (DirectiveDefinitionNode $node) {
137
                return $this->definitionBuilder->buildDirective($node);
138
            }, $this->info->getDirectiveDefinitions())
139
        );
140
    }
141
142
    /**
143
     * @param DefinitionBuilderInterface $definitionBuilder
144
     * @return ExtensionContext
145
     */
146
    public function setDefinitionBuilder(DefinitionBuilderInterface $definitionBuilder): ExtensionContext
147
    {
148
        $this->definitionBuilder = $definitionBuilder;
149
        return $this;
150
    }
151
152
    /**
153
     * @param NamedTypeNode $node
154
     * @return TypeInterface|null
155
     * @throws ExtensionException
156
     * @throws InvalidArgumentException
157
     * @throws InvariantException
158
     */
159
    public function resolveType(NamedTypeNode $node): ?TypeInterface
160
    {
161
        $typeName     = $node->getNameValue();
162
        $existingType = $this->info->getSchema()->getType($typeName);
163
164
        if (null !== $existingType) {
165
            return $this->getExtendedType($existingType);
166
        }
167
168
        throw new ExtensionException(
169
            \sprintf(
170
                'Unknown type: "%s". Ensure that this type exists ' .
171
                'either in the original schema, or is added in a type definition.',
172
                $typeName
173
            ),
174
            [$node]
175
        );
176
    }
177
178
    /**
179
     * @param TypeInterface $type
180
     * @return TypeInterface
181
     * @throws InvalidArgumentException
182
     * @throws InvariantException
183
     */
184
    protected function getExtendedType(TypeInterface $type): TypeInterface
185
    {
186
        /** @noinspection PhpUndefinedMethodInspection */
187
        $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

187
        /** @scrutinizer ignore-call */ 
188
        $typeName = $type->getName();
Loading history...
188
189
        if (!isset($this->extendTypeCache[$typeName])) {
190
            $this->extendTypeCache[$typeName] = $this->extendType($type);
191
        }
192
193
        return $this->extendTypeCache[$typeName];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->extendTypeCache[$typeName] returns the type Digia\GraphQL\Type\Definition\NamedTypeInterface which is incompatible with the type-hinted return Digia\GraphQL\Type\Definition\TypeInterface.
Loading history...
194
    }
195
196
    /**
197
     * @param TypeInterface $type
198
     * @return TypeInterface
199
     * @throws InvariantException
200
     */
201
    protected function extendType(TypeInterface $type): TypeInterface
202
    {
203
        /** @noinspection PhpParamsInspection */
204
        if (isIntrospectionType($type)) {
205
            // Introspection types are not extended.
206
            return $type;
207
        }
208
209
        if ($type instanceof ObjectType) {
210
            return $this->extendObjectType($type);
211
        }
212
213
        if ($type instanceof InterfaceType) {
214
            return $this->extendInterfaceType($type);
215
        }
216
217
        if ($type instanceof UnionType) {
218
            return $this->extendUnionType($type);
219
        }
220
221
        // This type is not yet extendable.
222
        return $type;
223
    }
224
225
    /**
226
     * @param ObjectType $type
227
     * @return ObjectType
228
     */
229
    protected function extendObjectType(ObjectType $type): ObjectType
230
    {
231
        $typeName          = $type->getName();
232
        $extensionASTNodes = $type->getExtensionAstNodes();
233
234
        if ($this->info->hasTypeExtensions($typeName)) {
235
            $extensionASTNodes = $this->extendExtensionASTNodes($typeName, $extensionASTNodes);
236
        }
237
238
        return newObjectType([
239
            'name'              => $typeName,
240
            'description'       => $type->getDescription(),
241
            'interfaces'        => function () use ($type) {
242
                return $this->extendImplementedInterfaces($type);
243
            },
244
            'fields'            => function () use ($type) {
245
                return $this->extendFieldMap($type);
246
            },
247
            'astNode'           => $type->getAstNode(),
248
            'extensionASTNodes' => $extensionASTNodes,
249
            'isTypeOf'          => $type->getIsTypeOf(),
250
        ]);
251
    }
252
253
    /**
254
     * @param InterfaceType $type
255
     * @return InterfaceType
256
     */
257
    protected function extendInterfaceType(InterfaceType $type): InterfaceType
258
    {
259
        $typeName          = $type->getName();
260
        $extensionASTNodes = $this->info->getTypeExtensions($typeName);
261
262
        if ($this->info->hasTypeExtensions($typeName)) {
263
            $extensionASTNodes = $this->extendExtensionASTNodes($typeName, $extensionASTNodes);
264
        }
265
266
        return newInterfaceType([
267
            'name'           => $typeName,
268
            'description'    => $type->getDescription(),
269
            'fields'         => function () use ($type) {
270
                return $this->extendFieldMap($type);
271
            },
272
            'astNode'        => $type->getAstNode(),
273
            'typeExtensions' => $extensionASTNodes,
274
            'resolveType'    => $type->getResolveType(),
275
        ]);
276
    }
277
278
    /**
279
     * @param string $typeName
280
     * @param array  $nodes
281
     * @return array
282
     */
283
    protected function extendExtensionASTNodes(string $typeName, array $nodes): array
284
    {
285
        $typeExtensions = $this->info->getTypeExtensions($typeName);
286
        return !empty($nodes) ? \array_merge($typeExtensions, $nodes) : $typeExtensions;
0 ignored issues
show
Bug Best Practice introduced by
The expression return ! empty($nodes) ?...odes) : $typeExtensions could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
287
    }
288
289
    /**
290
     * @param UnionType $type
291
     * @return UnionType
292
     * @throws InvariantException
293
     */
294
    protected function extendUnionType(UnionType $type): UnionType
295
    {
296
        return newUnionType([
297
            'name'        => $type->getName(),
298
            'description' => $type->getDescription(),
299
            'types'       => \array_map(function ($unionType) {
300
                return $this->getExtendedType($unionType);
301
            }, $type->getTypes()),
302
            'astNode'     => $type->getAstNode(),
303
            'resolveType' => $type->getResolveType(),
304
        ]);
305
    }
306
307
    /**
308
     * @param ObjectType $type
309
     * @return array
310
     * @throws InvariantException
311
     */
312
    protected function extendImplementedInterfaces(ObjectType $type): array
313
    {
314
        $typeName = $type->getName();
315
316
        $interfaces = \array_map(function (InterfaceType $interface) {
317
            return $this->getExtendedType($interface);
318
        }, $type->getInterfaces());
319
320
        // If there are any extensions to the interfaces, apply those here.
321
        $extensions = $this->info->getTypeExtensions($typeName);
322
323
        if (null !== $extensions) {
324
            foreach ($extensions as $extension) {
325
                foreach ($extension->getInterfaces() as $namedType) {
326
                    // Note: While this could make early assertions to get the correctly
327
                    // typed values, that would throw immediately while type system
328
                    // validation with validateSchema() will produce more actionable results.
329
                    $interfaces[] = $this->definitionBuilder->buildType($namedType);
330
                }
331
            }
332
        }
333
334
        return $interfaces;
335
    }
336
337
    /**
338
     * @param TypeInterface|ObjectType|InterfaceType $type
339
     * @return array
340
     * @throws InvalidTypeException
341
     * @throws InvariantException
342
     * @throws ExtensionException
343
     * @throws InvalidArgumentException
344
     */
345
    protected function extendFieldMap(TypeInterface $type): array
346
    {
347
        $typeName    = $type->getName();
348
        $newFieldMap = [];
349
        $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

349
        /** @scrutinizer ignore-call */ 
350
        $oldFieldMap = $type->getFields();
Loading history...
350
351
        foreach (\array_keys($oldFieldMap) as $fieldName) {
352
            $field = $oldFieldMap[$fieldName];
353
354
            $newFieldMap[$fieldName] = [
355
                'description'       => $field->getDescription(),
356
                'deprecationReason' => $field->getDeprecationReason(),
357
                'type'              => $this->extendFieldType($field->getType()),
358
                'args'              => keyMap($field->getArguments(), function (Argument $argument) {
359
                    return $argument->getName();
360
                }),
361
                'astNode'           => $field->getAstNode(),
362
                'resolve'           => $field->getResolve(),
363
            ];
364
        }
365
366
        // If there are any extensions to the fields, apply those here.
367
        $extensions = $this->info->getTypeExtensions($typeName);
368
369
        if (null !== $extensions) {
370
            foreach ($extensions as $extension) {
371
                foreach ($extension->getFields() as $field) {
372
                    $fieldName = $field->getNameValue();
373
374
                    if (isset($oldFieldMap[$fieldName])) {
375
                        throw new ExtensionException(
376
                            \sprintf(
377
                                'Field "%s.%s" already exists in the schema. ' .
378
                                'It cannot also be defined in this type extension.',
379
                                $typeName, $fieldName
380
                            ),
381
                            [$field]
382
                        );
383
                    }
384
385
                    $newFieldMap[$fieldName] = $this->definitionBuilder->buildField($field);
386
                }
387
            }
388
        }
389
390
        return $newFieldMap;
391
    }
392
393
    /**
394
     * @param TypeInterface $typeDefinition
395
     * @return TypeInterface
396
     * @throws InvalidArgumentException
397
     * @throws InvalidTypeException
398
     * @throws InvariantException
399
     */
400
    protected function extendFieldType(TypeInterface $typeDefinition): TypeInterface
401
    {
402
        if ($typeDefinition instanceof ListType) {
403
            return newList($this->extendFieldType($typeDefinition->getOfType()));
404
        }
405
406
        if ($typeDefinition instanceof NonNullType) {
407
            return newNonNull($this->extendFieldType($typeDefinition->getOfType()));
408
        }
409
410
        return $this->getExtendedType($typeDefinition);
411
    }
412
}
413