Passed
Push — master ( 253428...340885 )
by Bruno
03:15
created

Parser::processSchema()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.2109

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 12
ccs 5
cts 8
cp 0.625
rs 10
cc 2
nc 1
nop 0
crap 2.2109
1
<?php declare(strict_types=1);
2
3
namespace Modelarium;
4
5
use GraphQL\Error\SyntaxError;
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 34
    public function __construct()
45
    {
46 34
        $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 34
        $this->imports = [
54 34
            'formularium.graphql' => \Safe\file_get_contents(__DIR__ . '/Types/Graphql/scalars.graphql'),
55
        ];
56 34
    }
57
58
    /** @phpstan-ignore-next-line */
59 30
    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 30
        return $typeConfig;
76
    }
77
78
    /**
79
     * Returns a Parser from a string
80
     *
81
     * @param string $data the string
82
     * @return Parser
83
     */
84 31
    public function fromString(string $data): self
85
    {
86 31
        $this->ast = \GraphQL\Language\Parser::parse($data);
87 31
        $this->processAst();
88 31
        $schemaBuilder = new \GraphQL\Utils\BuildSchema(
89 31
            $this->ast,
90 31
            [__CLASS__, 'extendDatatypes']
91
        );
92
93 31
        $this->schema = $schemaBuilder->buildSchema();
94 31
        $this->processSchema();
95 31
        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
        try {
116 1
            $this->ast = \GraphQL\Language\Parser::parse($extensionSource);
117
        } catch (SyntaxError $e) {
118
            $source = $e->getSource();
119
            $start = $e->getPositions()[0] - 50;
120
            $end = $e->getPositions()[0] + 50;
121
            $start = $start <= 0 ? 0 : $start;
122
            $end = $end >= $source->length ? $source->length : $end;
123
            echo $e->message, "\nat: ...", mb_substr($source->body, $start, $end - $start), '...';
124
            throw $e;
125
        }
126
127
        // TODO: extendDatatypes
128 1
        $this->schema = SchemaExtender::extend(
129 1
            $schema,
130 1
            $this->ast
131
        );
132
        // $schemaBuilder = new \GraphQL\Utils\BuildSchema(
133
        //     $this->ast,
134
        //     [__CLASS__, 'extendDatatypes']
135
        // );
136
137
        // $this->schema = $schemaBuilder->buildSchema();
138 1
        $this->processAst();
139 1
        return $this;
140
    }
141
142
    /**
143
     *
144
     * @param array $files
145
     * @return self
146
     * @throws \Safe\Exceptions\FilesystemException
147
     */
148
    public function fromFiles(array $files): self
149
    {
150
        $sources = [
151
        ];
152
        foreach ($files as $f) {
153
            $data = \Safe\file_get_contents($f);
154
            $sources = array_merge($sources, $this->processImports($data, dirname($f)));
155
            $sources[] = $data;
156
        }
157
        return $this->fromStrings($sources);
158
    }
159
160
    /**
161
     * Returns a Parser from a file path
162
     *
163
     * @param string $path The file path
164
     * @return Parser
165
     * @throws \Safe\Exceptions\FilesystemException If file is not found or parsing fails.
166
     */
167 23
    public function fromFile(string $path): self
168
    {
169 23
        $data = \Safe\file_get_contents($path);
170 23
        $imports = $this->processImports($data, dirname($path));
171
        // TODO: recurse imports
172 23
        return $this->fromString(implode("\n", $imports) . $data);
173
    }
174
175 31
    protected function processSchema(): void
176
    {
177 31
        $originalTypeLoader = $this->schema->getConfig()->typeLoader;
178
179
        $this->schema->getConfig()->typeLoader = function ($typeName) use ($originalTypeLoader) {
180 18
            $type = $originalTypeLoader($typeName);
181 18
            if ($type instanceof \GraphQL\Type\Definition\CustomScalarType) {
182
                $scalarName = $type->name;
183
                $className = $this->scalars[$scalarName];
184
                return new $className($type->config);
185
            }
186 18
            return $type;
187
        };
188 31
    }
189
190 32
    protected function processAst(): void
191
    {
192 32
        $this->ast = Visitor::visit($this->ast, [
193
            // load the scalar type classes
194
            NodeKind::SCALAR_TYPE_DEFINITION => function ($node) {
195 5
                $scalarName = $node->name->value;
196
197
                // load classes
198 5
                $className = null;
199 5
                foreach ($node->directives as $directive) {
200 5
                    switch ($directive->name->value) {
201 5
                    case 'scalar':
202 5
                        foreach ($directive->arguments as $arg) {
203
                            /**
204
                             * @var \GraphQL\Language\AST\ArgumentNode $arg
205
                             */
206
207 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...
208
209 5
                            switch ($arg->name->value) {
210 5
                            case 'class':
211 5
                                $className = $value;
212 5
                            break;
213
                            }
214
                        }
215 5
                    break;
216
                    }
217
                }
218
219
                // Require special handler class for custom scalars:
220 5
                if (!class_exists($className, true)) {
221
                    throw new \Modelarium\Exception\Exception(
222
                        "Custom scalar must have corresponding handler class $className"
223
                    );
224
                }
225
226 5
                $this->scalars[$scalarName] = $className;
227
228
                // return
229
                //   null: no action
230
                //   Visitor::skipNode(): skip visiting this node
231
                //   Visitor::stop(): stop visiting altogether
232
                //   Visitor::removeNode(): delete this node
233
                //   any value: replace this node with the returned value
234 5
                return null;
235 32
            }
236
        ]);
237 32
    }
238
239 8
    public function setImport(string $name, string $data): self
240
    {
241 8
        $this->imports[$name] = $data;
242 8
        return $this;
243
    }
244
245
    /**
246
     * @param string $data
247
     * @return string[]
248
     */
249 23
    protected function processImports(string $data, string $basedir): array
250
    {
251 23
        $matches = [];
252 23
        $imports = \Safe\preg_match_all('/^#import\s+(.+)$/m', $data, $matches, PREG_SET_ORDER, 0);
253 23
        if (!$imports) {
254 20
            return [];
255
        }
256 3
        return array_map(
257
            function ($i) use ($basedir) {
258 3
                $name = $i[1];
259 3
                if (array_key_exists($name, $this->imports)) {
260 3
                    return $this->imports[$name];
261
                }
262
                return \Safe\file_get_contents($basedir . '/' . $name);
263 3
            },
264
            $matches
265
        );
266
    }
267
268 28
    public function getSchema(): Schema
269
    {
270 28
        return $this->schema;
271
    }
272
273
    public function getAST(): DocumentNode
274
    {
275
        return $this->ast;
276
    }
277
278 7
    public function getType(string $name) : ?Type
279
    {
280 7
        return $this->schema->getType($name);
281
    }
282
283 1
    public function getScalars(): array
284
    {
285 1
        return $this->scalars;
286
    }
287
288
    /**
289
     * Factory.
290
     *
291
     * @param string $datatype
292
     * @return ScalarType
293
     * @throws ScalarNotFoundException
294
     */
295 15
    public function getScalarType(string $datatype): ?ScalarType
296
    {
297 15
        $className = $this->scalars[$datatype] ?? null;
298 15
        if (!$className) {
299 9
            return null;
300
        }
301 9
        if (!class_exists($className)) {
302
            throw new ScalarNotFoundException("Class not found for $datatype ($className)");
303
        }
304 9
        return new $className();
305
    }
306
307
    /**
308
     * Given a list of directives, return an array with [ name => [ argument => value] ]
309
     *
310
     * @param NodeList $list
311
     * @return array
312
     */
313 3
    public static function getDirectives(NodeList $list): array
314
    {
315 3
        $directives = [];
316 3
        foreach ($list as $d) {
317
            /**
318
             * @var DirectiveNode $d
319
             */
320 3
            $directives[$d->name->value] = self::getDirectiveArguments($d);
321
        }
322 3
        return $directives;
323
    }
324
325
    /**
326
     * Gets unwrapped type
327
     *
328
     * @param OutputType $type
329
     * @return array [OutputType type, bool isRequired]
330
     */
331 10
    public static function getUnwrappedType(OutputType $type): array
332
    {
333 10
        $ret = $type;
334 10
        $isRequired = false;
335
336 10
        if ($ret instanceof NonNull) {
337 10
            $ret = $ret->getWrappedType();
338 10
            $isRequired = true;
339
        }
340
341 10
        if ($ret instanceof ListOfType) {
342 4
            $ret = $ret->getWrappedType();
343 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...
344 4
                $ret = $ret->getWrappedType();
345
            }
346
        }
347
348 10
        return [$ret, $isRequired];
349
    }
350
351
    /**
352
     * Convertes a directive node arguments to an associative array.
353
     *
354
     * @param DirectiveNode $directive
355
     * @return array
356
     */
357 7
    public static function getDirectiveArguments(DirectiveNode $directive): array
358
    {
359 7
        $data = [];
360 7
        foreach ($directive->arguments as $arg) {
361
            /**
362
             * @var ArgumentNode $arg
363
             */
364 4
            $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...
365
        }
366 7
        return $data;
367
    }
368
369
    /**
370
     * Gets a directive argument value given its name
371
     *
372
     * @param DirectiveNode $directive
373
     * @param string $name
374
     * @param mixed $default
375
     * @return mixed
376
     */
377 3
    public static function getDirectiveArgumentByName(DirectiveNode $directive, string $name, $default = null)
378
    {
379 3
        foreach ($directive->arguments as $arg) {
380
            /**
381
             * @var ArgumentNode $arg
382
             */
383
            if ($arg->name->value === $name) {
384
                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...
385
            }
386
        }
387 3
        return $default;
388
    }
389
390
    /**
391
     * Appends a scalar in realtime
392
     *
393
     * @param string $scalarName The scalar name
394
     * @param string $className The FQCN
395
     * @return self
396
     */
397
    public function appendScalar(string $scalarName, string $className): self
398
    {
399
        $this->scalars[$scalarName] = $className;
400
        return $this;
401
    }
402
}
403