Completed
Pull Request — master (#270)
by Christoffer
02:11
created

ExtensionContext   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 373
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 134
dl 0
loc 373
rs 9.1199
c 0
b 0
f 0
wmc 41

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A getExtendedTypes() 0 9 1
A getExtendedQueryType() 0 7 2
A getExtendedDirectives() 0 13 2
A resolveType() 0 16 2
A isSchemaExtended() 0 6 3
A getExtendedMutationType() 0 7 2
A getExtendedSubscriptionType() 0 7 2
A setDefinitionBuilder() 0 4 1
A getExtendedType() 0 9 2
A extendType() 0 22 5
A extendObjectType() 0 21 2
A extendUnionType() 0 10 1
A extendInterfaceType() 0 18 2
A extendExtensionASTNodes() 0 4 2
A extendFieldMap() 0 42 5
A extendImplementedInterfaces() 0 21 3
A extendFieldType() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like ExtensionContext 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 ExtensionContext, and based on these observations, apply Extract Interface, too.

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\Directive;
12
use Digia\GraphQL\Type\Definition\FieldsAwareInterface;
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
28
class ExtensionContext implements ExtensionContextInterface
29
{
30
    /**
31
     * @var ExtendInfo
32
     */
33
    protected $info;
34
35
    /**
36
     * @var DefinitionBuilderInterface
37
     */
38
    protected $definitionBuilder;
39
40
    /**
41
     * @var TypeInterface[]
42
     */
43
    protected $extendTypeCache = [];
44
45
    /**
46
     * ExtensionContext constructor.
47
     * @param ExtendInfo $info
48
     */
49
    public function __construct(ExtendInfo $info)
50
    {
51
        $this->info = $info;
52
    }
53
54
    /**
55
     * @return bool
56
     */
57
    public function isSchemaExtended(): bool
58
    {
59
        return
60
            $this->info->hasTypeExtensionsMap() ||
61
            $this->info->hasTypeDefinitionMap() ||
62
            $this->info->hasDirectiveDefinitions();
63
    }
64
65
    /**
66
     * @return TypeInterface|null
67
     * @throws InvariantException
68
     */
69
    public function getExtendedQueryType(): ?TypeInterface
70
    {
71
        $existingQueryType = $this->info->getSchema()->getQueryType();
72
73
        return null !== $existingQueryType
74
            ? $this->getExtendedType($existingQueryType)
75
            : null;
76
    }
77
78
    /**
79
     * @return TypeInterface|null
80
     * @throws InvariantException
81
     */
82
    public function getExtendedMutationType(): ?TypeInterface
83
    {
84
        $existingMutationType = $this->info->getSchema()->getMutationType();
85
86
        return null !== $existingMutationType
87
            ? $this->getExtendedType($existingMutationType)
88
            : null;
89
    }
90
91
    /**
92
     * @return TypeInterface|null
93
     * @throws InvariantException
94
     */
95
    public function getExtendedSubscriptionType(): ?TypeInterface
96
    {
97
        $existingSubscriptionType = $this->info->getSchema()->getSubscriptionType();
98
99
        return null !== $existingSubscriptionType
100
            ? $this->getExtendedType($existingSubscriptionType)
101
            : null;
102
    }
103
104
    /**
105
     * @return TypeInterface[]
106
     */
107
    public function getExtendedTypes(): array
108
    {
109
        $extendedTypes = \array_map(function ($type) {
110
            return $this->getExtendedType($type);
111
        }, $this->info->getSchema()->getTypeMap());
112
113
        return \array_merge(
114
            $extendedTypes,
115
            $this->definitionBuilder->buildTypes($this->info->getTypeDefinitionMap())
116
        );
117
    }
118
119
    /**
120
     * @return Directive[]
121
     * @throws InvariantException
122
     */
123
    public function getExtendedDirectives(): array
124
    {
125
        $existingDirectives = $this->info->getSchema()->getDirectives();
126
127
        if (empty($existingDirectives)) {
128
            throw new InvariantException('schema must have default directives');
129
        }
130
131
        return \array_merge(
132
            $existingDirectives,
133
            \array_map(function (DirectiveDefinitionNode $node) {
134
                return $this->definitionBuilder->buildDirective($node);
135
            }, $this->info->getDirectiveDefinitions())
136
        );
137
    }
138
139
    /**
140
     * @param DefinitionBuilderInterface $definitionBuilder
141
     * @return ExtensionContext
142
     */
143
    public function setDefinitionBuilder(DefinitionBuilderInterface $definitionBuilder): ExtensionContext
144
    {
145
        $this->definitionBuilder = $definitionBuilder;
146
        return $this;
147
    }
148
149
    /**
150
     * @param NamedTypeNode $node
151
     * @return TypeInterface|null
152
     * @throws ExtensionException
153
     * @throws InvariantException
154
     */
155
    public function resolveType(NamedTypeNode $node): ?TypeInterface
156
    {
157
        $typeName     = $node->getNameValue();
158
        $existingType = $this->info->getSchema()->getType($typeName);
159
160
        if ($existingType instanceof NamedTypeInterface) {
161
            return $this->getExtendedType($existingType);
162
        }
163
164
        throw new ExtensionException(
165
            \sprintf(
166
                'Unknown type: "%s". Ensure that this type exists ' .
167
                'either in the original schema, or is added in a type definition.',
168
                $typeName
169
            ),
170
            [$node]
171
        );
172
    }
173
174
    /**
175
     * @param NamedTypeInterface $type
176
     * @return TypeInterface
177
     * @throws InvariantException
178
     */
179
    protected function getExtendedType(NamedTypeInterface $type): TypeInterface
180
    {
181
        $typeName = $type->getName();
182
183
        if (!isset($this->extendTypeCache[$typeName])) {
184
            $this->extendTypeCache[$typeName] = $this->extendType($type);
185
        }
186
187
        return $this->extendTypeCache[$typeName];
188
    }
189
190
    /**
191
     * @param TypeInterface $type
192
     * @return TypeInterface
193
     * @throws InvariantException
194
     */
195
    protected function extendType(TypeInterface $type): TypeInterface
196
    {
197
        /** @noinspection PhpParamsInspection */
198
        if (isIntrospectionType($type)) {
199
            // Introspection types are not extended.
200
            return $type;
201
        }
202
203
        if ($type instanceof ObjectType) {
204
            return $this->extendObjectType($type);
205
        }
206
207
        if ($type instanceof InterfaceType) {
208
            return $this->extendInterfaceType($type);
209
        }
210
211
        if ($type instanceof UnionType) {
212
            return $this->extendUnionType($type);
213
        }
214
215
        // This type is not yet extendable.
216
        return $type;
217
    }
218
219
    /**
220
     * @param ObjectType $type
221
     * @return ObjectType
222
     * @throws InvariantException
223
     */
224
    protected function extendObjectType(ObjectType $type): ObjectType
225
    {
226
        $typeName          = $type->getName();
227
        $extensionASTNodes = $type->getExtensionAstNodes();
228
229
        if ($this->info->hasTypeExtensions($typeName)) {
230
            $extensionASTNodes = $this->extendExtensionASTNodes($typeName, $extensionASTNodes);
231
        }
232
233
        return newObjectType([
234
            'name'              => $typeName,
235
            'description'       => $type->getDescription(),
236
            'interfaces'        => function () use ($type) {
237
                return $this->extendImplementedInterfaces($type);
238
            },
239
            'fields'            => function () use ($type) {
240
                return $this->extendFieldMap($type);
241
            },
242
            'astNode'           => $type->getAstNode(),
243
            'extensionASTNodes' => $extensionASTNodes,
244
            'isTypeOf'          => $type->getIsTypeOf(),
245
        ]);
246
    }
247
248
    /**
249
     * @param InterfaceType $type
250
     * @return InterfaceType
251
     * @throws InvariantException
252
     */
253
    protected function extendInterfaceType(InterfaceType $type): InterfaceType
254
    {
255
        $typeName          = $type->getName();
256
        $extensionASTNodes = $this->info->getTypeExtensions($typeName);
257
258
        if ($this->info->hasTypeExtensions($typeName)) {
259
            $extensionASTNodes = $this->extendExtensionASTNodes($typeName, $extensionASTNodes);
260
        }
261
262
        return newInterfaceType([
263
            'name'              => $typeName,
264
            'description'       => $type->getDescription(),
265
            'fields'            => function () use ($type) {
266
                return $this->extendFieldMap($type);
267
            },
268
            'astNode'           => $type->getAstNode(),
269
            'extensionASTNodes' => $extensionASTNodes,
270
            'resolveType'       => $type->getResolveTypeCallback(),
271
        ]);
272
    }
273
274
    /**
275
     * @param string $typeName
276
     * @param array  $nodes
277
     * @return array
278
     */
279
    protected function extendExtensionASTNodes(string $typeName, array $nodes): array
280
    {
281
        $typeExtensions = $this->info->getTypeExtensions($typeName);
282
        return !empty($nodes) ? \array_merge($typeExtensions, $nodes) : $typeExtensions;
283
    }
284
285
    /**
286
     * @param UnionType $type
287
     * @return UnionType
288
     * @throws InvariantException
289
     */
290
    protected function extendUnionType(UnionType $type): UnionType
291
    {
292
        return newUnionType([
293
            'name'        => $type->getName(),
294
            'description' => $type->getDescription(),
295
            'types'       => \array_map(function ($unionType) {
296
                return $this->getExtendedType($unionType);
297
            }, $type->getTypes()),
298
            'astNode'     => $type->getAstNode(),
299
            'resolveType' => $type->getResolveTypeCallback(),
300
        ]);
301
    }
302
303
    /**
304
     * @param ObjectType $type
305
     * @return array
306
     * @throws InvariantException
307
     */
308
    protected function extendImplementedInterfaces(ObjectType $type): array
309
    {
310
        $typeName = $type->getName();
311
312
        $interfaces = \array_map(function (InterfaceType $interface) {
313
            return $this->getExtendedType($interface);
314
        }, $type->getInterfaces());
315
316
        // If there are any extensions to the interfaces, apply those here.
317
        $extensions = $this->info->getTypeExtensions($typeName);
318
319
        foreach ($extensions as $extension) {
320
            foreach ($extension->getInterfaces() as $namedType) {
321
                // Note: While this could make early assertions to get the correctly
322
                // typed values, that would throw immediately while type system
323
                // validation with validateSchema() will produce more actionable results.
324
                $interfaces[] = $this->definitionBuilder->buildType($namedType);
325
            }
326
        }
327
328
        return $interfaces;
329
    }
330
331
    /**
332
     * @param FieldsAwareInterface $type
333
     * @return array
334
     * @throws InvalidTypeException
335
     * @throws InvariantException
336
     * @throws ExtensionException
337
     * @throws InvalidArgumentException
338
     */
339
    protected function extendFieldMap(FieldsAwareInterface $type): array
340
    {
341
        $typeName    = $type->getName();
342
        $newFieldMap = [];
343
        $oldFieldMap = $type->getFields();
344
345
        foreach (\array_keys($oldFieldMap) as $fieldName) {
346
            $field = $oldFieldMap[$fieldName];
347
348
            $newFieldMap[$fieldName] = [
349
                'description'       => $field->getDescription(),
350
                'deprecationReason' => $field->getDeprecationReason(),
351
                'type'              => $this->extendFieldType($field->getType()),
352
                'args'              => $field->getRawArguments(),
353
                'astNode'           => $field->getAstNode(),
354
                'resolve'           => $field->getResolveCallback(),
355
            ];
356
        }
357
358
        // If there are any extensions to the fields, apply those here.
359
        $extensions = $this->info->getTypeExtensions($typeName);
360
361
        foreach ($extensions as $extension) {
362
            foreach ($extension->getFields() as $field) {
363
                $fieldName = $field->getNameValue();
364
365
                if (isset($oldFieldMap[$fieldName])) {
366
                    throw new ExtensionException(
367
                        \sprintf(
368
                            'Field "%s.%s" already exists in the schema. ' .
369
                            'It cannot also be defined in this type extension.',
370
                            $typeName, $fieldName
371
                        ),
372
                        [$field]
373
                    );
374
                }
375
376
                $newFieldMap[$fieldName] = $this->definitionBuilder->buildField($field);
377
            }
378
        }
379
380
        return $newFieldMap;
381
    }
382
383
    /**
384
     * @param TypeInterface $typeDefinition
385
     * @return TypeInterface
386
     * @throws InvalidArgumentException
387
     * @throws InvalidTypeException
388
     * @throws InvariantException
389
     */
390
    protected function extendFieldType(TypeInterface $typeDefinition): TypeInterface
391
    {
392
        if ($typeDefinition instanceof ListType) {
393
            return newList($this->extendFieldType($typeDefinition->getOfType()));
394
        }
395
396
        if ($typeDefinition instanceof NonNullType) {
397
            return newNonNull($this->extendFieldType($typeDefinition->getOfType()));
398
        }
399
400
        return $this->getExtendedType($typeDefinition);
401
    }
402
}
403