DefinitionPrinter   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 546
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 201
c 5
b 0
f 0
dl 0
loc 546
rs 3.28
wmc 64

25 Methods

Rating   Name   Duplication   Size   Complexity  
A printObjectType() 0 16 2
A print() 0 15 4
A printSchema() 0 11 2
A printEnumType() 0 10 1
A printSchemaDefinition() 0 24 5
A printFilteredSchema() 0 13 1
A printIntrospectionSchema() 0 11 1
A printUnionType() 0 8 1
A printDirectiveDefinition() 0 10 1
A printScalarType() 0 5 1
B isSchemaOfCommonNames() 0 18 7
A printInterfaceType() 0 10 1
B printType() 0 22 7
A getSchemaDirectives() 0 3 1
A getSchemaTypes() 0 9 1
A printDeprecated() 0 15 5
A printArguments() 0 28 3
A printDescriptionWithComments() 0 12 5
A printOne() 0 3 1
A printInputValue() 0 12 3
A printFields() 0 19 1
A printInputObjectType() 0 17 1
A printMany() 0 5 1
A printEnumValues() 0 18 2
A printDescription() 0 22 6

How to fix   Complexity   

Complex Class

Complex classes like DefinitionPrinter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DefinitionPrinter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Digia\GraphQL\Schema;
4
5
use Digia\GraphQL\Error\InvariantException;
6
use Digia\GraphQL\Language\PrintException;
7
use Digia\GraphQL\Type\Definition\Argument;
8
use Digia\GraphQL\Type\Definition\DeprecationAwareInterface;
9
use Digia\GraphQL\Type\Definition\DescriptionAwareInterface;
10
use Digia\GraphQL\Type\Definition\Directive;
11
use Digia\GraphQL\Type\Definition\EnumType;
12
use Digia\GraphQL\Type\Definition\EnumValue;
13
use Digia\GraphQL\Type\Definition\Field;
14
use Digia\GraphQL\Type\Definition\InputField;
15
use Digia\GraphQL\Type\Definition\InputObjectType;
16
use Digia\GraphQL\Type\Definition\InputValueInterface;
17
use Digia\GraphQL\Type\Definition\InterfaceType;
18
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
19
use Digia\GraphQL\Type\Definition\ObjectType;
20
use Digia\GraphQL\Type\Definition\ScalarType;
21
use Digia\GraphQL\Type\Definition\UnionType;
22
use Digia\GraphQL\Util\ValueConverter;
23
use function Digia\GraphQL\Language\printBlockString;
24
use function Digia\GraphQL\printNode;
25
use function Digia\GraphQL\Type\isIntrospectionType;
26
use function Digia\GraphQL\Type\isSpecifiedScalarType;
27
use function Digia\GraphQL\Type\stringType;
28
use function Digia\GraphQL\Util\arrayEvery;
29
use function Digia\GraphQL\Util\toString;
30
31
class DefinitionPrinter implements DefinitionPrinterInterface
32
{
33
    /**
34
     * @var array
35
     */
36
    protected $options;
37
38
    /**
39
     * @inheritdoc
40
     * @throws PrintException
41
     * @throws InvariantException
42
     */
43
    public function printSchema(Schema $schema, array $options = []): string
44
    {
45
        $this->options = $options;
46
47
        return $this->printFilteredSchema(
48
            $schema,
49
            function (Directive $directive): bool {
50
                return !isSpecifiedDirective($directive);
51
            },
52
            function (NamedTypeInterface $type): bool {
53
                return !isSpecifiedScalarType($type) && !isIntrospectionType($type);
54
            }
55
        );
56
    }
57
58
    /**
59
     * @inheritdoc
60
     * @throws PrintException
61
     * @throws InvariantException
62
     */
63
    public function printIntrospectionSchema(Schema $schema, array $options = []): string
64
    {
65
        $this->options = $options;
66
67
        return $this->printFilteredSchema(
68
            $schema,
69
            function (Directive $directive): bool {
70
                return isSpecifiedDirective($directive);
71
            },
72
            function (NamedTypeInterface $type): bool {
73
                return isIntrospectionType($type);
74
            }
75
        );
76
    }
77
78
    /**
79
     * @param DefinitionInterface $definition
80
     * @return string
81
     * @throws PrintException
82
     * @throws InvariantException
83
     */
84
    public function print(DefinitionInterface $definition): string
85
    {
86
        if ($definition instanceof Schema) {
87
            return $this->printSchemaDefinition($definition);
88
        }
89
90
        if ($definition instanceof Directive) {
91
            return $this->printDirectiveDefinition($definition);
92
        }
93
94
        if ($definition instanceof NamedTypeInterface) {
95
            return $this->printType($definition);
96
        }
97
98
        throw new PrintException(\sprintf('Invalid definition object: %s.', toString($definition)));
99
    }
100
101
    /**
102
     * @param Schema   $schema
103
     * @param callable $directiveFilter
104
     * @param callable $typeFilter
105
     * @return string
106
     * @throws PrintException
107
     * @throws InvariantException
108
     */
109
    protected function printFilteredSchema(
110
        Schema $schema,
111
        callable $directiveFilter,
112
        callable $typeFilter
113
    ): string {
114
        /** @noinspection PhpParamsInspection */
115
        $lines = \array_filter(\array_merge(
116
            [$this->printOne($schema)],
117
            $this->printMany($this->getSchemaDirectives($schema, $directiveFilter)),
118
            $this->printMany($this->getSchemaTypes($schema, $typeFilter))
119
        ));
120
121
        return printArray("\n\n", $lines) . "\n";
122
    }
123
124
    /**
125
     * @param Schema   $schema
126
     * @param callable $filter
127
     * @return array
128
     */
129
    protected function getSchemaDirectives(Schema $schema, callable $filter): array
130
    {
131
        return \array_filter($schema->getDirectives(), $filter);
132
    }
133
134
    /**
135
     * @param Schema   $schema
136
     * @param callable $filter
137
     * @return array
138
     */
139
    protected function getSchemaTypes(Schema $schema, callable $filter): array
140
    {
141
        $types = \array_filter(\array_values($schema->getTypeMap()), $filter);
142
143
        \usort($types, function (NamedTypeInterface $typeA, NamedTypeInterface $typeB) {
144
            return \strcasecmp($typeA->getName(), $typeB->getName());
145
        });
146
147
        return $types;
148
    }
149
150
    /**
151
     * @param Schema $definition
152
     * @return string
153
     */
154
    protected function printSchemaDefinition(Schema $definition): string
155
    {
156
        if ($this->isSchemaOfCommonNames($definition)) {
157
            return '';
158
        }
159
160
        $operationTypes = [];
161
162
        if (null !== ($queryType = $definition->getQueryType())) {
163
            $operationTypes[] = "  query: {$queryType->getName()}";
164
        }
165
166
        if (null !== ($mutationType = $definition->getMutationType())) {
167
            $operationTypes[] = "  mutation: {$mutationType->getName()}";
168
        }
169
170
        if (null !== ($subscriptionType = $definition->getSubscriptionType())) {
171
            $operationTypes[] = "  subscription: {$subscriptionType->getName()}";
172
        }
173
174
        return printLines([
175
            'schema {',
176
            printLines($operationTypes),
177
            '}'
178
        ]);
179
    }
180
181
    /**
182
     * GraphQL schema define root types for each type of operation. These types are
183
     * the same as any other type and can be named in any manner, however there is
184
     * a common naming convention:
185
     *
186
     *   schema {
187
     *     query: Query
188
     *     mutation: Mutation
189
     *     subscription: Subscription
190
     *   }
191
     *
192
     * When using this naming convention, the schema description can be omitted.
193
     *
194
     * @param Schema $schema
195
     * @return bool
196
     */
197
    protected function isSchemaOfCommonNames(Schema $schema): bool
198
    {
199
        if (null !== ($queryType = $schema->getQueryType()) &&
200
            $queryType->getName() !== 'Query') {
201
            return false;
202
        }
203
204
        if (null !== ($mutationType = $schema->getMutationType()) &&
205
            $mutationType->getName() !== 'Mutation') {
206
            return false;
207
        }
208
209
        if (null !== ($subscriptionType = $schema->getSubscriptionType()) &&
210
            $subscriptionType->getName() !== 'Subscription') {
211
            return false;
212
        }
213
214
        return true;
215
    }
216
217
    /**
218
     * @param Directive $directive
219
     * @return string
220
     */
221
    public function printDirectiveDefinition(Directive $directive): string
222
    {
223
        $description = $this->printDescription($directive);
224
        $name        = $directive->getName();
225
        $arguments   = $this->printArguments($directive->getArguments());
226
        $locations   = implode(' | ', $directive->getLocations());
227
228
        return printLines([
229
            $description,
230
            "directive @{$name}{$arguments} on {$locations}",
231
        ]);
232
    }
233
234
    /**
235
     * @param NamedTypeInterface $type
236
     * @return string
237
     * @throws PrintException
238
     * @throws InvariantException
239
     */
240
    protected function printType(NamedTypeInterface $type): string
241
    {
242
        if ($type instanceof ScalarType) {
243
            return $this->printScalarType($type);
244
        }
245
        if ($type instanceof ObjectType) {
246
            return $this->printObjectType($type);
247
        }
248
        if ($type instanceof InterfaceType) {
249
            return $this->printInterfaceType($type);
250
        }
251
        if ($type instanceof UnionType) {
252
            return $this->printUnionType($type);
253
        }
254
        if ($type instanceof EnumType) {
255
            return $this->printEnumType($type);
256
        }
257
        if ($type instanceof InputObjectType) {
258
            return $this->printInputObjectType($type);
259
        }
260
261
        throw new PrintException(\sprintf('Unknown type: %s', (string)$type));
262
    }
263
264
    /**
265
     * @param ScalarType $type
266
     * @return string
267
     */
268
    protected function printScalarType(ScalarType $type): string
269
    {
270
        return printLines([
271
            $this->printDescription($type),
272
            "scalar {$type->getName()}"
273
        ]);
274
    }
275
276
    /**
277
     * @param ObjectType $type
278
     * @return string
279
     * @throws InvariantException
280
     */
281
    protected function printObjectType(ObjectType $type): string
282
    {
283
        $description = $this->printDescription($type);
284
        $name        = $type->getName();
285
        $implements  = $type->hasInterfaces()
286
            ? ' implements ' . printArray(' & ', \array_map(function (InterfaceType $interface) {
287
                return $interface->getName();
288
            }, $type->getInterfaces()))
289
            : '';
290
        $fields      = $this->printFields($type->getFields());
291
292
        return printLines([
293
            $description,
294
            "type {$name}{$implements} {",
295
            $fields,
296
            '}'
297
        ]);
298
    }
299
300
    /**
301
     * @param InterfaceType $type
302
     * @return string
303
     * @throws InvariantException
304
     */
305
    protected function printInterfaceType(InterfaceType $type): string
306
    {
307
        $description = $this->printDescription($type);
308
        $fields      = $this->printFields($type->getFields());
309
310
        return printLines([
311
            $description,
312
            "interface {$type->getName()} {",
313
            $fields,
314
            '}'
315
        ]);
316
    }
317
318
    /**
319
     * @param UnionType $type
320
     * @return string
321
     * @throws InvariantException
322
     */
323
    protected function printUnionType(UnionType $type): string
324
    {
325
        $description = $this->printDescription($type);
326
        $types       = printArray(' | ', $type->getTypes());
327
328
        return printLines([
329
            $description,
330
            "union {$type->getName()} = {$types}"
331
        ]);
332
    }
333
334
    /**
335
     * @param EnumType $type
336
     * @return string
337
     * @throws InvariantException
338
     */
339
    protected function printEnumType(EnumType $type): string
340
    {
341
        $description = $this->printDescription($type);
342
        $values      = $this->printEnumValues($type->getValues());
343
344
        return printLines([
345
            $description,
346
            "enum {$type->getName()} {",
347
            $values,
348
            '}'
349
        ]);
350
    }
351
352
    /**
353
     * @param array $values
354
     * @return string
355
     */
356
    protected function printEnumValues(array $values): string
357
    {
358
        // The first item is always the first in block, all item after that are not.
359
        // This is important for getting the linebreaks correct between the items.
360
        $firstInBlock = true;
361
362
        return printLines(\array_map(function (EnumValue $value) use (&$firstInBlock): string {
363
            $description  = $this->printDescription($value, '  ', $firstInBlock);
364
            $name         = $value->getName();
365
            $deprecated   = $this->printDeprecated($value);
366
            $enum         = empty($deprecated) ? $name : "{$name} {$deprecated}";
367
            $firstInBlock = false;
368
369
            return printLines([
370
                $description,
371
                "  {$enum}"
372
            ]);
373
        }, $values));
374
    }
375
376
    /**
377
     * @param InputObjectType $type
378
     * @return string
379
     * @throws InvariantException
380
     */
381
    protected function printInputObjectType(InputObjectType $type): string
382
    {
383
        $description = $this->printDescription($type);
384
        $fields      = \array_map(function (InputField $field): string {
385
            $description = $this->printDescription($field, '  ');
386
            $inputValue  = $this->printInputValue($field);
387
            return printLines([
388
                $description,
389
                "  {$inputValue}"
390
            ]);
391
        }, \array_values($type->getFields()));
392
393
        return printLines([
394
            $description,
395
            "input {$type->getName()} {",
396
            printLines($fields),
397
            '}'
398
        ]);
399
    }
400
401
    /**
402
     * @param InputValueInterface $inputValue
403
     * @return string
404
     * @throws InvariantException
405
     * @throws \Digia\GraphQL\Language\SyntaxErrorException
406
     * @throws \Digia\GraphQL\Util\ConversionException
407
     */
408
    protected function printInputValue(InputValueInterface $inputValue): string
409
    {
410
        $type = $inputValue->getType();
411
        $name = $inputValue->getName();
412
413
        $defaultValue = $inputValue->hasDefaultValue()
414
            ? printNode(ValueConverter::convert($inputValue->getDefaultValue(), $type))
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type null; however, parameter $type of Digia\GraphQL\Util\ValueConverter::convert() does only seem to accept Digia\GraphQL\Type\Definition\TypeInterface, 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

414
            ? printNode(ValueConverter::convert($inputValue->getDefaultValue(), /** @scrutinizer ignore-type */ $type))
Loading history...
415
            : null;
416
417
        return null !== $defaultValue
418
            ? "{$name}: {$type} = {$defaultValue}"
419
            : "{$name}: {$type}";
420
    }
421
422
    /**
423
     * @param array $fields
424
     * @return string
425
     */
426
    protected function printFields(array $fields): string
427
    {
428
        // The first item is always the first in block, all item after that are not.
429
        // This is important for getting the linebreaks correct between the items.
430
        $firstInBlock = true;
431
432
        return printLines(\array_map(function (Field $field) use (&$firstInBlock): string {
433
            $description  = $this->printDescription($field, '  ', $firstInBlock);
434
            $name         = $field->getName();
435
            $arguments    = $this->printArguments($field->getArguments());
436
            $type         = (string)$field->getType();
437
            $deprecated   = $this->printDeprecated($field);
438
            $firstInBlock = false;
439
440
            return printLines([
441
                $description,
442
                "  {$name}{$arguments}: {$type}{$deprecated}"
443
            ]);
444
        }, $fields));
445
    }
446
447
    /**
448
     * @param array  $arguments
449
     * @param string $indentation
450
     * @return string
451
     */
452
    protected function printArguments(array $arguments, string $indentation = ''): string
453
    {
454
        if (empty($arguments)) {
455
            return '';
456
        }
457
458
        // If every arg does not have a description, print them on one line.
459
        if (arrayEvery($arguments, function (Argument $argument): bool {
460
            return !$argument->hasDescription();
461
        })) {
462
            return printInputFields(\array_map(function (Argument $argument) {
463
                return $this->printInputValue($argument);
464
            }, $arguments));
465
        }
466
467
        $args = \array_map(function (Argument $argument) use ($indentation) {
468
            $description = $this->printDescription($argument, '  ');
469
            $inputValue  = $this->printInputValue($argument);
470
            return printLines([
471
                "{$indentation}{$description}",
472
                "  {$indentation}{$inputValue}"
473
            ]);
474
        }, $arguments);
475
476
        return printLines([
477
            '(',
478
            \implode(", ", $args),
479
            $indentation . ')'
480
        ]);
481
    }
482
483
    /**
484
     * @param DeprecationAwareInterface $fieldOrEnumValue
485
     * @return string
486
     * @throws InvariantException
487
     * @throws \Digia\GraphQL\Language\SyntaxErrorException
488
     * @throws \Digia\GraphQL\Util\ConversionException
489
     */
490
    protected function printDeprecated(DeprecationAwareInterface $fieldOrEnumValue): string
491
    {
492
        if (!$fieldOrEnumValue->isDeprecated()) {
493
            return '';
494
        }
495
496
        $reason = $fieldOrEnumValue->getDeprecationReason();
497
498
        if (null === $reason || '' === $reason || DEFAULT_DEPRECATION_REASON === $reason) {
499
            return '@deprecated';
500
        }
501
502
        $reasonValue = printNode(ValueConverter::convert($reason, stringType()));
503
504
        return "@deprecated(reason: {$reasonValue})";
505
    }
506
507
    /**
508
     * @param DescriptionAwareInterface $definition
509
     * @param string                    $indentation
510
     * @param bool                      $isFirstInBlock
511
     * @return string
512
     */
513
    protected function printDescription(
514
        DescriptionAwareInterface $definition,
515
        string $indentation = '',
516
        bool $isFirstInBlock = true
517
    ): string {
518
        // Don't print anything if the type has no description
519
        if ($definition->getDescription() === null) {
520
            return '';
521
        }
522
523
        $lines = descriptionLines($definition->getDescription(), 120 - \strlen($indentation));
524
525
        if (isset($this->options['commentDescriptions']) && true === $this->options['commentDescriptions']) {
526
            return $this->printDescriptionWithComments($lines, $indentation, $isFirstInBlock);
527
        }
528
529
        $text                = \implode("\n", $lines);
530
        $preferMultipleLines = \strlen($text) > 70;
531
        $blockString         = printBlockString($text, '', $preferMultipleLines);
532
        $prefix              = strlen($indentation) > 0 && !$isFirstInBlock ? "\n" . $indentation : $indentation;
533
534
        return $prefix . \str_replace("\n", "\n" . $indentation, $blockString);
535
    }
536
537
    /**
538
     * @param array  $lines
539
     * @param string $indentation
540
     * @param bool   $isFirstInBlock
541
     * @return string
542
     */
543
    protected function printDescriptionWithComments(array $lines, string $indentation, bool $isFirstInBlock): string
544
    {
545
        $description = \strlen($indentation) > 0 && !$isFirstInBlock ? "\n" : '';
546
        $linesCount  = \count($lines);
547
548
        for ($i = 0; $i < $linesCount; $i++) {
549
            $description .= $lines[$i] === ''
550
                ? $indentation . '#' . "\n"
551
                : $indentation . '# ' . $lines[$i] . "\n";
552
        }
553
554
        return $description;
555
    }
556
557
    /**
558
     * @param DefinitionInterface $definition
559
     * @return string
560
     * @throws PrintException
561
     * @throws InvariantException
562
     */
563
    protected function printOne(DefinitionInterface $definition): string
564
    {
565
        return $this->print($definition);
566
    }
567
568
    /**
569
     * @param DefinitionInterface[] $definitions
570
     * @return array
571
     */
572
    protected function printMany(array $definitions): array
573
    {
574
        return \array_map(function ($definition) {
575
            return $this->print($definition);
576
        }, $definitions);
577
    }
578
}
579