BuildSchema::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
c 0
b 0
f 0
rs 10
ccs 4
cts 4
cp 1
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\ObjectTypeDefinitionNode;
14
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
15
use GraphQL\Language\AST\SchemaDefinitionNode;
16
use GraphQL\Language\AST\TypeDefinitionNode;
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 GraphQL\Validator\DocumentValidator;
23
use function array_map;
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 TypeDefinitionNode[] */
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 157
    public function __construct(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [])
48
    {
49 157
        $this->ast                 = $ast;
50 157
        $this->typeConfigDecorator = $typeConfigDecorator;
51 157
        $this->options             = $options;
52 157
    }
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 113
    public static function build($source, ?callable $typeConfigDecorator = null, array $options = [])
66
    {
67 113
        $doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
68
69 113
        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 157
    public static function buildAST(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [])
96
    {
97 157
        $builder = new self($ast, $typeConfigDecorator, $options);
98
99 157
        return $builder->buildSchema();
100
    }
101
102 157
    public function buildSchema()
103
    {
104 157
        $options = $this->options;
105 157
        if (empty($options['assumeValid']) && empty($options['assumeValidSDL'])) {
106 154
            DocumentValidator::assertValidSDL($this->ast);
107
        }
108
109 156
        $schemaDef     = null;
110 156
        $typeDefs      = [];
111 156
        $this->nodeMap = [];
112 156
        $directiveDefs = [];
113 156
        foreach ($this->ast->definitions as $definition) {
114
            switch (true) {
115 156
                case $definition instanceof SchemaDefinitionNode:
116 31
                    $schemaDef = $definition;
117 31
                    break;
118 156
                case $definition instanceof ScalarTypeDefinitionNode:
119 156
                case $definition instanceof ObjectTypeDefinitionNode:
120 96
                case $definition instanceof InterfaceTypeDefinitionNode:
121 70
                case $definition instanceof EnumTypeDefinitionNode:
122 61
                case $definition instanceof UnionTypeDefinitionNode:
123 49
                case $definition instanceof InputObjectTypeDefinitionNode:
124 140
                    $typeName = $definition->name->value;
125 140
                    if (! empty($this->nodeMap[$typeName])) {
126 1
                        throw new Error(sprintf('Type "%s" was defined more than once.', $typeName));
127
                    }
128 140
                    $typeDefs[]               = $definition;
129 140
                    $this->nodeMap[$typeName] = $definition;
130 140
                    break;
131 31
                case $definition instanceof DirectiveDefinitionNode:
132 29
                    $directiveDefs[] = $definition;
133 156
                    break;
134
            }
135
        }
136
137 155
        $operationTypes = $schemaDef !== null
138 30
            ? $this->getOperationTypes($schemaDef)
139
            : [
140 146
                'query'        => isset($this->nodeMap['Query']) ? 'Query' : null,
141 132
                'mutation'     => isset($this->nodeMap['Mutation']) ? 'Mutation' : null,
142 132
                'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null,
143
            ];
144
145 146
        $DefinitionBuilder = new ASTDefinitionBuilder(
146 146
            $this->nodeMap,
147 146
            $this->options,
148
            static function ($typeName) {
149 5
                throw new Error('Type "' . $typeName . '" not found in document.');
150 146
            },
151 146
            $this->typeConfigDecorator
152
        );
153
154 146
        $directives = array_map(
155
            static function ($def) use ($DefinitionBuilder) {
156 28
                return $DefinitionBuilder->buildDirective($def);
157 146
            },
158 146
            $directiveDefs
159
        );
160
161
        // If specified directives were not explicitly declared, add them.
162 146
        $directivesByName = Utils::groupBy(
163 146
            $directives,
164
            static function (Directive $directive) : string {
165 28
                return $directive->name;
166 146
            }
167
        );
168 146
        if (! isset($directivesByName['skip'])) {
169 145
            $directives[] = Directive::skipDirective();
170
        }
171 146
        if (! isset($directivesByName['include'])) {
172 145
            $directives[] = Directive::includeDirective();
173
        }
174 146
        if (! isset($directivesByName['deprecated'])) {
175 145
            $directives[] = Directive::deprecatedDirective();
176
        }
177
178
        // Note: While this could make early assertions to get the correctly
179
        // typed values below, that would throw immediately while type system
180
        // validation with validateSchema() will produce more actionable results.
181
182 146
        return new Schema([
183 146
            'query'        => isset($operationTypes['query'])
184 127
                ? $DefinitionBuilder->buildType($operationTypes['query'])
185
                : null,
186 146
            'mutation'     => isset($operationTypes['mutation'])
187 7
                ? $DefinitionBuilder->buildType($operationTypes['mutation'])
188
                : null,
189 146
            'subscription' => isset($operationTypes['subscription'])
190 5
                ? $DefinitionBuilder->buildType($operationTypes['subscription'])
191
                : null,
192
            'typeLoader'   => static function ($name) use ($DefinitionBuilder) {
193 15
                return $DefinitionBuilder->buildType($name);
194 146
            },
195 146
            'directives'   => $directives,
196 146
            'astNode'      => $schemaDef,
197
            'types'        => function () use ($DefinitionBuilder) {
198 112
                $types = [];
199
                /** @var ScalarTypeDefinitionNode|ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|UnionTypeDefinitionNode|EnumTypeDefinitionNode|InputObjectTypeDefinitionNode $def */
200 112
                foreach ($this->nodeMap as $name => $def) {
201 112
                    $types[] = $DefinitionBuilder->buildType($def->name->value);
202
                }
203
204 112
                return $types;
205 146
            },
206
        ]);
207
    }
208
209
    /**
210
     * @param SchemaDefinitionNode $schemaDef
211
     *
212
     * @return string[]
213
     *
214
     * @throws Error
215
     */
216 30
    private function getOperationTypes($schemaDef)
217
    {
218 30
        $opTypes = [];
219
220 30
        foreach ($schemaDef->operationTypes as $operationType) {
221 30
            $typeName  = $operationType->type->name->value;
222 30
            $operation = $operationType->operation;
223
224 30
            if (isset($opTypes[$operation])) {
225 3
                throw new Error(sprintf('Must provide only one %s type in schema.', $operation));
226
            }
227
228 30
            if (! isset($this->nodeMap[$typeName])) {
229 6
                throw new Error(sprintf('Specified %s type "%s" not found in document.', $operation, $typeName));
230
            }
231
232 26
            $opTypes[$operation] = $typeName;
233
        }
234
235 21
        return $opTypes;
236
    }
237
}
238