Passed
Push — master ( 341a7c...2223cb )
by Bruno
03:55
created

Parser::setImport()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
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 36
        $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 16
                if (in_array($scalarName, ['BuilderValue', 'CanArgs', 'EnumValue', 'EqValue'])) {
200
                    // special lighthouse scalar
201 16
                    return null;
202
                }
203
204
                // load classes
205 5
                $className = '';
206 5
                foreach ($node->directives as $directive) {
207 5
                    switch ($directive->name->value) {
208 5
                    case 'scalar':
209 5
                        foreach ($directive->arguments as $arg) {
210 5
                            $value = $arg->value->value;
211
212 5
                            switch ($arg->name->value) {
213 5
                            case 'class':
214 5
                                $className = (string)$value;
215 5
                            break;
216
                            }
217
                        }
218 5
                    break;
219
                    }
220
                }
221
222
                // Require special handler class for custom scalars:
223 5
                if (!class_exists($className, true)) {
224
                    throw new \Modelarium\Exception\Exception(
225
                        "Custom scalar must have corresponding handler class: '$className' at {$scalarName}"
226
                    );
227
                }
228
229 5
                $this->scalars[$scalarName] = $className;
230
231
                // return
232
                //   null: no action
233
                //   Visitor::skipNode(): skip visiting this node
234
                //   Visitor::stop(): stop visiting altogether
235
                //   Visitor::removeNode(): delete this node
236
                //   any value: replace this node with the returned value
237 5
                return null;
238 34
            }
239
        ]);
240 34
    }
241
242 8
    public function setImport(string $name, string $data): self
243
    {
244 8
        $this->imports[$name] = $data;
245 8
        return $this;
246
    }
247
248
    /**
249
     * @param string $data
250
     * @return string[]
251
     */
252 32
    protected function processImports(string $data, string $basedir): array
253
    {
254 32
        $matches = [];
255 32
        $imports = \Safe\preg_match_all('/^#import\s+(.+)$/m', $data, $matches, PREG_SET_ORDER, 0);
256 32
        if (!$imports) {
257 7
            return [];
258
        }
259 25
        return array_map(
260
            function ($i) use ($basedir) {
261 25
                $name = $i[1];
262 25
                if (array_key_exists($name, $this->imports)) {
263 25
                    return $this->imports[$name];
264
                }
265
                return \Safe\file_get_contents($basedir . '/' . $name);
266 25
            },
267
            $matches
268
        );
269
    }
270
271 30
    public function getSchema(): Schema
272
    {
273 30
        return $this->schema;
274
    }
275
276
    public function getAST(): DocumentNode
277
    {
278
        return $this->ast;
279
    }
280
281 7
    public function getType(string $name) : ?Type
282
    {
283 7
        return $this->schema->getType($name);
284
    }
285
286 1
    public function getScalars(): array
287
    {
288 1
        return $this->scalars;
289
    }
290
291
    /**
292
     * Factory.
293
     *
294
     * @param string $datatype
295
     * @return ScalarType
296
     * @throws ScalarNotFoundException
297
     */
298 15
    public function getScalarType(string $datatype): ?ScalarType
299
    {
300 15
        $className = $this->scalars[$datatype] ?? null;
301 15
        if (!$className) {
302 9
            return null;
303
        }
304 9
        if (!class_exists($className)) {
305
            throw new ScalarNotFoundException("Class not found for $datatype ($className)");
306
        }
307 9
        return new $className();
308
    }
309
310
    /**
311
     * Given a list of directives, return an array with [ name => [ argument => value] ]
312
     *
313
     * @param \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $list
314
     * @return array
315
     */
316 3
    public static function getDirectives(NodeList $list): array
317
    {
318 3
        $directives = [];
319 3
        foreach ($list as $d) {
320
            /**
321
             * @var DirectiveNode $d
322
             */
323 3
            $directives[$d->name->value] = self::getDirectiveArguments($d);
324
        }
325 3
        return $directives;
326
    }
327
328
    /**
329
     * Gets unwrapped type
330
     *
331
     * @param Type $type
332
     * @return array [OutputType type, bool isRequired]
333
     */
334 11
    public static function getUnwrappedType(Type $type): array
335
    {
336 11
        $ret = $type;
337 11
        $isRequired = false;
338
339 11
        if ($ret instanceof NonNull) {
340 10
            $ret = $ret->getWrappedType();
341 10
            $isRequired = true;
342
        }
343
344 11
        if ($ret instanceof ListOfType) {
345 4
            $ret = $ret->getWrappedType();
346 4
            if ($ret instanceof NonNull) {
347 4
                $ret = $ret->getWrappedType();
348
            }
349
        }
350
351 11
        return [$ret, $isRequired];
352
    }
353
354
    /**
355
     * Convertes a directive node arguments to an associative array.
356
     *
357
     * @param DirectiveNode $directive
358
     * @return array
359
     */
360 7
    public static function getDirectiveArguments(DirectiveNode $directive): array
361
    {
362 7
        $data = [];
363 7
        foreach ($directive->arguments as $arg) {
364
            /**
365
             * @var ArgumentNode $arg
366
             */
367 4
            $data[$arg->name->value] = $arg->value->value; /** @phpstan-ignore-line */
0 ignored issues
show
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 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\VariableNode.
Loading history...
Bug introduced by
The property value does not seem to exist on GraphQL\Language\AST\NullValueNode.
Loading history...
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...
368
        }
369 7
        return $data;
370
    }
371
372
    /**
373
     * Gets a directive argument value given its name
374
     *
375
     * @param DirectiveNode $directive
376
     * @param string $name
377
     * @param mixed $default
378
     * @return mixed
379
     */
380 8
    public static function getDirectiveArgumentByName(DirectiveNode $directive, string $name, $default = null)
381
    {
382 8
        foreach ($directive->arguments as $arg) {
383
            /**
384
             * @var ArgumentNode $arg
385
             */
386 5
            if ($arg->name->value === $name) {
387 5
                $v = $arg->value;
388 5
                if ($v instanceof ListValueNode) {
389 3
                    $fields = [];
390 3
                    foreach ($v->values as $i) {
391 3
                        $fields[] = $i->value; /** @phpstan-ignore-line */
392
                    }
393 3
                    return $fields;
394
                } else {
395 2
                    return $arg->value->value; /** @phpstan-ignore-line */
0 ignored issues
show
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 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\VariableNode.
Loading history...
Bug introduced by
The property value does not seem to exist on GraphQL\Language\AST\NullValueNode.
Loading history...
396
                }
397
            }
398
        }
399 3
        return $default;
400
    }
401
402
    /**
403
     * Appends a scalar in realtime
404
     *
405
     * @param string $scalarName The scalar name
406
     * @param string $className The FQCN
407
     * @return self
408
     */
409
    public function appendScalar(string $scalarName, string $className): self
410
    {
411
        $this->scalars[$scalarName] = $className;
412
        return $this;
413
    }
414
}
415