Passed
Pull Request — master (#272)
by Christoffer
02:34
created

ExtensionContext::extendObjectType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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