Passed
Push — master ( 41a7c0...823f6c )
by Bruno
15:50 queued 07:18
created

Parser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 11
ccs 4
cts 4
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
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
     */
284 15
    public function getScalarType(string $datatype): ?ScalarType
285
    {
286 15
        $className = $this->scalars[$datatype] ?? null;
287 15
        if (!$className) {
288 9
            return null;
289
        }
290 9
        if (!class_exists($className)) {
291
            throw new ScalarNotFoundException("Class not found for $datatype ($className)");
292
        }
293 9
        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 3
    public static function getDirectives(NodeList $list): array
303
    {
304 3
        $directives = [];
305 3
        foreach ($list as $d) {
306
            /**
307
             * @var DirectiveNode $d
308
             */
309 3
            $directives[$d->name->value] = self::getDirectiveArguments($d);
310
        }
311 3
        return $directives;
312
    }
313
314
    /**
315
     * Gets unwrapped type
316
     *
317
     * @param OutputType $type
318
     * @return array [OutputType type, bool isRequired]
319
     */
320 10
    public static function getUnwrappedType(OutputType $type): array
321
    {
322 10
        $ret = $type;
323 10
        $isRequired = false;
324
325 10
        if ($ret instanceof NonNull) {
326 10
            $ret = $ret->getWrappedType();
327 10
            $isRequired = true;
328
        }
329
330 10
        if ($ret instanceof ListOfType) {
331 4
            $ret = $ret->getWrappedType();
332 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...
333 4
                $ret = $ret->getWrappedType();
334
            }
335
        }
336
337 10
        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 7
    public static function getDirectiveArguments(DirectiveNode $directive): array
347
    {
348 7
        $data = [];
349 7
        foreach ($directive->arguments as $arg) {
350
            /**
351
             * @var ArgumentNode $arg
352
             */
353 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...
354
        }
355 7
        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 mixed $default
364
     * @return mixed
365
     */
366 3
    public static function getDirectiveArgumentByName(DirectiveNode $directive, string $name, $default = null)
367
    {
368 3
        foreach ($directive->arguments as $arg) {
369
            /**
370
             * @var ArgumentNode $arg
371
             */
372
            if ($arg->name->value === $name) {
373
                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...
374
            }
375
        }
376 3
        return $default;
377
    }
378
}
379