Completed
Push — master ( 6a9361...911cdf )
by Alexandr
02:46
created

Parser::parseOperation()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 37
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6.0052

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 37
ccs 18
cts 19
cp 0.9474
rs 8.439
cc 6
eloc 19
nc 10
nop 1
crap 6.0052
1
<?php
2
/*
3
* This file is a part of graphql-youshido project.
4
*
5
* @author Portey Vasil <[email protected]>
6
* created: 11/23/15 1:22 AM
7
*/
8
9
namespace Youshido\GraphQL\Parser;
10
11
12
use Youshido\GraphQL\Exception\Parser\SyntaxErrorException;
13
use Youshido\GraphQL\Parser\Ast\Argument;
14
use Youshido\GraphQL\Parser\Ast\ArgumentValue\InputList;
15
use Youshido\GraphQL\Parser\Ast\ArgumentValue\InputObject;
16
use Youshido\GraphQL\Parser\Ast\ArgumentValue\Literal;
17
use Youshido\GraphQL\Parser\Ast\ArgumentValue\Variable;
18
use Youshido\GraphQL\Parser\Ast\ArgumentValue\VariableReference;
19
use Youshido\GraphQL\Parser\Ast\Directive;
20
use Youshido\GraphQL\Parser\Ast\Field;
21
use Youshido\GraphQL\Parser\Ast\Fragment;
22
use Youshido\GraphQL\Parser\Ast\FragmentReference;
23
use Youshido\GraphQL\Parser\Ast\Mutation;
24
use Youshido\GraphQL\Parser\Ast\Query;
25
use Youshido\GraphQL\Parser\Ast\TypedFragmentReference;
26
27
class Parser extends Tokenizer
28
{
29
30
    /** @var array */
31
    private $data = [];
32
33 102
    public function parse($source = null)
34
    {
35 102
        $this->init($source);
36
37 102
        while (!$this->end()) {
38 101
            $tokenType = $this->peek()->getType();
39
40
            switch ($tokenType) {
41 101
                case Token::TYPE_LBRACE:
42 55
                    foreach ($this->parseBody() as $query) {
43 50
                        $this->data['queries'][] = $query;
44
                    }
45
46 51
                    break;
47 50
                case Token::TYPE_QUERY:
48 29
                    $queries = $this->parseOperation(Token::TYPE_QUERY);
49 29
                    foreach ($queries as $query) {
50 27
                        $this->data['queries'][] = $query;
51
                    }
52
53 29
                    break;
54 26
                case Token::TYPE_MUTATION:
55 17
                    $mutations = $this->parseOperation(Token::TYPE_MUTATION);
56 12
                    foreach ($mutations as $query) {
57 12
                        $this->data['mutations'][] = $query;
58
                    }
59
60 12
                    break;
61
62 9
                case Token::TYPE_FRAGMENT:
63 8
                    $this->data['fragments'][] = $this->parseFragment();
64
65 8
                    break;
66
67
                default:
68 1
                    throw new SyntaxErrorException('Incorrect request syntax', $this->getLocation());
69
            }
70
        }
71
72 92
        return $this->data;
73
    }
74
75 102 View Code Duplication
    private function init($source = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
76
    {
77 102
        $this->initTokenizer($source);
78
79 102
        $this->data = [
80
            'queries'            => [],
81
            'mutations'          => [],
82
            'fragments'          => [],
83
            'fragmentReferences' => [],
84
            'variables'          => [],
85
            'variableReferences' => [],
86
        ];
87 102
    }
88
89 46
    protected function parseOperation($type = Token::TYPE_QUERY)
90
    {
91 46
        $operation  = null;
0 ignored issues
show
Unused Code introduced by
$operation is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
92 46
        $directives = [];
93
94 46
        if ($this->matchMulti([Token::TYPE_QUERY, Token::TYPE_MUTATION])) {
95 46
            $this->lex();
96
97 46
            $this->eat(Token::TYPE_IDENTIFIER);
98
99 46
            if ($this->match(Token::TYPE_LPAREN)) {
100 13
                $this->parseVariables();
101
            }
102
103 46
            if ($this->match(Token::TYPE_AT)) {
104
                $directives = $this->parseDirectiveList();
105
            }
106
107
        }
108
109 46
        $this->lex();
110
111 46
        $fields = [];
112
113 46
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
114 44
            $this->eatMulti([Token::TYPE_COMMA]);
115
116 44
            $operation = $this->parseBodyItem($type, true);
117 39
            $operation->setDirectives($directives);
118
119 39
            $fields[] = $operation;
120
        }
121
122 41
        $this->expect(Token::TYPE_RBRACE);
123
124 41
        return $fields;
125
    }
126
127 80
    protected function parseBody($token = Token::TYPE_QUERY, $highLevel = true)
128
    {
129 80
        $fields = [];
130
131 80
        $this->lex();
132
133 80
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
134 79
            $this->eatMulti([Token::TYPE_COMMA]);
135
136 79
            if ($this->match(Token::TYPE_FRAGMENT_REFERENCE)) {
137 10
                $this->lex();
138
139 10
                if ($this->eat(Token::TYPE_ON)) {
140 3
                    $fields[] = $this->parseBodyItem(Token::TYPE_TYPED_FRAGMENT, $highLevel);
141
                } else {
142 10
                    $fields[] = $this->parseFragmentReference();
143
                }
144
            } else {
145 78
                $fields[] = $this->parseBodyItem($token, $highLevel);
146
            }
147
        }
148
149 76
        $this->expect(Token::TYPE_RBRACE);
150
151 76
        return $fields;
152
    }
153
154 13
    protected function parseVariables()
155
    {
156 13
        $this->eat(Token::TYPE_LPAREN);
157
158 13
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
159 13
            $this->eat(Token::TYPE_COMMA);
160
161 13
            $variableToken = $this->eat(Token::TYPE_VARIABLE);
162 13
            $nameToken     = $this->eatIdentifierToken();
163 13
            $this->eat(Token::TYPE_COLON);
164
165 13
            $isArray              = false;
166 13
            $arrayElementNullable = true;
167
168 13
            if ($this->match(Token::TYPE_LSQUARE_BRACE)) {
169 3
                $isArray = true;
170
171 3
                $this->eat(Token::TYPE_LSQUARE_BRACE);
172 3
                $type = $this->eatIdentifierToken()->getData();
173
174 3
                if ($this->match(Token::TYPE_REQUIRED)) {
175 2
                    $arrayElementNullable = false;
176 2
                    $this->eat(Token::TYPE_REQUIRED);
177
                }
178
179 3
                $this->eat(Token::TYPE_RSQUARE_BRACE);
180
            } else {
181 12
                $type = $this->eatIdentifierToken()->getData();
182
            }
183
184 13
            $required = false;
185 13
            if ($this->match(Token::TYPE_REQUIRED)) {
186 2
                $required = true;
187 2
                $this->eat(Token::TYPE_REQUIRED);
188
            }
189
190 13
            $variable = new Variable(
191 13
                $nameToken->getData(),
192 13
                $type,
193 13
                $required,
194 13
                $isArray,
195 13
                $arrayElementNullable,
196 13
                new Location($variableToken->getLine(), $variableToken->getColumn())
197
            );
198
199 13
            if ($this->match(Token::TYPE_EQUAL)) {
200 1
                $this->eat(Token::TYPE_EQUAL);
201 1
                $variable->setDefaultValue($this->parseValue());
202
            }
203
204 13
            $this->data['variables'][] = $variable;
205
        }
206
207 13
        $this->expect(Token::TYPE_RPAREN);
208 13
    }
209
210 97
    protected function expectMulti($types)
211
    {
212 97
        if ($this->matchMulti($types)) {
213 97
            return $this->lex();
214
        }
215
216 2
        throw $this->createUnexpectedException($this->peek());
217
    }
218
219 12
    protected function parseVariableReference()
220
    {
221 12
        $startToken = $this->expectMulti([Token::TYPE_VARIABLE]);
222
223 12
        if ($this->match(Token::TYPE_NUMBER) || $this->match(Token::TYPE_IDENTIFIER) || $this->match(Token::TYPE_QUERY)) {
224 12
            $name = $this->lex()->getData();
225
226 12
            $variable = $this->findVariable($name);
227 12
            if ($variable) {
228 12
                $variable->setUsed(true);
229
            }
230
231 12
            $variableReference = new VariableReference($name, $variable, new Location($startToken->getLine(), $startToken->getColumn()));
232
233 12
            $this->data['variableReferences'][] = $variableReference;
234
235 12
            return $variableReference;
236
        }
237
238
        throw $this->createUnexpectedException($this->peek());
239
    }
240
241 12
    protected function findVariable($name)
242
    {
243 12
        foreach ((array) $this->data['variables'] as $variable) {
244
            /** @var $variable Variable */
245 12
            if ($variable->getName() === $name) {
246 12
                return $variable;
247
            }
248
        }
249
250
        return null;
251
    }
252
253 8
    protected function parseFragmentReference()
254
    {
255 8
        $nameToken         = $this->eatIdentifierToken();
256 8
        $fragmentReference = new FragmentReference($nameToken->getData(), new Location($nameToken->getLine(), $nameToken->getColumn()));
257
258 8
        $this->data['fragmentReferences'][] = $fragmentReference;
259
260 8
        return $fragmentReference;
261
    }
262
263 97
    protected function eatIdentifierToken()
264
    {
265 97
        return $this->expectMulti([
266 97
            Token::TYPE_IDENTIFIER,
267 97
            Token::TYPE_MUTATION,
268 97
            Token::TYPE_QUERY,
269 97
            Token::TYPE_FRAGMENT,
270
        ]);
271
    }
272
273 97
    protected function parseBodyItem($type = Token::TYPE_QUERY, $highLevel = true)
274
    {
275 97
        $nameToken = $this->eatIdentifierToken();
276 97
        $alias     = null;
277
278 97
        if ($this->eat(Token::TYPE_COLON)) {
279 20
            $alias     = $nameToken->getData();
280 20
            $nameToken = $this->eatIdentifierToken();
281
        }
282
283 97
        $bodyLocation = new Location($nameToken->getLine(), $nameToken->getColumn());
284 97
        $arguments    = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
285 90
        $directives   = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
286
287 90
        if ($this->match(Token::TYPE_LBRACE)) {
288 61
            $fields = $this->parseBody($type === Token::TYPE_TYPED_FRAGMENT ? Token::TYPE_QUERY : $type, false);
289
290 60
            if (!$fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
291 3
                throw $this->createUnexpectedTokenTypeException($this->lookAhead->getType());
292
            }
293
294 59 View Code Duplication
            if ($type === Token::TYPE_QUERY) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
295 57
                return new Query($nameToken->getData(), $alias, $arguments, $fields, $directives, $bodyLocation);
296 6
            } elseif ($type === Token::TYPE_TYPED_FRAGMENT) {
297 3
                return new TypedFragmentReference($nameToken->getData(), $fields, $directives, $bodyLocation);
298
            } else {
299 3
                return new Mutation($nameToken->getData(), $alias, $arguments, $fields, $directives, $bodyLocation);
300
            }
301 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
302 88
            if ($highLevel && $type === Token::TYPE_MUTATION) {
303 10
                return new Mutation($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
304 79
            } elseif ($highLevel && $type === Token::TYPE_QUERY) {
305 24
                return new Query($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
306
            }
307
308 59
            return new Field($nameToken->getData(), $alias, $arguments, $directives, $bodyLocation);
309
        }
310
    }
311
312 60
    protected function parseArgumentList()
313
    {
314 60
        $args = [];
315
316 60
        $this->expect(Token::TYPE_LPAREN);
317
318 60
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
319 60
            $this->eat(Token::TYPE_COMMA);
320 60
            $args[] = $this->parseArgument();
321
        }
322
323 54
        $this->expect(Token::TYPE_RPAREN);
324
325 53
        return $args;
326
    }
327
328 60 View Code Duplication
    protected function parseArgument()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
329
    {
330 60
        $nameToken = $this->eatIdentifierToken();
331 60
        $this->expect(Token::TYPE_COLON);
332 59
        $value = $this->parseValue();
333
334 55
        return new Argument($nameToken->getData(), $value, new Location($nameToken->getLine(), $nameToken->getColumn()));
0 ignored issues
show
Bug introduced by
It seems like $value defined by $this->parseValue() on line 332 can also be of type array; however, Youshido\GraphQL\Parser\...Argument::__construct() does only seem to accept object<Youshido\GraphQL\...erfaces\ValueInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
335
    }
336
337
    protected function parseDirectiveList()
338
    {
339
        $directives = [];
340
341
        while ($this->match(Token::TYPE_AT)) {
342
            $directives[] = $this->parseDirective();
343
            $this->eat(Token::TYPE_COMMA);
344
        }
345
346
        return $directives;
347
    }
348
349 View Code Duplication
    protected function parseDirective()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
350
    {
351
        $this->expect(Token::TYPE_AT);
352
353
        $nameToken = $this->eatIdentifierToken();
354
        $args      = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
355
356
        return new Directive($nameToken->getData(), $args, new Location($nameToken->getLine(), $nameToken->getColumn()));
357
    }
358
359
    /**
360
     * @return array|InputList|InputObject|Literal|VariableReference
361
     *
362
     * @throws SyntaxErrorException
363
     */
364 59
    protected function parseValue()
365
    {
366 59
        switch ($this->lookAhead->getType()) {
367 59
            case Token::TYPE_LSQUARE_BRACE:
368 11
                return $this->parseList();
369
370 48
            case Token::TYPE_LBRACE:
371 8
                return $this->parseObject();
372
373 40
            case Token::TYPE_VARIABLE:
374 12
                return $this->parseVariableReference();
375
376 33
            case Token::TYPE_NUMBER:
377 25
            case Token::TYPE_STRING:
378 11
            case Token::TYPE_IDENTIFIER:
379 8
            case Token::TYPE_NULL:
380 5
            case Token::TYPE_TRUE:
381 1
            case Token::TYPE_FALSE:
382 32
                $token = $this->lex();
383
384 32
                return new Literal($token->getData(), new Location($token->getLine(), $token->getColumn()));
385
        }
386
387 1
        throw $this->createUnexpectedException($this->lookAhead);
388
    }
389
390 13
    protected function parseList($createType = true)
391
    {
392 13
        $startToken = $this->eat(Token::TYPE_LSQUARE_BRACE);
393
394 13
        $list = [];
395 13
        while (!$this->match(Token::TYPE_RSQUARE_BRACE) && !$this->end()) {
396 13
            $list[] = $this->parseListValue();
397
398 12
            $this->eat(Token::TYPE_COMMA);
399
        }
400
401 12
        $this->expect(Token::TYPE_RSQUARE_BRACE);
402
403 12
        return $createType ? new InputList($list, new Location($startToken->getLine(), $startToken->getColumn())) : $list;
404
    }
405
406 19
    protected function parseListValue()
407
    {
408 19
        switch ($this->lookAhead->getType()) {
409 19
            case Token::TYPE_NUMBER:
410 15
            case Token::TYPE_STRING:
411 9
            case Token::TYPE_TRUE:
412 9
            case Token::TYPE_FALSE:
413 9
            case Token::TYPE_NULL:
414 6
            case Token::TYPE_IDENTIFIER:
415 18
                return $this->expect($this->lookAhead->getType())->getData();
416
417 5
            case Token::TYPE_VARIABLE:
418
                return $this->parseVariableReference();
419
420 5
            case Token::TYPE_LBRACE:
421 4
                return $this->parseObject(true);
422
423 3
            case Token::TYPE_LSQUARE_BRACE:
424 2
                return $this->parseList(false);
425
        }
426
427 1
        throw new SyntaxErrorException('Can\'t parse argument', $this->getLocation());
428
    }
429
430 9
    protected function parseObject($createType = true)
431
    {
432 9
        $startToken = $this->eat(Token::TYPE_LBRACE);
433
434 9
        $object = [];
435 9
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
436 9
            $key = $this->expectMulti([Token::TYPE_STRING, Token::TYPE_IDENTIFIER])->getData();
437 9
            $this->expect(Token::TYPE_COLON);
438 9
            $value = $this->parseListValue();
439
440 9
            $this->eat(Token::TYPE_COMMA);
441
442 9
            $object[$key] = $value;
443
        }
444
445 7
        $this->eat(Token::TYPE_RBRACE);
446
447 7
        return $createType ? new InputObject($object, new Location($startToken->getLine(), $startToken->getColumn())) : $object;
448
    }
449
450 8
    protected function parseFragment()
451
    {
452 8
        $this->lex();
453 8
        $nameToken = $this->eatIdentifierToken();
454
455 8
        $this->eat(Token::TYPE_ON);
456
457 8
        $model = $this->eatIdentifierToken();
458
459 8
        $directives = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
460
461 8
        $fields = $this->parseBody(Token::TYPE_QUERY, false);
462
463 8
        return new Fragment($nameToken->getData(), $model->getData(), $directives, $fields, new Location($nameToken->getLine(), $nameToken->getColumn()));
464
    }
465
466 99
    protected function eat($type)
467
    {
468 99
        if ($this->match($type)) {
469 52
            return $this->lex();
470
        }
471
472 97
        return null;
473
    }
474
475 97
    protected function eatMulti($types)
476
    {
477 97
        if ($this->matchMulti($types)) {
478
            return $this->lex();
479
        }
480
481 97
        return null;
482
    }
483
484 99
    protected function matchMulti($types)
485
    {
486 99
        foreach ((array) $types as $type) {
487 99
            if ($this->peek()->getType() === $type) {
488 99
                return true;
489
            }
490
        }
491
492 97
        return false;
493
    }
494
}
495