Passed
Push — master ( 9503af...0b2a3d )
by Bruno
05:22
created

Parser::getDirectives()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 10
rs 10
ccs 0
cts 0
cp 0
cc 2
nc 2
nop 1
crap 6
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 22
    /**
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 22
     * @var string[]
41
     */
42
    protected $imports = [];
43 23
44
    public function __construct()
45
    {
46 23
        $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
        $this->imports = [
54
            'formularium.graphql' => \Safe\file_get_contents(__DIR__ . '/Types/Graphql/scalars.graphql'),
55
        ];
56
    }
57
58
    /** @phpstan-ignore-next-line */
59
    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 17
                },
70
                'parseLiteral' => function ($ast) {
71 17
                    // ...
72 17
                }
73
            ]);
74
        } */
75
        return $typeConfig;
76
    }
77
78
    /**
79
     * Returns a Parser from a string
80
     *
81
     * @param string $data the string
82 22
     * @return Parser
83
     */
84 22
    public function fromString(string $data): self
85 22
    {
86 22
        $this->ast = \GraphQL\Language\Parser::parse($data);
87 22
        $this->processAst();
88 22
        $schemaBuilder = new \GraphQL\Utils\BuildSchema(
89
            $this->ast,
90 22
            [__CLASS__, 'extendDatatypes']
91 22
        );
92
93
        $this->schema = $schemaBuilder->buildSchema();
94
        $this->processSchema();
95
        return $this;
96
    }
97
98
    /**
99 1
     *
100
     * @param string[] $sources
101 1
     * @return self
102 1
     */
103 1
    public function fromStrings(array $sources): self
104 1
    {
105
        $schema = new Schema([
106
            'query' => new ObjectType(['name' => 'Query']),
107 1
            'mutation' => new ObjectType(['name' => 'Mutation']),
108 1
        ]);
109 1
110
        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
        $extensionSource = implode("\n\n", $sources);
115 1
        $this->ast = \GraphQL\Language\Parser::parse($extensionSource);
116 1
117 1
        // TODO: extendDatatypes
118
        $this->schema = SchemaExtender::extend(
119
            $schema,
120
            $this->ast
121
        );
122
        // $schemaBuilder = new \GraphQL\Utils\BuildSchema(
123
        //     $this->ast,
124
        //     [__CLASS__, 'extendDatatypes']
125 1
        // );
126
127
        // $this->schema = $schemaBuilder->buildSchema();
128
        $this->processAst();
129 21
        return $this;
130
    }
131 21
132
    /**
133
     *
134 4
     * @param array $files
135
     * @return self
136 4
     * @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
    public function fromFile(string $path): self
158
    {
159
        $data = \Safe\file_get_contents($path);
160
        $imports = $this->processImports($data, dirname($path));
161
        // TODO: recurse imports
162
        return $this->fromString(implode("\n", $imports) . $data);
163
    }
164
165
    protected function processSchema(): void
166
    {
167
        $originalTypeLoader = $this->schema->getConfig()->typeLoader;
168
169
        $this->schema->getConfig()->typeLoader = function ($typeName) use ($originalTypeLoader) {
170
            $type = $originalTypeLoader($typeName);
171
            if ($type instanceof \GraphQL\Type\Definition\CustomScalarType) {
172
                $scalarName = $type->name;
173
                $className = $this->scalars[$scalarName];
174
                return new $className($type->config);
175
            }
176
            return $type;
177
        };
178
    }
179
180
    protected function processAst(): void
181
    {
182
        $this->ast = Visitor::visit($this->ast, [
183
            // load the scalar type classes
184
            NodeKind::SCALAR_TYPE_DEFINITION => function ($node) {
185
                $scalarName = $node->name->value;
186
187
                // load classes
188
                $className = null;
189
                foreach ($node->directives as $directive) {
190
                    switch ($directive->name->value) {
191
                    case 'scalar':
192
                        foreach ($directive->arguments as $arg) {
193
                            /**
194
                             * @var \GraphQL\Language\AST\ArgumentNode $arg
195
                             */
196
197
                            $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
                            switch ($arg->name->value) {
200
                            case 'class':
201
                                $className = $value;
202
                            break;
203
                            }
204
                        }
205
                    break;
206
                    }
207
                }
208
209
                // Require special handler class for custom scalars:
210
                if (!class_exists($className, true)) {
211
                    throw new \Modelarium\Exception\Exception(
212
                        "Custom scalar must have corresponding handler class $className"
213
                    );
214
                }
215
216
                $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
                return null;
225
            }
226
        ]);
227
    }
228
229
    public function setImport(string $name, string $data): self
230
    {
231
        $this->imports[$name] = $data;
232
        return $this;
233
    }
234
235
    /**
236
     * @param string $data
237
     * @return string[]
238
     */
239
    protected function processImports(string $data, string $basedir): array
240
    {
241
        $matches = [];
242
        $imports = \Safe\preg_match_all('/^#import\s+(.+)$/m', $data, $matches, PREG_SET_ORDER, 0);
243
        if (!$imports) {
244
            return [];
245
        }
246
        return array_map(
247
            function ($i) use ($basedir) {
248
                $name = $i[1];
249
                if (array_key_exists($name, $this->imports)) {
250
                    return $this->imports[$name];
251
                }
252
                return \Safe\file_get_contents($basedir . '/' . $name);
253
            },
254
            $matches
255
        );
256
    }
257
258
    public function getSchema(): Schema
259
    {
260
        return $this->schema;
261
    }
262
263
    public function getAST(): DocumentNode
264
    {
265
        return $this->ast;
266
    }
267
268
    public function getType(string $name) : ?Type
269
    {
270
        return $this->schema->getType($name);
271
    }
272
273
    public function getScalars(): array
274
    {
275
        return $this->scalars;
276
    }
277
278
    /**
279
     * Factory.
280
     *
281
     * @param string $datatype
282
     * @return ScalarType
283
     */
284
    public function getScalarType(string $datatype): ?ScalarType
285
    {
286
        $className = $this->scalars[$datatype] ?? null;
287
        if (!$className) {
288
            return null;
289
        }
290
        if (!class_exists($className)) {
291
            throw new ScalarNotFoundException("Class not found for $datatype ($className)");
292
        }
293
        return new $className();
294
    }
295
296
    /**
297
     * Given a list of directives, return an array with [ name => [ argument => value] ]
298
     *
299
     * @param NodeList $list
300
     * @return array
301
     */
302
    public static function getDirectives(NodeList $list): array
303
    {
304
        $directives = [];
305
        foreach ($list as $d) {
306
            /**
307
             * @var DirectiveNode $d
308
             */
309
            $directives[$d->name->value] = self::getDirectiveArguments($d);
310
        }
311
        return $directives;
312
    }
313
314
    /**
315
     * Gets unwrapped type
316
     *
317
     * @param OutputType $type
318
     * @return array [OutputType type, bool isRequired]
319
     */
320
    public static function getUnwrappedType(OutputType $type): array
321
    {
322
        $ret = $type;
323
        $isRequired = false;
324
325
        if ($ret instanceof NonNull) {
326
            $ret = $ret->getWrappedType();
327
            $isRequired = true;
328
        }
329
330
        if ($ret instanceof ListOfType) {
331
            $ret = $ret->getWrappedType();
332
            if ($ret instanceof NonNull) {
0 ignored issues
show
introduced by
$ret is never a sub-type of GraphQL\Type\Definition\NonNull.
Loading history...
333
                $ret = $ret->getWrappedType();
334
            }
335
        }
336
337
        return [$ret, $isRequired];
338
    }
339
340
    /**
341
     * Convertes a directive node arguments to an associative array.
342
     *
343
     * @param DirectiveNode $directive
344
     * @return array
345
     */
346
    public static function getDirectiveArguments(DirectiveNode $directive): array
347
    {
348
        $data = [];
349
        foreach ($directive->arguments as $arg) {
350
            /**
351
             * @var ArgumentNode $arg
352
             */
353
            $data[$arg->name->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...
354
        }
355
        return $data;
356
    }
357
358
    /**
359
     * Gets a directive argument value given its name
360
     *
361
     * @param DirectiveNode $directive
362
     * @param string $name
363
     * @param [type] $default
0 ignored issues
show
Documentation Bug introduced by
The doc comment [type] at position 0 could not be parsed: Unknown type name '[' at position 0 in [type].
Loading history...
364
     * @return mixed
365
     */
366
    public static function getDirectiveArgumentByName(DirectiveNode $directive, string $name, $default = null)
367
    {
368
        foreach ($directive->arguments as $arg) {
369
            /**
370
             * @var ArgumentNode $arg
371
             */
372
            if ($arg->name->value === $name) {
373
                return $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...
374
            }
375
        }
376
        return $default;
377
    }
378
}
379