Passed
Push — master ( da4ce9...54c779 )
by Christoffer
02:24
created

ExtensionContext::getExtendedOperationTypes()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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