Passed
Pull Request — master (#209)
by Christoffer
02:27
created

DefinitionPrinter::printIntrospectionSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 2
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Schema;
4
5
use Digia\GraphQL\Error\InvariantException;
6
use Digia\GraphQL\Error\PrintException;
7
use Digia\GraphQL\Type\Definition\DeprecationAwareInterface;
8
use Digia\GraphQL\Type\Definition\DescriptionAwareInterface;
9
use Digia\GraphQL\Type\Definition\Directive;
10
use Digia\GraphQL\Type\Definition\EnumType;
11
use Digia\GraphQL\Type\Definition\EnumValue;
12
use Digia\GraphQL\Type\Definition\InputObjectType;
13
use Digia\GraphQL\Type\Definition\InterfaceType;
14
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
15
use Digia\GraphQL\Type\Definition\ObjectType;
16
use Digia\GraphQL\Type\Definition\ScalarType;
17
use Digia\GraphQL\Type\Definition\TypeInterface;
18
use Digia\GraphQL\Type\Definition\UnionType;
19
use function Digia\GraphQL\Type\isIntrospectionType;
20
use function Digia\GraphQL\Type\isSpecifiedScalarType;
21
use function Digia\GraphQL\Util\toString;
22
23
class DefinitionPrinter implements DefinitionPrinterInterface
24
{
25
    /**
26
     * @var array
27
     */
28
    protected $options;
29
30
    /**
31
     * @inheritdoc
32
     * @throws PrintException
33
     */
34
    public function printSchema(SchemaInterface $schema, array $options = []): string
35
    {
36
        $this->options = $options;
37
38
        return $this->printFilteredSchema(
39
            $schema,
40
            function (Directive $directive): bool {
41
                return !isSpecifiedDirective($directive);
42
            },
43
            function (TypeInterface $type): bool {
44
                return !isSpecifiedScalarType($type) && !isIntrospectionType($type);
45
            }
46
        );
47
    }
48
49
    /**
50
     * @inheritdoc
51
     * @throws PrintException
52
     */
53
    public function printIntrospectionSchema(SchemaInterface $schema, array $options = []): string
54
    {
55
        $this->options = $options;
56
57
        return $this->printFilteredSchema(
58
            $schema,
59
            function (Directive $directive): bool {
60
                return isSpecifiedDirective($directive);
61
            },
62
            function (TypeInterface $type): bool {
63
                return isIntrospectionType($type);
64
            }
65
        );
66
    }
67
68
    /**
69
     * @param mixed $definition
70
     * @return string
71
     * @throws PrintException
72
     */
73
    public function print(DefinitionInterface $definition): string
74
    {
75
        if ($definition instanceof Schema) {
76
            return $this->printSchemaDefinition($definition);
77
        }
78
        if ($definition instanceof NamedTypeInterface) {
79
            return $this->printType($definition);
80
        }
81
82
        throw new PrintException(\sprintf('Invalid definition object: %s.', toString($definition)));
83
    }
84
85
    /**
86
     * @param SchemaInterface $schema
87
     * @param callable        $directiveFilter
88
     * @param callable        $typeFilter
89
     * @return string
90
     * @throws PrintException
91
     */
92
    protected function printFilteredSchema(
93
        SchemaInterface $schema,
94
        callable $directiveFilter,
95
        callable $typeFilter
96
    ): string {
97
        /** @noinspection PhpParamsInspection */
98
        $lines = \array_filter(\array_merge(
99
            [$this->printOne($schema)],
100
            $this->printMany($this->getSchemaDirectives($schema, $directiveFilter)),
101
            $this->printMany($this->getSchemaTypes($schema, $typeFilter))
102
        ));
103
104
        return printArray($lines, "\n\n") . "\n";
0 ignored issues
show
Bug introduced by
' ' of type string is incompatible with the type array expected by parameter $items of Digia\GraphQL\Schema\printArray(). ( Ignorable by Annotation )

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

104
        return printArray($lines, /** @scrutinizer ignore-type */ "\n\n") . "\n";
Loading history...
Bug introduced by
$lines of type array is incompatible with the type string expected by parameter $glue of Digia\GraphQL\Schema\printArray(). ( Ignorable by Annotation )

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

104
        return printArray(/** @scrutinizer ignore-type */ $lines, "\n\n") . "\n";
Loading history...
105
    }
106
107
    /**
108
     * @param SchemaInterface $schema
109
     * @param callable        $filter
110
     * @return array
111
     */
112
    protected function getSchemaDirectives(SchemaInterface $schema, callable $filter): array
113
    {
114
        return \array_filter($schema->getDirectives(), $filter);
115
    }
116
117
    /**
118
     * @param SchemaInterface $schema
119
     * @param callable        $filter
120
     * @return array
121
     */
122
    protected function getSchemaTypes(SchemaInterface $schema, callable $filter): array
123
    {
124
        $types = \array_filter(\array_values($schema->getTypeMap()), $filter);
125
126
        \usort($types, function (NamedTypeInterface $typeA, NamedTypeInterface $typeB) {
127
            return \strcasecmp($typeA->getName(), $typeB->getName());
128
        });
129
130
        return $types;
131
    }
132
133
    /**
134
     * @param Schema $definition
135
     * @return string
136
     */
137
    protected function printSchemaDefinition(Schema $definition): string
138
    {
139
        if ($this->isSchemaOfCommonNames($definition)) {
140
            return '';
141
        }
142
143
        $operationTypes = [];
144
145
        if (null !== ($queryType = $definition->getQueryType())) {
146
            $operationTypes[] = "  query: {$queryType->getName()}";
147
        }
148
149
        if (null !== ($mutationType = $definition->getMutationType())) {
150
            $operationTypes[] = "  mutation: {$mutationType->getName()}";
151
        }
152
153
        if (null !== ($subscriptionType = $definition->getSubscriptionType())) {
154
            $operationTypes[] = "  subscription: {$subscriptionType->getName()}";
155
        }
156
157
        return printLines([
158
            'schema {\n',
159
            printLines($operationTypes),
160
            '}'
161
        ]);
162
    }
163
164
    /**
165
     * GraphQL schema define root types for each type of operation. These types are
166
     * the same as any other type and can be named in any manner, however there is
167
     * a common naming convention:
168
     *
169
     *   schema {
170
     *     query: Query
171
     *     mutation: Mutation
172
     *     subscription: Subscription
173
     *   }
174
     *
175
     * When using this naming convention, the schema description can be omitted.
176
     *
177
     * @param SchemaInterface $schema
178
     * @return bool
179
     */
180
    protected function isSchemaOfCommonNames(SchemaInterface $schema): bool
181
    {
182
        if (null !== ($queryType = $schema->getQueryType()) &&
183
            $queryType->getName() !== 'Query') {
184
            return false;
185
        }
186
187
        if (null !== ($mutationType = $schema->getMutationType()) &&
188
            $mutationType->getName() !== 'Mutation') {
189
            return false;
190
        }
191
192
        if (null !== ($subscriptionType = $schema->getSubscriptionType()) &&
193
            $subscriptionType->getName() !== 'Subscription') {
194
            return false;
195
        }
196
197
        return true;
198
    }
199
200
    /**
201
     * @param NamedTypeInterface $type
202
     * @return string
203
     * @throws PrintException
204
     * @throws InvariantException
205
     */
206
    protected function printType(NamedTypeInterface $type): string
207
    {
208
        if ($type instanceof ScalarType) {
209
            return $this->printScalarType($type);
210
        }
211
        if ($type instanceof ObjectType) {
212
            return $this->printObjectType($type);
213
        }
214
        if ($type instanceof InterfaceType) {
215
            return $this->printInterfaceType($type);
216
        }
217
        if ($type instanceof UnionType) {
218
            return $this->printUnionType($type);
219
        }
220
        if ($type instanceof EnumType) {
221
            return $this->printEnumType($type);
222
        }
223
        if ($type instanceof InputObjectType) {
224
            return $this->printInputObjectType($type);
0 ignored issues
show
Bug introduced by
The method printInputObjectType() does not exist on Digia\GraphQL\Schema\DefinitionPrinter. ( Ignorable by Annotation )

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

224
            return $this->/** @scrutinizer ignore-call */ printInputObjectType($type);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
225
        }
226
227
        throw new PrintException(\sprintf('Unknown type: %s', (string)$type));
228
    }
229
230
    /**
231
     * @param ScalarType $type
232
     * @return string
233
     */
234
    protected function printScalarType(ScalarType $type): string
235
    {
236
        return printLines([
237
            $this->printDescription($type),
238
            "scalar {$type->getName()}"
239
        ]);
240
    }
241
242
    /**
243
     * @param ObjectType $type
244
     * @return string
245
     * @throws InvariantException
246
     */
247
    protected function printObjectType(ObjectType $type): string
248
    {
249
        $description = $this->printDescription($type);
250
        $name        = $type->getName();
251
        $implements  = $type->hasInterfaces()
252
            ? ' implements ' . printArray(' & ', \array_map(function (InterfaceType $interface) {
253
                return $interface->getName();
254
            }, $type->getInterfaces()))
255
            : '';
256
        $fields      = $this->printMany($type->getFields());
257
258
        return printLines([
259
            $description,
260
            "type {$name}{$implements} {",
261
            $fields,
262
            '}'
263
        ]);
264
    }
265
266
    /**
267
     * @param InterfaceType $type
268
     * @return string
269
     * @throws InvariantException
270
     */
271
    protected function printInterfaceType(InterfaceType $type): string
272
    {
273
        $description = $this->printDescription($type);
274
        $fields      = $this->printMany($type->getFields());
275
276
        return printLines([
277
            $description,
278
            "interface {$type->getName()} {",
279
            $fields,
280
            '}'
281
        ]);
282
    }
283
284
    /**
285
     * @param UnionType $type
286
     * @return string
287
     * @throws InvariantException
288
     */
289
    protected function printUnionType(UnionType $type): string
290
    {
291
        $description = $this->printDescription($type);
292
        $types       = printArray(' | ', $type->getTypes());
293
294
        return printLines([
295
            $description,
296
            "union {$type->getName()} = {$types}"
297
        ]);
298
    }
299
300
    /**
301
     * @param EnumType $type
302
     * @return string
303
     * @throws InvariantException
304
     */
305
    protected function printEnumType(EnumType $type): string
306
    {
307
        $description = $this->printDescription($type);
308
        $values      = $this->printEnumValues($type->getValues());
309
310
        return printLines([
311
            $description,
312
            "enum {$type->getName()} {",
313
            $values,
314
            '}'
315
        ]);
316
    }
317
318
    protected function printEnumValues(array $values): string
319
    {
320
        return printLines(\array_map(function (EnumValue $value, $i) {
321
            $description = $this->printDescription($value, '  ', 0 === $i);
322
            $name        = $value->getName();
323
            $deprecated  = $this->printDeprecated($value);
324
            return printLines([
325
                "  {$description}",
326
                "  {$name} {$deprecated}"
327
            ]);
328
        }, $values));
329
    }
330
331
    protected function printDeprecated(DeprecationAwareInterface $fieldOrEnumValue): string
332
    {
333
        if (!$fieldOrEnumValue->isDeprecated()) {
334
            return '';
335
        }
336
337
        $reason = $fieldOrEnumValue->getDeprecationReason();
338
339
        if (null === $reason || '' === $reason || DEFAULT_DEPRECATION_REASON === $reason) {
340
            return '@deprecated';
341
        }
342
343
        return "@deprecated(reason: {$reason})";
344
    }
345
346
    /**
347
     * @param mixed  $type
348
     * @param string $indentation
349
     * @param bool   $isFirstInBlock
350
     * @return string
351
     */
352
    protected function printDescription(
353
        $type,
354
        string $indentation = '',
355
        bool $isFirstInBlock = true
356
    ): string {
357
        if (!($type instanceof DescriptionAwareInterface)) {
358
            return '';
359
        }
360
361
        $lines      = descriptionLines($type->getDescription(), 120 - \strlen($indentation));
362
        $linesCount = \count($lines);
363
364
        if (isset($this->options['commentDescriptions'])) {
365
            return $this->printDescriptionWithComments($lines, $indentation, $isFirstInBlock);
366
        }
367
368
        $description = $indentation && !$isFirstInBlock
369
            ? "\n" . $indentation . '"""'
370
            : $indentation . '"""';
371
372
        // In some circumstances, a single line can be used for the description.
373
        if (
374
            $linesCount === 1 &&
375
            ($firstLineLength = \strlen($lines[0])) < 70 &&
376
            $lines[0][$firstLineLength - 1] !== '"'
377
        ) {
378
            return $description . escapeQuote($lines[0]) . '"""' . "\n";
379
        }
380
381
        // Format a multi-line block quote to account for leading space.
382
        $hasLeadingSpace = $lines[0][0] === ' ' || $lines[0][0] === "\t";
383
        if (!$hasLeadingSpace) {
384
            $description .= "\n";
385
        }
386
387
        for ($i = 0; $i < $linesCount; $i++) {
388
            $description .= $i !== 0 || !$hasLeadingSpace
389
                ? $description .= $indentation
390
                : escapeQuote($lines[$i]) . "\n";
391
        }
392
393
        $description .= $indentation . '"""';
394
395
        return $description;
396
    }
397
398
    protected function printDescriptionWithComments(array $lines, string $indentation, bool $isFirstInBlock): string
399
    {
400
        $description = $indentation && !$isFirstInBlock ? "\n" : '';
401
        $linesCount  = \count($lines);
402
403
        for ($i = 0; $i < $linesCount; $i++) {
404
            $description .= $lines[$i] === ''
405
                ? $indentation . '#' . "\n"
406
                : $indentation . '# ' . $lines[$i] . "\n";
407
        }
408
409
        return $description;
410
    }
411
412
    /**
413
     * @param DefinitionInterface $definition
414
     * @return string
415
     * @throws PrintException
416
     */
417
    protected function printOne(DefinitionInterface $definition): string
418
    {
419
        return $this->print($definition);
420
    }
421
422
    /**
423
     * @param DefinitionInterface[] $definitions
424
     * @return array
425
     */
426
    protected function printMany(array $definitions): array
427
    {
428
        return \array_map(function ($definition) {
429
            return $this->print($definition);
430
        }, $definitions);
431
    }
432
}
433