Passed
Push — master ( 0f211d...735e54 )
by Bruno
07:58
created

Parser::getAST()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
nc 1
nop 0
crap 2
1
<?php declare(strict_types=1);
2
3
namespace Modelarium;
4
5
use Formularium\Formularium;
6
use GraphQL\Language\AST\ArgumentNode;
7
use GraphQL\Language\AST\DirectiveNode;
8
use GraphQL\Language\AST\DocumentNode;
9
use GraphQL\Language\AST\NodeKind;
10
use GraphQL\Language\AST\NodeList;
11
use GraphQL\Language\Visitor;
12
use GraphQL\Type\Definition\ListOfType;
13
use GraphQL\Type\Definition\NonNull;
14
use GraphQL\Type\Definition\ObjectType;
15
use GraphQL\Type\Definition\OutputType;
16
use GraphQL\Type\Definition\Type;
17
use GraphQL\Type\Schema;
18
use GraphQL\Utils\SchemaExtender;
19
use Modelarium\Exception\ScalarNotFoundException;
20
use Modelarium\Types\ScalarType;
21
22
class Parser
23
{
24
    /**
25
     * @var \GraphQL\Language\AST\DocumentNode
26
     */
27
    protected $ast;
28
29
    /**
30
     * @var \GraphQL\Type\Schema
31
     */
32
    protected $schema;
33
34
    /**
35
     * @var string[]
36
     */
37
    protected $scalars = [];
38
39
    /**
40
     * @var string[]
41
     */
42
    protected $imports = [];
43
44 33
    public function __construct()
45
    {
46 33
        $this->scalars = [
47
            'String' => 'Modelarium\\Types\\Datatype_string',
48
            'Int' => 'Modelarium\\Types\\Datatype_integer',
49
            'Float' => 'Modelarium\\Types\\Datatype_float',
50
            'Boolean' => 'Modelarium\\Types\\Datatype_bool',
51
        ];
52
53 33
        $this->imports = [
54 33
            'formularium.graphql' => \Safe\file_get_contents(__DIR__ . '/Types/Graphql/scalars.graphql'),
55
        ];
56 33
    }
57
58
    /** @phpstan-ignore-next-line */
59 29
    public static function extendDatatypes(array $typeConfig, $typeDefinitionNode): array
0 ignored issues
show
Unused Code introduced by
The parameter $typeDefinitionNode is not used and could be removed. ( Ignorable by Annotation )

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

59
    public static function extendDatatypes(array $typeConfig, /** @scrutinizer ignore-unused */ $typeDefinitionNode): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
60
    {
61
        /* TODO: extended datatypes
62
        if ($typeConfig['name'] === 'Email') {
63
            $typeConfig = array_merge($typeConfig, [
64
                'serialize' => function ($value) {
65
                    // ...
66
                },
67
                'parseValue' => function ($value) {
68
                    // ...
69
                },
70
                'parseLiteral' => function ($ast) {
71
                    // ...
72
                }
73
            ]);
74
        } */
75 29
        return $typeConfig;
76
    }
77
78
    /**
79
     * Returns a Parser from a string
80
     *
81
     * @param string $data the string
82
     * @return Parser
83
     */
84 30
    public function fromString(string $data): self
85
    {
86 30
        $this->ast = \GraphQL\Language\Parser::parse($data);
87 30
        $this->processAst();
88 30
        $schemaBuilder = new \GraphQL\Utils\BuildSchema(
89 30
            $this->ast,
90 30
            [__CLASS__, 'extendDatatypes']
91
        );
92
93 30
        $this->schema = $schemaBuilder->buildSchema();
94 30
        $this->processSchema();
95 30
        return $this;
96
    }
97
98
    /**
99
     *
100
     * @param string[] $sources
101
     * @return self
102
     */
103 1
    public function fromStrings(array $sources): self
104
    {
105 1
        $schema = new Schema([
106 1
            'query' => new ObjectType(['name' => 'Query']),
107 1
            'mutation' => new ObjectType(['name' => 'Mutation']),
108
        ]);
109
110 1
        foreach ($sources as &$s) {
111 1
            $s = \Safe\preg_replace('/^type Mutation/m', 'extend type Mutation', $s);
112 1
            $s = \Safe\preg_replace('/^type Query/m', 'extend type Query', $s);
113
        }
114 1
        $extensionSource = implode("\n\n", $sources);
115 1
        $this->ast = \GraphQL\Language\Parser::parse($extensionSource);
116
117
        // TODO: extendDatatypes
118 1
        $this->schema = SchemaExtender::extend(
119 1
            $schema,
120 1
            $this->ast
121
        );
122
        // $schemaBuilder = new \GraphQL\Utils\BuildSchema(
123
        //     $this->ast,
124
        //     [__CLASS__, 'extendDatatypes']
125
        // );
126
127
        // $this->schema = $schemaBuilder->buildSchema();
128 1
        $this->processAst();
129 1
        return $this;
130
    }
131
132
    /**
133
     *
134
     * @param array $files
135
     * @return self
136
     * @throws \Safe\Exceptions\FilesystemException
137
     */
138
    public function fromFiles(array $files): self
139
    {
140
        $sources = [
141
        ];
142
        foreach ($files as $f) {
143
            $data = \Safe\file_get_contents($f);
144
            $sources = array_merge($sources, $this->processImports($data, dirname($f)));
145
            $sources[] = $data;
146
        }
147
        return $this->fromStrings($sources);
148
    }
149
150
    /**
151
     * Returns a Parser from a file path
152
     *
153
     * @param string $path The file path
154
     * @return Parser
155
     * @throws \Safe\Exceptions\FilesystemException If file is not found or parsing fails.
156
     */
157 22
    public function fromFile(string $path): self
158
    {
159 22
        $data = \Safe\file_get_contents($path);
160 22
        $imports = $this->processImports($data, dirname($path));
161
        // TODO: recurse imports
162 22
        return $this->fromString(implode("\n", $imports) . $data);
163
    }
164
165 30
    protected function processSchema(): void
166
    {
167 30
        $originalTypeLoader = $this->schema->getConfig()->typeLoader;
168
169
        $this->schema->getConfig()->typeLoader = function ($typeName) use ($originalTypeLoader) {
170 17
            $type = $originalTypeLoader($typeName);
171 17
            if ($type instanceof \GraphQL\Type\Definition\CustomScalarType) {
172
                $scalarName = $type->name;
173
                $className = $this->scalars[$scalarName];
174
                return new $className($type->config);
175
            }
176 17
            return $type;
177
        };
178 30
    }
179
180 31
    protected function processAst(): void
181
    {
182 31
        $this->ast = Visitor::visit($this->ast, [
183
            // load the scalar type classes
184
            NodeKind::SCALAR_TYPE_DEFINITION => function ($node) {
185 5
                $scalarName = $node->name->value;
186
187
                // load classes
188 5
                $className = null;
189 5
                foreach ($node->directives as $directive) {
190 5
                    switch ($directive->name->value) {
191 5
                    case 'scalar':
192 5
                        foreach ($directive->arguments as $arg) {
193
                            /**
194
                             * @var \GraphQL\Language\AST\ArgumentNode $arg
195
                             */
196
197 5
                            $value = $arg->value->value;
0 ignored issues
show
Bug introduced by
Accessing value on the interface GraphQL\Language\AST\ValueNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
198
199 5
                            switch ($arg->name->value) {
200 5
                            case 'class':
201 5
                                $className = $value;
202 5
                            break;
203
                            }
204
                        }
205 5
                    break;
206
                    }
207
                }
208
209
                // Require special handler class for custom scalars:
210 5
                if (!class_exists($className, true)) {
211
                    throw new \Modelarium\Exception\Exception(
212
                        "Custom scalar must have corresponding handler class $className"
213
                    );
214
                }
215
216 5
                $this->scalars[$scalarName] = $className;
217
218
                // return
219
                //   null: no action
220
                //   Visitor::skipNode(): skip visiting this node
221
                //   Visitor::stop(): stop visiting altogether
222
                //   Visitor::removeNode(): delete this node
223
                //   any value: replace this node with the returned value
224 5
                return null;
225 31
            }
226
        ]);
227 31
    }
228
229 8
    public function setImport(string $name, string $data): self
230
    {
231 8
        $this->imports[$name] = $data;
232 8
        return $this;
233
    }
234
235
    /**
236
     * @param string $data
237
     * @return string[]
238
     */
239 22
    protected function processImports(string $data, string $basedir): array
240
    {
241 22
        $matches = [];
242 22
        $imports = \Safe\preg_match_all('/^#import\s+(.+)$/m', $data, $matches, PREG_SET_ORDER, 0);
243 22
        if (!$imports) {
244 19
            return [];
245
        }
246 3
        return array_map(
247
            function ($i) use ($basedir) {
248 3
                $name = $i[1];
249 3
                if (array_key_exists($name, $this->imports)) {
250 3
                    return $this->imports[$name];
251
                }
252
                return \Safe\file_get_contents($basedir . '/' . $name);
253 3
            },
254 3
            $matches
255
        );
256
    }
257
258 27
    public function getSchema(): Schema
259
    {
260 27
        return $this->schema;
261
    }
262
263
    public function getAST(): DocumentNode
264
    {
265
        return $this->ast;
266
    }
267
268 7
    public function getType(string $name) : ?Type
269
    {
270 7
        return $this->schema->getType($name);
271
    }
272
273 1
    public function getScalars(): array
274
    {
275 1
        return $this->scalars;
276
    }
277
278
    /**
279
     * Factory.
280
     *
281
     * @param string $datatype
282
     * @return ScalarType
283
     * @throws ScalarNotFoundException
284 15
     */
285
    public function getScalarType(string $datatype): ?ScalarType
286 15
    {
287 15
        $className = $this->scalars[$datatype] ?? null;
288 9
        if (!$className) {
289
            return null;
290 9
        }
291
        if (!class_exists($className)) {
292
            throw new ScalarNotFoundException("Class not found for $datatype ($className)");
293 9
        }
294
        return new $className();
295
    }
296
297
    /**
298
     * Given a list of directives, return an array with [ name => [ argument => value] ]
299
     *
300
     * @param NodeList $list
301
     * @return array
302 3
     */
303
    public static function getDirectives(NodeList $list): array
304 3
    {
305 3
        $directives = [];
306
        foreach ($list as $d) {
307
            /**
308
             * @var DirectiveNode $d
309 3
             */
310
            $directives[$d->name->value] = self::getDirectiveArguments($d);
311 3
        }
312
        return $directives;
313
    }
314
315
    /**
316
     * Gets unwrapped type
317
     *
318
     * @param OutputType $type
319
     * @return array [OutputType type, bool isRequired]
320 10
     */
321
    public static function getUnwrappedType(OutputType $type): array
322 10
    {
323 10
        $ret = $type;
324
        $isRequired = false;
325 10
326 10
        if ($ret instanceof NonNull) {
327 10
            $ret = $ret->getWrappedType();
328
            $isRequired = true;
329
        }
330 10
331 4
        if ($ret instanceof ListOfType) {
332 4
            $ret = $ret->getWrappedType();
333 4
            if ($ret instanceof NonNull) { /** @phpstan-ignore-line */
0 ignored issues
show
introduced by
$ret is never a sub-type of GraphQL\Type\Definition\NonNull.
Loading history...
334
                $ret = $ret->getWrappedType();
335
            }
336
        }
337 10
338
        return [$ret, $isRequired];
339
    }
340
341
    /**
342
     * Convertes a directive node arguments to an associative array.
343
     *
344
     * @param DirectiveNode $directive
345
     * @return array
346 7
     */
347
    public static function getDirectiveArguments(DirectiveNode $directive): array
348 7
    {
349 7
        $data = [];
350
        foreach ($directive->arguments as $arg) {
351
            /**
352
             * @var ArgumentNode $arg
353 4
             */
354
            $data[$arg->name->value] = $arg->value->value; /** @phpstan-ignore-line */
0 ignored issues
show
Bug introduced by
Accessing value on the interface GraphQL\Language\AST\ValueNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
355 7
        }
356
        return $data;
357
    }
358
359
    /**
360
     * Gets a directive argument value given its name
361
     *
362
     * @param DirectiveNode $directive
363
     * @param string $name
364
     * @param mixed $default
365
     * @return mixed
366 3
     */
367
    public static function getDirectiveArgumentByName(DirectiveNode $directive, string $name, $default = null)
368 3
    {
369
        foreach ($directive->arguments as $arg) {
370
            /**
371
             * @var ArgumentNode $arg
372
             */
373
            if ($arg->name->value === $name) {
374
                return $arg->value->value; /** @phpstan-ignore-line */
0 ignored issues
show
Bug introduced by
Accessing value on the interface GraphQL\Language\AST\ValueNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
375
            }
376 3
        }
377
        return $default;
378
    }
379
380
    public function appendClass(string $scalarName, string $className)
381
    {
382
        $this->scalars[$scalarName] = $className;
383
    }
384
}
385