GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 3cccd4...21c599 )
by Šimon
10:52
created

BuildClientSchema::getObjectType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Utils;
6
7
use GraphQL\Error\InvariantViolation;
8
use GraphQL\Language\Parser;
9
use GraphQL\Type\Definition\CustomScalarType;
10
use GraphQL\Type\Definition\Directive;
11
use GraphQL\Type\Definition\EnumType;
12
use GraphQL\Type\Definition\InputObjectType;
13
use GraphQL\Type\Definition\InputType;
14
use GraphQL\Type\Definition\InterfaceType;
15
use GraphQL\Type\Definition\ListOfType;
16
use GraphQL\Type\Definition\NamedType;
17
use GraphQL\Type\Definition\NonNull;
18
use GraphQL\Type\Definition\NullableType;
19
use GraphQL\Type\Definition\ObjectType;
20
use GraphQL\Type\Definition\OutputType;
21
use GraphQL\Type\Definition\ScalarType;
22
use GraphQL\Type\Definition\Type;
23
use GraphQL\Type\Definition\UnionType;
24
use GraphQL\Type\Introspection;
25
use GraphQL\Type\Schema;
26
use GraphQL\Type\SchemaConfig;
27
use GraphQL\Type\TypeKind;
28
use function array_key_exists;
29
use function array_map;
30
use function array_merge;
31
use function json_encode;
32
33
class BuildClientSchema
34
{
35
    /** @var array<string, mixed[]> */
36
    private $introspection;
37
38
    /** @var array<string, bool> */
39
    private $options;
40
41
    /** @var array<string, NamedType&Type> */
42
    private $typeMap;
43
44
    /**
45
     * @param array<string, mixed[]> $introspectionQuery
46
     * @param array<string, bool>    $options
47
     */
48
    public function __construct(array $introspectionQuery, array $options = [])
49
    {
50
        $this->introspection = $introspectionQuery;
51
        $this->options       = $options;
52
    }
53
54
    /**
55
     * Build a schema for use by client tools.
56
     *
57
     * Given the result of a client running the introspection query, creates and
58
     * returns a \GraphQL\Type\Schema instance which can be then used with all graphql-php
59
     * tools, but cannot be used to execute a query, as introspection does not
60
     * represent the "resolver", "parse" or "serialize" functions or any other
61
     * server-internal mechanisms.
62
     *
63
     * This function expects a complete introspection result. Don't forget to check
64
     * the "errors" field of a server response before calling this function.
65
     *
66
     * Accepts options as a third argument:
67
     *
68
     *    - assumeValid:
69
     *          When building a schema from a GraphQL service's introspection result, it
70
     *          might be safe to assume the schema is valid. Set to true to assume the
71
     *          produced schema is valid.
72
     *
73
     *          Default: false
74
     *
75
     * @param array<string, mixed[]> $introspectionQuery
76
     * @param array<string, bool>    $options
77
     *
78
     * @api
79
     */
80
    public static function build(array $introspectionQuery, array $options = []) : Schema
81
    {
82
        $builder = new self($introspectionQuery, $options);
83
84
        return $builder->buildSchema();
85
    }
86
87
    public function buildSchema() : Schema
88
    {
89
        if (! array_key_exists('__schema', $this->introspection)) {
90
            throw new InvariantViolation('Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: ' . json_encode($this->introspection) . '.');
91
        }
92
93
        $schemaIntrospection = $this->introspection['__schema'];
94
95
        $this->typeMap = Utils::keyValMap(
96
            $schemaIntrospection['types'],
97
            static function (array $typeIntrospection) {
98
                return $typeIntrospection['name'];
99
            },
100
            function (array $typeIntrospection) {
101
                return $this->buildType($typeIntrospection);
102
            }
103
        );
104
105
        $builtInTypes = array_merge(
106
            Type::getStandardTypes(),
107
            Introspection::getTypes()
108
        );
109
        foreach ($builtInTypes as $name => $type) {
110
            if (! isset($this->typeMap[$name])) {
111
                continue;
112
            }
113
114
            $this->typeMap[$name] = $type;
115
        }
116
117
        $queryType = isset($schemaIntrospection['queryType'])
118
            ? $this->getObjectType($schemaIntrospection['queryType'])
119
            : null;
120
121
        $mutationType = isset($schemaIntrospection['mutationType'])
122
            ? $this->getObjectType($schemaIntrospection['mutationType'])
123
            : null;
124
125
        $subscriptionType = isset($schemaIntrospection['subscriptionType'])
126
            ? $this->getObjectType($schemaIntrospection['subscriptionType'])
127
            : null;
128
129
        $directives = isset($schemaIntrospection['directives'])
130
            ? array_map(
131
                [$this, 'buildDirective'],
132
                $schemaIntrospection['directives']
133
            )
134
            : [];
135
136
        $schemaConfig = new SchemaConfig();
137
        $schemaConfig->setQuery($queryType)
138
            ->setMutation($mutationType)
139
            ->setSubscription($subscriptionType)
140
            ->setTypes($this->typeMap)
141
            ->setDirectives($directives)
142
            ->setAssumeValid(
143
                isset($this->options)
144
                && isset($this->options['assumeValid'])
145
                && $this->options['assumeValid']
146
            );
147
148
        return new Schema($schemaConfig);
149
    }
150
151
    /**
152
     * @param array<string, mixed> $typeRef
153
     */
154
    private function getType(array $typeRef) : Type
155
    {
156
        if (isset($typeRef['kind'])) {
157
            if ($typeRef['kind'] === TypeKind::LIST) {
158
                if (! isset($typeRef['ofType'])) {
159
                    throw new InvariantViolation('Decorated type deeper than introspection query.');
160
                }
161
162
                return new ListOfType($this->getType($typeRef['ofType']));
163
            }
164
165
            if ($typeRef['kind'] === TypeKind::NON_NULL) {
166
                if (! isset($typeRef['ofType'])) {
167
                    throw new InvariantViolation('Decorated type deeper than introspection query.');
168
                }
169
                /** @var NullableType $nullableType */
170
                $nullableType = $this->getType($typeRef['ofType']);
171
172
                return new NonNull($nullableType);
173
            }
174
        }
175
176
        if (! isset($typeRef['name'])) {
177
            throw new InvariantViolation('Unknown type reference: ' . json_encode($typeRef) . '.');
178
        }
179
180
        return $this->getNamedType($typeRef['name']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getNamedType($typeRef['name']) returns the type GraphQL\Type\Definition\NamedType which is incompatible with the type-hinted return GraphQL\Type\Definition\Type.
Loading history...
181
    }
182
183
    /**
184
     * @return NamedType&Type
185
     */
186
    private function getNamedType(string $typeName) : NamedType
187
    {
188
        if (! isset($this->typeMap[$typeName])) {
189
            throw new InvariantViolation(
190
                "Invalid or incomplete schema, unknown type: ${typeName}. Ensure that a full introspection query is used in order to build a client schema."
191
            );
192
        }
193
194
        return $this->typeMap[$typeName];
195
    }
196
197
    /**
198
     * @param array<string, mixed> $typeRef
199
     */
200
    private function getInputType(array $typeRef) : InputType
201
    {
202
        $type = $this->getType($typeRef);
203
204
        if ($type instanceof InputType) {
0 ignored issues
show
introduced by
$type is always a sub-type of GraphQL\Type\Definition\InputType.
Loading history...
205
            return $type;
206
        }
207
208
        throw new InvariantViolation('Introspection must provide input type for arguments, but received: ' . json_encode($type) . '.');
209
    }
210
211
    /**
212
     * @param array<string, mixed> $typeRef
213
     */
214
    private function getOutputType(array $typeRef) : OutputType
215
    {
216
        $type = $this->getType($typeRef);
217
218
        if ($type instanceof OutputType) {
0 ignored issues
show
introduced by
$type is always a sub-type of GraphQL\Type\Definition\OutputType.
Loading history...
219
            return $type;
220
        }
221
222
        throw new InvariantViolation('Introspection must provide output type for fields, but received: ' . json_encode($type) . '.');
223
    }
224
225
    /**
226
     * @param array<string, mixed> $typeRef
227
     */
228
    private function getObjectType(array $typeRef) : ObjectType
229
    {
230
        $type = $this->getType($typeRef);
231
232
        return ObjectType::assertObjectType($type);
233
    }
234
235
    /**
236
     * @param array<string, mixed> $typeRef
237
     */
238
    public function getInterfaceType(array $typeRef) : InterfaceType
239
    {
240
        $type = $this->getType($typeRef);
241
242
        return InterfaceType::assertInterfaceType($type);
243
    }
244
245
    /**
246
     * @param array<string, mixed> $type
247
     */
248
    private function buildType(array $type) : NamedType
249
    {
250
        if (array_key_exists('name', $type) && array_key_exists('kind', $type)) {
251
            switch ($type['kind']) {
252
                case TypeKind::SCALAR:
253
                    return $this->buildScalarDef($type);
254
                case TypeKind::OBJECT:
255
                    return $this->buildObjectDef($type);
256
                case TypeKind::INTERFACE:
257
                    return $this->buildInterfaceDef($type);
258
                case TypeKind::UNION:
259
                    return $this->buildUnionDef($type);
260
                case TypeKind::ENUM:
261
                    return $this->buildEnumDef($type);
262
                case TypeKind::INPUT_OBJECT:
263
                    return $this->buildInputObjectDef($type);
264
            }
265
        }
266
267
        throw new InvariantViolation(
268
            'Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: ' . json_encode($type) . '.'
269
        );
270
    }
271
272
    /**
273
     * @param array<string, string> $scalar
274
     */
275
    private function buildScalarDef(array $scalar) : ScalarType
276
    {
277
        return new CustomScalarType([
278
            'name' => $scalar['name'],
279
            'description' => $scalar['description'],
280
            'serialize' => static function ($value) : string {
281
                return (string) $value;
282
            },
283
        ]);
284
    }
285
286
    /**
287
     * @param array<string, mixed> $object
288
     */
289
    private function buildObjectDef(array $object) : ObjectType
290
    {
291
        if (! array_key_exists('interfaces', $object)) {
292
            throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($object) . '.');
293
        }
294
295
        return new ObjectType([
296
            'name' => $object['name'],
297
            'description' => $object['description'],
298
            'interfaces' => function () use ($object) {
299
                return array_map(
300
                    [$this, 'getInterfaceType'],
301
                    // Legacy support for interfaces with null as interfaces field
302
                    $object['interfaces'] ?? []
303
                );
304
            },
305
            'fields' => function () use ($object) {
306
                return $this->buildFieldDefMap($object);
307
            },
308
        ]);
309
    }
310
311
    /**
312
     * @param array<string, mixed> $interface
313
     */
314
    private function buildInterfaceDef(array $interface) : InterfaceType
315
    {
316
        return new InterfaceType([
317
            'name' => $interface['name'],
318
            'description' => $interface['description'],
319
            'fields' => function () use ($interface) {
320
                return $this->buildFieldDefMap($interface);
321
            },
322
        ]);
323
    }
324
325
    /**
326
     * @param array<string, string|array<string>> $union
327
     */
328
    private function buildUnionDef(array $union) : UnionType
329
    {
330
        if (! array_key_exists('possibleTypes', $union)) {
331
            throw new InvariantViolation('Introspection result missing possibleTypes: ' . json_encode($union) . '.');
332
        }
333
334
        return new UnionType([
335
            'name' => $union['name'],
336
            'description' => $union['description'],
337
            'types' => function () use ($union) {
338
                return array_map(
339
                    [$this, 'getObjectType'],
340
                    $union['possibleTypes']
0 ignored issues
show
Bug introduced by
It seems like $union['possibleTypes'] can also be of type string; however, parameter $arr1 of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

340
                    /** @scrutinizer ignore-type */ $union['possibleTypes']
Loading history...
341
                );
342
            },
343
        ]);
344
    }
345
346
    /**
347
     * @param array<string, string|array<string, string>> $enum
348
     */
349
    private function buildEnumDef(array $enum) : EnumType
350
    {
351
        if (! array_key_exists('enumValues', $enum)) {
352
            throw new InvariantViolation('Introspection result missing enumValues: ' . json_encode($enum) . '.');
353
        }
354
355
        return new EnumType([
356
            'name' => $enum['name'],
357
            'description' => $enum['description'],
358
            'values' => Utils::keyValMap(
359
                $enum['enumValues'],
0 ignored issues
show
Bug introduced by
It seems like $enum['enumValues'] can also be of type string; however, parameter $traversable of GraphQL\Utils\Utils::keyValMap() does only seem to accept iterable, maybe add an additional type check? ( Ignorable by Annotation )

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

359
                /** @scrutinizer ignore-type */ $enum['enumValues'],
Loading history...
360
                static function (array $enumValue) : string {
361
                    return $enumValue['name'];
362
                },
363
                static function (array $enumValue) {
364
                    return [
365
                        'description' => $enumValue['description'],
366
                        'deprecationReason' => $enumValue['deprecationReason'],
367
                    ];
368
                }
369
            ),
370
        ]);
371
    }
372
373
    /**
374
     * @param array<string, mixed> $inputObject
375
     */
376
    private function buildInputObjectDef(array $inputObject) : InputObjectType
377
    {
378
        if (! array_key_exists('inputFields', $inputObject)) {
379
            throw new InvariantViolation('Introspection result missing inputFields: ' . json_encode($inputObject) . '.');
380
        }
381
382
        return new InputObjectType([
383
            'name' => $inputObject['name'],
384
            'description' => $inputObject['description'],
385
            'fields' => function () use ($inputObject) {
386
                return $this->buildInputValueDefMap($inputObject['inputFields']);
387
            },
388
        ]);
389
    }
390
391
    /**
392
     * @param array<string, mixed> $typeIntrospection
393
     */
394
    private function buildFieldDefMap(array $typeIntrospection)
395
    {
396
        if (! array_key_exists('fields', $typeIntrospection)) {
397
            throw new InvariantViolation('Introspection result missing fields: ' . json_encode($typeIntrospection) . '.');
398
        }
399
400
        return Utils::keyValMap(
401
            $typeIntrospection['fields'],
402
            static function (array $fieldIntrospection) : string {
403
                return $fieldIntrospection['name'];
404
            },
405
            function (array $fieldIntrospection) {
406
                if (! array_key_exists('args', $fieldIntrospection)) {
407
                    throw new InvariantViolation('Introspection result missing field args: ' . json_encode($fieldIntrospection) . '.');
408
                }
409
410
                return [
411
                    'description' => $fieldIntrospection['description'],
412
                    'deprecationReason' => $fieldIntrospection['deprecationReason'],
413
                    'type' => $this->getOutputType($fieldIntrospection['type']),
414
                    'args' => $this->buildInputValueDefMap($fieldIntrospection['args']),
415
                ];
416
            }
417
        );
418
    }
419
420
    /**
421
     * @param array<int, array<string, mixed>> $inputValueIntrospections
422
     *
423
     * @return array<string, array<string, mixed>>
424
     */
425
    private function buildInputValueDefMap(array $inputValueIntrospections) : array
426
    {
427
        return Utils::keyValMap(
428
            $inputValueIntrospections,
429
            static function (array $inputValue) : string {
430
                return $inputValue['name'];
431
            },
432
            [$this, 'buildInputValue']
433
        );
434
    }
435
436
    /**
437
     * @param array<string, mixed> $inputValueIntrospection
438
     *
439
     * @return array<string, mixed>
440
     */
441
    public function buildInputValue(array $inputValueIntrospection) : array
442
    {
443
        $type = $this->getInputType($inputValueIntrospection['type']);
444
445
        $inputValue = [
446
            'description' => $inputValueIntrospection['description'],
447
            'type' => $type,
448
        ];
449
450
        if (isset($inputValueIntrospection['defaultValue'])) {
451
            $inputValue['defaultValue'] = AST::valueFromAST(
452
                Parser::parseValue($inputValueIntrospection['defaultValue']),
453
                $type
454
            );
455
        }
456
457
        return $inputValue;
458
    }
459
460
    /**
461
     * @param array<string, mixed> $directive
462
     */
463
    public function buildDirective(array $directive) : Directive
464
    {
465
        if (! array_key_exists('args', $directive)) {
466
            throw new InvariantViolation('Introspection result missing directive args: ' . json_encode($directive) . '.');
467
        }
468
        if (! array_key_exists('locations', $directive)) {
469
            throw new InvariantViolation('Introspection result missing directive locations: ' . json_encode($directive) . '.');
470
        }
471
472
        return new Directive([
473
            'name' => $directive['name'],
474
            'description' => $directive['description'],
475
            'locations' => $directive['locations'],
476
            'args' => $this->buildInputValueDefMap($directive['args']),
477
        ]);
478
    }
479
}
480