Completed
Push — master ( 4401f4...a01b08 )
by Vladimir
36:08 queued 32:42
created

BuildSchema::buildAST()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Utils;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Language\AST\DirectiveDefinitionNode;
9
use GraphQL\Language\AST\DocumentNode;
10
use GraphQL\Language\AST\EnumTypeDefinitionNode;
11
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
12
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
13
use GraphQL\Language\AST\Node;
14
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
15
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
16
use GraphQL\Language\AST\SchemaDefinitionNode;
17
use GraphQL\Language\AST\UnionTypeDefinitionNode;
18
use GraphQL\Language\Parser;
19
use GraphQL\Language\Source;
20
use GraphQL\Type\Definition\Directive;
21
use GraphQL\Type\Schema;
22
use function array_map;
23
use function array_reduce;
24
use function sprintf;
25
26
/**
27
 * Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST)
28
 * See [section in docs](type-system/type-language.md) for details.
29
 */
30
class BuildSchema
31
{
32
    /** @var DocumentNode */
33
    private $ast;
34
35
    /** @var Node[] */
36
    private $nodeMap;
37
38
    /** @var callable|null */
39
    private $typeConfigDecorator;
40
41
    /** @var bool[] */
42
    private $options;
43
44
    /**
45
     * @param bool[] $options
46
     */
47 11
    public function __construct(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [])
48
    {
49 11
        $this->ast                 = $ast;
50 11
        $this->typeConfigDecorator = $typeConfigDecorator;
51 11
        $this->options             = $options;
52 11
    }
53
54
    /**
55
     * A helper function to build a GraphQLSchema directly from a source
56
     * document.
57
     *
58
     * @param DocumentNode|Source|string $source
59
     * @param bool[]                     $options
60
     *
61
     * @return Schema
62
     *
63
     * @api
64
     */
65
    public static function build($source, ?callable $typeConfigDecorator = null, array $options = [])
66
    {
67
        $doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
68
69
        return self::buildAST($doc, $typeConfigDecorator, $options);
70
    }
71
72
    /**
73
     * This takes the ast of a schema document produced by the parse function in
74
     * GraphQL\Language\Parser.
75
     *
76
     * If no schema definition is provided, then it will look for types named Query
77
     * and Mutation.
78
     *
79
     * Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
80
     * has no resolve methods, so execution will use default resolvers.
81
     *
82
     * Accepts options as a third argument:
83
     *
84
     *    - commentDescriptions:
85
     *        Provide true to use preceding comments as the description.
86
     *
87
     * @param bool[] $options
88
     *
89
     * @return Schema
90
     *
91
     * @throws Error
92
     *
93
     * @api
94
     */
95 11
    public static function buildAST(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [])
96
    {
97 11
        $builder = new self($ast, $typeConfigDecorator, $options);
98
99 11
        return $builder->buildSchema();
100
    }
101
102 11
    public function buildSchema()
103
    {
104 11
        $schemaDef     = null;
105 11
        $typeDefs      = [];
106 11
        $this->nodeMap = [];
107 11
        $directiveDefs = [];
108 11
        foreach ($this->ast->definitions as $definition) {
109
            switch (true) {
110 11
                case $definition instanceof SchemaDefinitionNode:
111 10
                    if ($schemaDef !== null) {
112 1
                        throw new Error('Must provide only one schema definition.');
113
                    }
114 10
                    $schemaDef = $definition;
115 10
                    break;
116 10
                case $definition instanceof ScalarTypeDefinitionNode:
117 10
                case $definition instanceof ObjectTypeDefinitionNode:
118 3
                case $definition instanceof InterfaceTypeDefinitionNode:
119 3
                case $definition instanceof EnumTypeDefinitionNode:
120 3
                case $definition instanceof UnionTypeDefinitionNode:
121 3
                case $definition instanceof InputObjectTypeDefinitionNode:
122 8
                    $typeName = $definition->name->value;
0 ignored issues
show
Bug introduced by
Accessing name on the interface GraphQL\Language\AST\DefinitionNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
123 8
                    if (! empty($this->nodeMap[$typeName])) {
124 1
                        throw new Error(sprintf('Type "%s" was defined more than once.', $typeName));
125
                    }
126 8
                    $typeDefs[]               = $definition;
127 8
                    $this->nodeMap[$typeName] = $definition;
128 8
                    break;
129 3
                case $definition instanceof DirectiveDefinitionNode:
130 1
                    $directiveDefs[] = $definition;
131 11
                    break;
132
            }
133
        }
134
135 9
        $operationTypes = $schemaDef !== null
136 8
            ? $this->getOperationTypes($schemaDef)
137
            : [
138 1
                'query'        => isset($this->nodeMap['Query']) ? 'Query' : null,
139 1
                'mutation'     => isset($this->nodeMap['Mutation']) ? 'Mutation' : null,
140 1
                'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null,
141
            ];
142
143 1
        $DefinitionBuilder = new ASTDefinitionBuilder(
144 1
            $this->nodeMap,
145 1
            $this->options,
146
            static function ($typeName) {
147
                throw new Error('Type "' . $typeName . '" not found in document.');
148 1
            },
149 1
            $this->typeConfigDecorator
150
        );
151
152 1
        $directives = array_map(
153
            static function ($def) use ($DefinitionBuilder) {
154 1
                return $DefinitionBuilder->buildDirective($def);
155 1
            },
156 1
            $directiveDefs
157
        );
158
159
        // If specified directives were not explicitly declared, add them.
160 1
        $skip = array_reduce(
161 1
            $directives,
162
            static function (bool $hasSkip, Directive $directive) : bool {
163 1
                return $hasSkip || $directive->name === 'skip';
164 1
            },
165 1
            false
166
        );
167 1
        if (! $skip) {
168
            $directives[] = Directive::skipDirective();
169
        }
170
171 1
        $include = array_reduce(
172 1
            $directives,
173
            static function (bool $hasInclude, Directive $directive) : bool {
174 1
                return $hasInclude || $directive->name === 'include';
175 1
            },
176 1
            false
177
        );
178 1
        if (! $include) {
179
            $directives[] = Directive::includeDirective();
180
        }
181
182 1
        $deprecated = array_reduce(
183 1
            $directives,
184
            static function (bool $hasDeprecated, Directive $directive) : bool {
185 1
                return $hasDeprecated || $directive->name === 'deprecated';
186 1
            },
187 1
            false
188
        );
189 1
        if (! $deprecated) {
190
            $directives[] = Directive::deprecatedDirective();
191
        }
192
193
        // Note: While this could make early assertions to get the correctly
194
        // typed values below, that would throw immediately while type system
195
        // validation with validateSchema() will produce more actionable results.
196
197 1
        return new Schema([
198 1
            'query'        => isset($operationTypes['query'])
199 1
                ? $DefinitionBuilder->buildType($operationTypes['query'])
200
                : null,
201 1
            'mutation'     => isset($operationTypes['mutation'])
202
                ? $DefinitionBuilder->buildType($operationTypes['mutation'])
203
                : null,
204 1
            'subscription' => isset($operationTypes['subscription'])
205
                ? $DefinitionBuilder->buildType($operationTypes['subscription'])
206
                : null,
207
            'typeLoader'   => static function ($name) use ($DefinitionBuilder) {
208
                return $DefinitionBuilder->buildType($name);
209 1
            },
210 1
            'directives'   => $directives,
211 1
            'astNode'      => $schemaDef,
212
            'types'        => function () use ($DefinitionBuilder) {
213
                $types = [];
214
                foreach ($this->nodeMap as $name => $def) {
215
                    $types[] = $DefinitionBuilder->buildType($def->name->value);
216
                }
217
218
                return $types;
219 1
            },
220
        ]);
221
    }
222
223
    /**
224
     * @param SchemaDefinitionNode $schemaDef
225
     *
226
     * @return string[]
227
     *
228
     * @throws Error
229
     */
230 8
    private function getOperationTypes($schemaDef)
231
    {
232 8
        $opTypes = [];
233
234 8
        foreach ($schemaDef->operationTypes as $operationType) {
235 8
            $typeName  = $operationType->type->name->value;
236 8
            $operation = $operationType->operation;
237
238 8
            if (isset($opTypes[$operation])) {
239 3
                throw new Error(sprintf('Must provide only one %s type in schema.', $operation));
240
            }
241
242 8
            if (! isset($this->nodeMap[$typeName])) {
243 5
                throw new Error(sprintf('Specified %s type "%s" not found in document.', $operation, $typeName));
244
            }
245
246 5
            $opTypes[$operation] = $typeName;
247
        }
248
249
        return $opTypes;
250
    }
251
}
252