Passed
Push — master ( 6bf22a...09030e )
by Bruno
04:28
created

Parser::appendScalar()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 3
cp 0
crap 2
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\ListValueNode;
10
use GraphQL\Language\AST\NodeKind;
11
use GraphQL\Language\AST\NodeList;
12
use GraphQL\Language\Visitor;
13
use GraphQL\Type\Definition\ListOfType;
14
use GraphQL\Type\Definition\NonNull;
15
use GraphQL\Type\Definition\ObjectType;
16
use GraphQL\Type\Definition\OutputType;
17
use GraphQL\Type\Definition\Type;
18
use GraphQL\Type\Schema;
19
use GraphQL\Utils\SchemaExtender;
20
use Modelarium\Exception\ScalarNotFoundException;
21
use Modelarium\Types\ScalarType;
22
23
class Parser
24
{
25
    /**
26
     * @var \GraphQL\Language\AST\DocumentNode
27
     */
28
    protected $ast;
29
30
    /**
31
     * @var \GraphQL\Type\Schema
32
     */
33
    protected $schema;
34
35
    /**
36
     * @var string[]
37
     */
38
    protected $scalars = [];
39
40
    /**
41
     * @var string[]
42
     */
43
    protected $imports = [];
44
45 35
    public function __construct()
46
    {
47 35
        $this->scalars = [
48
            'String' => 'Modelarium\\Types\\Datatype_string',
49
            'Int' => 'Modelarium\\Types\\Datatype_integer',
50
            'Float' => 'Modelarium\\Types\\Datatype_float',
51
            'Boolean' => 'Modelarium\\Types\\Datatype_bool',
52
        ];
53
54
        $this->imports = [
55 35
            'formularium.graphql' => \Safe\file_get_contents(__DIR__ . '/Types/Graphql/scalars.graphql'),
56
        ];
57 35
    }
58
59
    /** @phpstan-ignore-next-line */
60 31
    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

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