Passed
Pull Request — master (#209)
by Christoffer
02:13
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\DefinitionInterface;
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\InputObjectType;
12
use Digia\GraphQL\Type\Definition\InterfaceType;
13
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
14
use Digia\GraphQL\Type\Definition\ObjectType;
15
use Digia\GraphQL\Type\Definition\ScalarType;
16
use Digia\GraphQL\Type\Definition\TypeInterface;
17
use Digia\GraphQL\Type\Definition\UnionType;
18
use function Digia\GraphQL\Type\isIntrospectionType;
19
use function Digia\GraphQL\Type\isSpecifiedScalarType;
20
use function Digia\GraphQL\Util\toString;
21
22
class DefinitionPrinter implements DefinitionPrinterInterface
23
{
24
    /**
25
     * @var array
26
     */
27
    protected $options;
28
29
    /**
30
     * @inheritdoc
31
     * @throws PrintException
32
     */
33
    public function printSchema(SchemaInterface $schema, array $options = []): string
34
    {
35
        $this->options = $options;
36
37
        return $this->printFilteredSchema(
38
            $schema,
39
            function (Directive $directive): bool {
40
                return !isSpecifiedDirective($directive);
41
            },
42
            function (TypeInterface $type): bool {
43
                return !isSpecifiedScalarType($type) && !isIntrospectionType($type);
44
            }
45
        );
46
    }
47
48
    /**
49
     * @inheritdoc
50
     * @throws PrintException
51
     */
52
    public function printIntrospectionSchema(SchemaInterface $schema, array $options = []): string
53
    {
54
        $this->options = $options;
55
56
        return $this->printFilteredSchema(
57
            $schema,
58
            function (Directive $directive): bool {
59
                return isSpecifiedDirective($directive);
60
            },
61
            function (TypeInterface $type): bool {
62
                return isIntrospectionType($type);
63
            }
64
        );
65
    }
66
67
    /**
68
     * @param mixed $definition
69
     * @return string
70
     * @throws PrintException
71
     */
72
    public function print(DefinitionInterface $definition): string
73
    {
74
        if ($definition instanceof Schema) {
75
            return $this->printSchemaDefinition($definition);
76
        }
77
        if ($definition instanceof NamedTypeInterface) {
78
            return $this->printType($definition);
79
        }
80
81
        throw new PrintException(\sprintf('Invalid definition object: %s.', toString($definition)));
82
    }
83
84
    /**
85
     * @param SchemaInterface $schema
86
     * @param callable        $directiveFilter
87
     * @param callable        $typeFilter
88
     * @return string
89
     * @throws PrintException
90
     */
91
    protected function printFilteredSchema(
92
        SchemaInterface $schema,
93
        callable $directiveFilter,
94
        callable $typeFilter
95
    ): string {
96
        /** @noinspection PhpParamsInspection */
97
        $lines = \array_filter(\array_merge(
98
            [$this->printOne($schema)],
99
            $this->printMany($this->getSchemaDirectives($schema, $directiveFilter)),
100
            $this->printMany($this->getSchemaTypes($schema, $typeFilter))
101
        ));
102
103
        return printArray($lines, "\n\n") . "\n";
104
    }
105
106
    /**
107
     * @param SchemaInterface $schema
108
     * @param callable        $filter
109
     * @return array
110
     */
111
    protected function getSchemaDirectives(SchemaInterface $schema, callable $filter): array
112
    {
113
        return \array_filter($schema->getDirectives(), $filter);
114
    }
115
116
    /**
117
     * @param SchemaInterface $schema
118
     * @param callable        $filter
119
     * @return array
120
     */
121
    protected function getSchemaTypes(SchemaInterface $schema, callable $filter): array
122
    {
123
        $types = \array_filter(\array_values($schema->getTypeMap()), $filter);
124
125
        \usort($types, function (TypeInterface $typeA, TypeInterface $typeB) {
126
            return \strcasecmp($typeA->getName(), $typeB->getName());
127
        });
128
129
        return $types;
130
    }
131
132
    /**
133
     * @param Schema $definition
134
     * @return string
135
     */
136
    protected function printSchemaDefinition(Schema $definition): string
137
    {
138
        if ($this->isSchemaOfCommonNames($definition)) {
139
            return '';
140
        }
141
142
        $operationTypes = [];
143
144
        if (null !== ($queryType = $definition->getQueryType())) {
145
            $operationTypes[] = "  query: {$queryType->getName()}";
146
        }
147
148
        if (null !== ($mutationType = $definition->getMutationType())) {
149
            $operationTypes[] = "  mutation: {$mutationType->getName()}";
150
        }
151
152
        if (null !== ($subscriptionType = $definition->getSubscriptionType())) {
153
            $operationTypes[] = "  subscription: {$subscriptionType->getName()}";
154
        }
155
156
        return printLines([
157
            'schema {\n',
158
              printLines($operationTypes),
159
            '}'
160
        ]);
161
    }
162
163
    /**
164
     * GraphQL schema define root types for each type of operation. These types are
165
     * the same as any other type and can be named in any manner, however there is
166
     * a common naming convention:
167
     *
168
     *   schema {
169
     *     query: Query
170
     *     mutation: Mutation
171
     *     subscription: Subscription
172
     *   }
173
     *
174
     * When using this naming convention, the schema description can be omitted.
175
     *
176
     * @param SchemaInterface $schema
177
     * @return bool
178
     */
179
    protected function isSchemaOfCommonNames(SchemaInterface $schema): bool
180
    {
181
        if (null !== ($queryType = $schema->getQueryType()) &&
182
            $queryType->getName() !== 'Query') {
183
            return false;
184
        }
185
186
        if (null !== ($mutationType = $schema->getMutationType()) &&
187
            $mutationType->getName() !== 'Mutation') {
188
            return false;
189
        }
190
191
        if (null !== ($subscriptionType = $schema->getSubscriptionType()) &&
192
            $subscriptionType->getName() !== 'Subscription') {
193
            return false;
194
        }
195
196
        return true;
197
    }
198
199
    /**
200
     * @param NamedTypeInterface $type
201
     * @return string
202
     * @throws PrintException
203
     */
204
    protected function printType(NamedTypeInterface $type): string
205
    {
206
        if ($type instanceof ScalarType) {
207
            return $this->printScalarType($type);
208
        }
209
        if ($type instanceof ObjectType) {
210
            return $this->printObjectType($type);
211
        }
212
        if ($type instanceof InterfaceType) {
213
            return $this->printInterfaceType($type);
0 ignored issues
show
Bug introduced by
The method printInterfaceType() 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

213
            return $this->/** @scrutinizer ignore-call */ printInterfaceType($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...
214
        }
215
        if ($type instanceof UnionType) {
216
            return $this->printUnionType($type);
0 ignored issues
show
Bug introduced by
The method printUnionType() does not exist on Digia\GraphQL\Schema\DefinitionPrinter. Did you maybe mean printType()? ( Ignorable by Annotation )

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

216
            return $this->/** @scrutinizer ignore-call */ printUnionType($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...
217
        }
218
        if ($type instanceof EnumType) {
219
            return $this->printEnumType($type);
0 ignored issues
show
Bug introduced by
The method printEnumType() does not exist on Digia\GraphQL\Schema\DefinitionPrinter. Did you maybe mean printType()? ( Ignorable by Annotation )

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

219
            return $this->/** @scrutinizer ignore-call */ printEnumType($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...
220
        }
221
        if ($type instanceof InputObjectType) {
222
            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

222
            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...
223
        }
224
225
        throw new PrintException(\sprintf('Unknown type: %s', (string)$type));
226
    }
227
228
    /**
229
     * @param ScalarType $type
230
     * @return string
231
     */
232
    protected function printScalarType(ScalarType $type): string
233
    {
234
        return printLines([
235
            $this->printDescription($type),
236
            "scalar {$type->getName()}"
237
        ]);
238
    }
239
240
    /**
241
     * @param ObjectType $type
242
     * @return string
243
     * @throws InvariantException
244
     */
245
    protected function printObjectType(ObjectType $type): string
246
    {
247
        $description = $this->printDescription($type);
248
        $name        = $type->getName();
249
        $implements  = $type->hasInterfaces()
250
            ? ' implements ' . printArray(\array_map(function (InterfaceType $interface) {
251
                return $interface->getName();
252
            }, $type->getInterfaces()), ' & ')
253
            : '';
254
        $fields      = $this->printMany($type->getFields());
255
256
        return printLines([
257
            $description,
258
            "type {$name}{$implements} {",
259
            $fields,
260
            '}'
261
        ]);
262
    }
263
264
    /**
265
     * @param NamedTypeInterface $type
266
     * @param string             $indentation
267
     * @param bool               $isFirstInBlock
268
     * @return string
269
     */
270
    protected function printDescription(
271
        NamedTypeInterface $type,
272
        string $indentation = '',
273
        bool $isFirstInBlock = true
274
    ): string {
275
        if (!($type instanceof DescriptionAwareInterface)) {
276
            return '';
277
        }
278
279
        $lines      = descriptionLines($type->getDescription(), 120 - \strlen($indentation));
280
        $linesCount = \count($lines);
281
282
        if (isset($this->options['commentDescriptions'])) {
283
            return $this->printDescriptionWithComments($lines, $indentation, $isFirstInBlock);
284
        }
285
286
        $description = $indentation && !$isFirstInBlock
287
            ? "\n" . $indentation . '"""'
288
            : $indentation . '"""';
289
290
        // In some circumstances, a single line can be used for the description.
291
        if (
292
            $linesCount === 1 &&
293
            ($firstLineLength = \strlen($lines[0])) < 70 &&
294
            $lines[0][$firstLineLength - 1] !== '"'
295
        ) {
296
            return $description . escapeQuote($lines[0]) . '"""' . "\n";
297
        }
298
299
        // Format a multi-line block quote to account for leading space.
300
        $hasLeadingSpace = $lines[0][0] === ' ' || $lines[0][0] === "\t";
301
        if (!$hasLeadingSpace) {
302
            $description .= "\n";
303
        }
304
305
        for ($i = 0; $i < $linesCount; $i++) {
306
            $description .= $i !== 0 || !$hasLeadingSpace
307
                ? $description .= $indentation
308
                : escapeQuote($lines[$i]) . "\n";
309
        }
310
311
        $description .= $indentation . '"""';
312
313
        return $description;
314
    }
315
316
    protected function printDescriptionWithComments(array $lines, string $indentation, bool $isFirstInBlock): string
317
    {
318
        $description = $indentation && !$isFirstInBlock ? "\n" : '';
319
        $linesCount  = \count($lines);
320
321
        for ($i = 0; $i < $linesCount; $i++) {
322
            $description .= $lines[$i] === ''
323
                ? $indentation . '#' . "\n"
324
                : $indentation . '# ' . $lines[$i] . "\n";
325
        }
326
327
        return $description;
328
    }
329
330
    /**
331
     * @param DefinitionInterface $definition
332
     * @return string
333
     * @throws PrintException
334
     */
335
    protected function printOne(DefinitionInterface $definition): string
336
    {
337
        return $this->print($definition);
338
    }
339
340
    /**
341
     * @param DefinitionInterface[] $definitions
342
     * @return array
343
     */
344
    protected function printMany(array $definitions): array
345
    {
346
        return \array_map(function ($definition) {
347
            return $this->print($definition);
348
        }, $definitions);
349
    }
350
}
351