Completed
Push — master ( 6bdead...4d4282 )
by Vladimir
15s queued 14s
created

SchemaPrinter::printDeprecated()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 4
nc 3
nop 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Utils;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Language\Printer;
9
use GraphQL\Type\Definition\Directive;
10
use GraphQL\Type\Definition\EnumType;
11
use GraphQL\Type\Definition\InputObjectType;
12
use GraphQL\Type\Definition\InterfaceType;
13
use GraphQL\Type\Definition\ObjectType;
14
use GraphQL\Type\Definition\ScalarType;
15
use GraphQL\Type\Definition\Type;
16
use GraphQL\Type\Definition\UnionType;
17
use GraphQL\Type\Introspection;
18
use GraphQL\Type\Schema;
19
use function array_filter;
20
use function array_keys;
21
use function array_map;
22
use function array_merge;
23
use function array_values;
24
use function count;
25
use function explode;
26
use function implode;
27
use function ksort;
28
use function mb_strlen;
29
use function preg_match_all;
30
use function sprintf;
31
use function str_replace;
32
use function strlen;
33
use function substr;
34
35
/**
36
 * Given an instance of Schema, prints it in GraphQL type language.
37
 */
38
class SchemaPrinter
39
{
40
    /**
41
     * Accepts options as a second argument:
42
     *
43
     *    - commentDescriptions:
44
     *        Provide true to use preceding comments as the description.
45
     * @api
46
     * @param bool[] $options
47
     */
48 52
    public static function doPrint(Schema $schema, array $options = []) : string
49
    {
50 52
        return self::printFilteredSchema(
51 52
            $schema,
52
            function ($type) {
53 52
                return ! Directive::isSpecifiedDirective($type);
54 52
            },
55
            function ($type) {
56 52
                return ! Type::isBuiltInType($type);
57 52
            },
58 52
            $options
59
        );
60
    }
61
62
    /**
63
     * @param bool[] $options
64
     */
65 54
    private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter, $options) : string
66
    {
67 54
        $directives = array_filter(
68 54
            $schema->getDirectives(),
69
            function ($directive) use ($directiveFilter) {
70 54
                return $directiveFilter($directive);
71 54
            }
72
        );
73
74 54
        $types = $schema->getTypeMap();
75 54
        ksort($types);
76 54
        $types = array_filter($types, $typeFilter);
77
78 54
        return sprintf(
79 54
            "%s\n",
80 54
            implode(
81 54
                "\n\n",
82 54
                array_filter(
83 54
                    array_merge(
84 54
                        [self::printSchemaDefinition($schema)],
85 54
                        array_map(
86
                            function ($directive) use ($options) {
87 7
                                return self::printDirective($directive, $options);
88 54
                            },
89 54
                            $directives
90
                        ),
91 54
                        array_map(
92
                            function ($type) use ($options) {
93 54
                                return self::printType($type, $options);
94 54
                            },
95 54
                            $types
96
                        )
97
                    )
98
                )
99
            )
100
        );
101
    }
102
103 54
    private static function printSchemaDefinition(Schema $schema)
104
    {
105 54
        if (self::isSchemaOfCommonNames($schema)) {
106 50
            return;
107
        }
108
109 4
        $operationTypes = [];
110
111 4
        $queryType = $schema->getQueryType();
112 4
        if ($queryType) {
0 ignored issues
show
introduced by
$queryType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
113 4
            $operationTypes[] = sprintf('  query: %s', $queryType->name);
114
        }
115
116 4
        $mutationType = $schema->getMutationType();
117 4
        if ($mutationType) {
0 ignored issues
show
introduced by
$mutationType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
118 1
            $operationTypes[] = sprintf('  mutation: %s', $mutationType->name);
119
        }
120
121 4
        $subscriptionType = $schema->getSubscriptionType();
122 4
        if ($subscriptionType) {
0 ignored issues
show
introduced by
$subscriptionType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
123 1
            $operationTypes[] = sprintf('  subscription: %s', $subscriptionType->name);
124
        }
125
126 4
        return sprintf("schema {\n%s\n}", implode("\n", $operationTypes));
127
    }
128
129
    /**
130
     * GraphQL schema define root types for each type of operation. These types are
131
     * the same as any other type and can be named in any manner, however there is
132
     * a common naming convention:
133
     *
134
     *   schema {
135
     *     query: Query
136
     *     mutation: Mutation
137
     *   }
138
     *
139
     * When using this naming convention, the schema description can be omitted.
140
     */
141 54
    private static function isSchemaOfCommonNames(Schema $schema)
142
    {
143 54
        $queryType = $schema->getQueryType();
144 54
        if ($queryType && $queryType->name !== 'Query') {
145 4
            return false;
146
        }
147
148 50
        $mutationType = $schema->getMutationType();
149 50
        if ($mutationType && $mutationType->name !== 'Mutation') {
150
            return false;
151
        }
152
153 50
        $subscriptionType = $schema->getSubscriptionType();
154 50
        if ($subscriptionType && $subscriptionType->name !== 'Subscription') {
155
            return false;
156
        }
157
158 50
        return true;
159
    }
160
161 7
    private static function printDirective($directive, $options) : string
162
    {
163 7
        return self::printDescription($options, $directive) .
164 7
            'directive @' . $directive->name . self::printArgs($options, $directive->args) .
165 7
            ' on ' . implode(' | ', $directive->locations);
166
    }
167
168 54
    private static function printDescription($options, $def, $indentation = '', $firstInBlock = true) : string
169
    {
170 54
        if (! $def->description) {
171 54
            return '';
172
        }
173 7
        $lines = self::descriptionLines($def->description, 120 - strlen($indentation));
174 7
        if (isset($options['commentDescriptions'])) {
175 2
            return self::printDescriptionWithComments($lines, $indentation, $firstInBlock);
176
        }
177
178 5
        $description = ($indentation && ! $firstInBlock)
179 2
            ? "\n" . $indentation . '"""'
180 5
            : $indentation . '"""';
181
182
        // In some circumstances, a single line can be used for the description.
183 5
        if (count($lines) === 1 &&
184 5
            mb_strlen($lines[0]) < 70 &&
185 5
            substr($lines[0], -1) !== '"'
186
        ) {
187 3
            return $description . self::escapeQuote($lines[0]) . "\"\"\"\n";
188
        }
189
190
        // Format a multi-line block quote to account for leading space.
191 3
        $hasLeadingSpace = isset($lines[0]) &&
192
            (
193 3
                substr($lines[0], 0, 1) === ' ' ||
194 3
                substr($lines[0], 0, 1) === '\t'
195
            );
196 3
        if (! $hasLeadingSpace) {
197 2
            $description .= "\n";
198
        }
199
200 3
        $lineLength = count($lines);
201 3
        for ($i = 0; $i < $lineLength; $i++) {
202 3
            if ($i !== 0 || ! $hasLeadingSpace) {
203 2
                $description .= $indentation;
204
            }
205 3
            $description .= self::escapeQuote($lines[$i]) . "\n";
206
        }
207 3
        $description .= $indentation . "\"\"\"\n";
208
209 3
        return $description;
210
    }
211
212
    /**
213
     * @return string[]
214
     */
215 7
    private static function descriptionLines(string $description, int $maxLen) : array
216
    {
217 7
        $lines    = [];
218 7
        $rawLines = explode("\n", $description);
219 7
        foreach ($rawLines as $line) {
220 7
            if ($line === '') {
221 2
                $lines[] = $line;
222
            } else {
223
                // For > 120 character long lines, cut at space boundaries into sublines
224
                // of ~80 chars.
225 7
                $sublines = self::breakLine($line, $maxLen);
226 7
                foreach ($sublines as $subline) {
227 7
                    $lines[] = $subline;
228
                }
229
            }
230
        }
231
232 7
        return $lines;
233
    }
234
235
    /**
236
     * @return string[]
237
     */
238 7
    private static function breakLine(string $line, int $maxLen) : array
239
    {
240 7
        if (strlen($line) < $maxLen + 5) {
241 7
            return [$line];
242
        }
243 2
        preg_match_all('/((?: |^).{15,' . ($maxLen - 40) . '}(?= |$))/', $line, $parts);
244 2
        $parts = $parts[0];
245
246 2
        return array_map('trim', $parts);
247
    }
248
249 2
    private static function printDescriptionWithComments($lines, $indentation, $firstInBlock) : string
250
    {
251 2
        $description = $indentation && ! $firstInBlock ? "\n" : '';
252 2
        foreach ($lines as $line) {
253 2
            if ($line === '') {
254 1
                $description .= $indentation . "#\n";
255
            } else {
256 2
                $description .= $indentation . '# ' . $line . "\n";
257
            }
258
        }
259
260 2
        return $description;
261
    }
262
263 5
    private static function escapeQuote($line) : string
264
    {
265 5
        return str_replace('"""', '\\"""', $line);
266
    }
267
268 54
    private static function printArgs($options, $args, $indentation = '') : string
269
    {
270 54
        if (! $args) {
271 38
            return '';
272
        }
273
274
        // If every arg does not have a description, print them on one line.
275 24
        if (Utils::every(
276 24
            $args,
277
            function ($arg) {
278 24
                return empty($arg->description);
279 24
            }
280
        )) {
281 22
            return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')';
282
        }
283
284 4
        return sprintf(
285 4
            "(\n%s\n%s)",
286 4
            implode(
287 4
                "\n",
288 4
                array_map(
289
                    function ($arg, $i) use ($indentation, $options) {
290 4
                        return self::printDescription($options, $arg, '  ' . $indentation, ! $i) . '  ' . $indentation .
291 4
                            self::printInputValue($arg);
292 4
                    },
293 4
                    $args,
294 4
                    array_keys($args)
295
                )
296
            ),
297 4
            $indentation
298
        );
299
    }
300
301 24
    private static function printInputValue($arg) : string
302
    {
303 24
        $argDecl = $arg->name . ': ' . (string) $arg->getType();
304 24
        if ($arg->defaultValueExists()) {
305 10
            $argDecl .= ' = ' . Printer::doPrint(AST::astFromValue($arg->defaultValue, $arg->getType()));
306
        }
307
308 24
        return $argDecl;
309
    }
310
311
    /**
312
     * @param bool[] $options
313
     */
314 54
    public static function printType(Type $type, array $options = []) : string
315
    {
316 54
        if ($type instanceof ScalarType) {
317 4
            return self::printScalar($type, $options);
318
        }
319
320 54
        if ($type instanceof ObjectType) {
321 54
            return self::printObject($type, $options);
322
        }
323
324 20
        if ($type instanceof InterfaceType) {
325 5
            return self::printInterface($type, $options);
326
        }
327
328 16
        if ($type instanceof UnionType) {
329 5
            return self::printUnion($type, $options);
330
        }
331
332 12
        if ($type instanceof EnumType) {
333 10
            return self::printEnum($type, $options);
334
        }
335
336 3
        if ($type instanceof InputObjectType) {
337 3
            return self::printInputObject($type, $options);
338
        }
339
340
        throw new Error(sprintf('Unknown type: %s.', Utils::printSafe($type)));
341
    }
342
343
    /**
344
     * @param bool[] $options
345
     */
346 4
    private static function printScalar(ScalarType $type, array $options) : string
347
    {
348 4
        return sprintf('%sscalar %s', self::printDescription($options, $type), $type->name);
349
    }
350
351
    /**
352
     * @param bool[] $options
353
     */
354 54
    private static function printObject(ObjectType $type, array $options) : string
355
    {
356 54
        $interfaces            = $type->getInterfaces();
357 54
        $implementedInterfaces = ! empty($interfaces) ?
358 5
            ' implements ' . implode(
359 5
                ' & ',
360 5
                array_map(
361
                    function ($i) {
362 5
                        return $i->name;
363 5
                    },
364 5
                    $interfaces
365
                )
366 54
            ) : '';
367
368 54
        return self::printDescription($options, $type) .
369 54
            sprintf("type %s%s {\n%s\n}", $type->name, $implementedInterfaces, self::printFields($options, $type));
370
    }
371
372
    /**
373
     * @param bool[] $options
374
     */
375 54
    private static function printFields($options, $type) : string
376
    {
377 54
        $fields = array_values($type->getFields());
378
379 54
        return implode(
380 54
            "\n",
381 54
            array_map(
382
                function ($f, $i) use ($options) {
383 54
                    return self::printDescription($options, $f, '  ', ! $i) . '  ' .
384 54
                        $f->name . self::printArgs($options, $f->args, '  ') . ': ' .
385 54
                        (string) $f->getType() . self::printDeprecated($f);
386 54
                },
387 54
                $fields,
388 54
                array_keys($fields)
389
            )
390
        );
391
    }
392
393 54
    private static function printDeprecated($fieldOrEnumVal) : string
394
    {
395 54
        $reason = $fieldOrEnumVal->deprecationReason;
396 54
        if (empty($reason)) {
397 54
            return '';
398
        }
399 3
        if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) {
400 1
            return ' @deprecated';
401
        }
402
403
        return ' @deprecated(reason: ' .
404 3
            Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')';
405
    }
406
407
    /**
408
     * @param bool[] $options
409
     */
410 5
    private static function printInterface(InterfaceType $type, array $options) : string
411
    {
412 5
        return self::printDescription($options, $type) .
413 5
            sprintf("interface %s {\n%s\n}", $type->name, self::printFields($options, $type));
414
    }
415
416
    /**
417
     * @param bool[] $options
418
     */
419 5
    private static function printUnion(UnionType $type, array $options) : string
420
    {
421 5
        return self::printDescription($options, $type) .
422 5
            sprintf('union %s = %s', $type->name, implode(' | ', $type->getTypes()));
423
    }
424
425
    /**
426
     * @param bool[] $options
427
     */
428 10
    private static function printEnum(EnumType $type, array $options) : string
429
    {
430 10
        return self::printDescription($options, $type) .
431 10
            sprintf("enum %s {\n%s\n}", $type->name, self::printEnumValues($type->getValues(), $options));
432
    }
433
434
    /**
435
     * @param bool[] $options
436
     */
437 10
    private static function printEnumValues($values, $options) : string
438
    {
439 10
        return implode(
440 10
            "\n",
441 10
            array_map(
442
                function ($value, $i) use ($options) {
443 10
                    return self::printDescription($options, $value, '  ', ! $i) . '  ' .
444 10
                        $value->name . self::printDeprecated($value);
445 10
                },
446 10
                $values,
447 10
                array_keys($values)
448
            )
449
        );
450
    }
451
452
    /**
453
     * @param bool[] $options
454
     */
455 3
    private static function printInputObject(InputObjectType $type, array $options) : string
456
    {
457 3
        $fields = array_values($type->getFields());
458
459 3
        return self::printDescription($options, $type) .
460 3
            sprintf(
461 3
                "input %s {\n%s\n}",
462 3
                $type->name,
463 3
                implode(
464 3
                    "\n",
465 3
                    array_map(
466
                        function ($f, $i) use ($options) {
467 3
                            return self::printDescription($options, $f, '  ', ! $i) . '  ' . self::printInputValue($f);
468 3
                        },
469 3
                        $fields,
470 3
                        array_keys($fields)
471
                    )
472
                )
473
            );
474
    }
475
476
    /**
477
     * @api
478
     * @param bool[] $options
479
     */
480 2
    public static function printIntrospectionSchema(Schema $schema, array $options = []) : string
481
    {
482 2
        return self::printFilteredSchema(
483 2
            $schema,
484 2
            [Directive::class, 'isSpecifiedDirective'],
485 2
            [Introspection::class, 'isIntrospectionType'],
486 2
            $options
487
        );
488
    }
489
}
490