Passed
Push — master ( a266d2...fe3e3c )
by Bruno
03:11
created

Parser::getSchema()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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\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 36
    public function __construct()
45
    {
46 36
        $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 36
            'formularium.graphql' => \Safe\file_get_contents(__DIR__ . '/Types/Graphql/scalars.graphql'),
55 36
            'lighthouse.graphql' => \Safe\file_get_contents(__DIR__ . '/Laravel/Graphql/definitionsLighthouse.graphql'),
56 36
            'modelarium.graphql' => \Safe\file_get_contents(__DIR__ . '/Types/Graphql/directives.graphql'),
57
        ];
58 36
    }
59
60
    /** @phpstan-ignore-next-line */
61 26
    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

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

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Bug introduced by
The property value does not seem to exist on GraphQL\Language\AST\VariableNode.
Loading history...
Bug introduced by
The property value does not exist on GraphQL\Language\AST\ListValueNode. Did you mean values?
Loading history...
Bug introduced by
The property value does not seem to exist on GraphQL\Language\AST\NullValueNode.
Loading history...
Bug introduced by
The property value does not seem to exist on GraphQL\Language\AST\ObjectValueNode.
Loading history...
363
        }
364 7
        return $data;
365
    }
366
367
    /**
368
     * Gets a directive argument value given its name
369
     *
370
     * @param DirectiveNode $directive
371
     * @param string $name
372
     * @param mixed $default
373
     * @return mixed
374
     */
375 8
    public static function getDirectiveArgumentByName(DirectiveNode $directive, string $name, $default = null)
376
    {
377 8
        foreach ($directive->arguments as $arg) {
378
            /**
379
             * @var ArgumentNode $arg
380
             */
381 5
            if ($arg->name->value === $name) {
382 5
                $v = $arg->value;
383 5
                if ($v instanceof ListValueNode) {
384 3
                    $fields = [];
385 3
                    foreach ($v->values as $i) {
386 3
                        $fields[] = $i->value; /** @phpstan-ignore-line */
387
                    }
388 3
                    return $fields;
389
                } else {
390 2
                    return $arg->value->value; /** @phpstan-ignore-line */
0 ignored issues
show
Bug introduced by
The property value does not exist on GraphQL\Language\AST\ListValueNode. Did you mean values?
Loading history...
Bug introduced by
The property value does not seem to exist on GraphQL\Language\AST\ObjectValueNode.
Loading history...
Bug introduced by
The property value does not seem to exist on GraphQL\Language\AST\NullValueNode.
Loading history...
Bug introduced by
The property value does not seem to exist on GraphQL\Language\AST\VariableNode.
Loading history...
391
                }
392
            }
393
        }
394 3
        return $default;
395
    }
396
397
    /**
398
     * Appends a scalar in realtime
399
     *
400
     * @param string $scalarName The scalar name
401
     * @param string $className The FQCN
402
     * @return self
403
     */
404
    public function appendScalar(string $scalarName, string $className): self
405
    {
406
        $this->scalars[$scalarName] = $className;
407
        return $this;
408
    }
409
}
410