Completed
Pull Request — master (#188)
by Sebastian
02:55
created

Parser::parseBodyItem()   C

Complexity

Conditions 13
Paths 56

Size

Total Lines 38
Code Lines 25

Duplication

Lines 16
Ratio 42.11 %

Code Coverage

Tests 24
CRAP Score 13

Importance

Changes 0
Metric Value
dl 16
loc 38
ccs 24
cts 24
cp 1
rs 5.1234
c 0
b 0
f 0
cc 13
eloc 25
nc 56
nop 2
crap 13

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 106
    public function parse($source = null)
34
    {
35 106
        $this->init($source);
36
37 106
        while (!$this->end()) {
38 105
            $tokenType = $this->peek()->getType();
39
40
            switch ($tokenType) {
41 105
                case Token::TYPE_LBRACE:
42 57
                    foreach ($this->parseBody() as $query) {
43 52
                        $this->data['queries'][] = $query;
44 53
                    }
45
46 53
                    break;
47 53
                case Token::TYPE_QUERY:
48 31
                    $queries = $this->parseOperation(Token::TYPE_QUERY);
49 31
                    foreach ($queries as $query) {
50 29
                        $this->data['queries'][] = $query;
51 31
                    }
52
53 31
                    break;
54 29
                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 12
                    }
59
60 12
                    break;
61
62 12
                case Token::TYPE_FRAGMENT:
63 11
                    $this->data['fragments'][] = $this->parseFragment();
64
65 11
                    break;
66
67 1
                default:
68 1
                    throw new SyntaxErrorException('Incorrect request syntax', $this->getLocation());
69 1
            }
70 95
        }
71
72 96
        return $this->data;
73
    }
74
75 106 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 106
        $this->initTokenizer($source);
78
79 106
        $this->data = [
80 106
            'queries'            => [],
81 106
            'mutations'          => [],
82 106
            'fragments'          => [],
83 106
            'fragmentReferences' => [],
84 106
            'variables'          => [],
85 106
            'variableReferences' => [],
86
        ];
87 106
    }
88
89 48
    protected function parseOperation($type = Token::TYPE_QUERY)
90
    {
91 48
        $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 48
        $directives = [];
93
94 48
        if ($this->matchMulti([Token::TYPE_QUERY, Token::TYPE_MUTATION])) {
95 48
            $this->lex();
96
97 48
            $this->eat(Token::TYPE_IDENTIFIER);
98
99 48
            if ($this->match(Token::TYPE_LPAREN)) {
100 13
                $this->parseVariables();
101 13
            }
102
103 48
            if ($this->match(Token::TYPE_AT)) {
104
                $directives = $this->parseDirectiveList();
105
            }
106
107 48
        }
108
109 48
        $this->lex();
110
111 48
        $fields = [];
112
113 48
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
114 46
            $this->eatMulti([Token::TYPE_COMMA]);
115
116 46
            $operation = $this->parseBodyItem($type, true);
117 41
            $operation->setDirectives($directives);
118
119 41
            $fields[] = $operation;
120 41
        }
121
122 43
        $this->expect(Token::TYPE_RBRACE);
123
124 43
        return $fields;
125
    }
126
127 84
    protected function parseBody($token = Token::TYPE_QUERY, $highLevel = true)
128
    {
129 84
        $fields = [];
130
131 84
        $this->lex();
132
133 84
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
134 83
            $this->eatMulti([Token::TYPE_COMMA]);
135
136 83
            if ($this->match(Token::TYPE_FRAGMENT_REFERENCE)) {
137 13
                $this->lex();
138
139 13
                if ($this->eat(Token::TYPE_ON)) {
140 4
                    $fields[] = $this->parseBodyItem(Token::TYPE_TYPED_FRAGMENT, $highLevel);
141 4
                } else {
142 11
                    $fields[] = $this->parseFragmentReference();
143
                }
144 13
            } else {
145 82
                $fields[] = $this->parseBodyItem($token, $highLevel);
146
            }
147 79
        }
148
149 80
        $this->expect(Token::TYPE_RBRACE);
150
151 80
        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 2
                }
178
179 3
                $this->eat(Token::TYPE_RSQUARE_BRACE);
180 3
            } 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 2
            }
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 13
            );
198
199 13
            if ($this->match(Token::TYPE_EQUAL)) {
200 1
                $this->eat(Token::TYPE_EQUAL);
201 1
                $variable->setDefaultValue($this->parseValue());
202 1
            }
203
204 13
            $this->data['variables'][] = $variable;
205 13
        }
206
207 13
        $this->expect(Token::TYPE_RPAREN);
208 13
    }
209
210 101
    protected function expectMulti($types)
211
    {
212 101
        if ($this->matchMulti($types)) {
213 101
            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 12
            }
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 3
        }
249
250
        return null;
251
    }
252
253 11
    protected function parseFragmentReference()
254
    {
255 11
        $nameToken         = $this->eatIdentifierToken();
256 11
        $fragmentReference = new FragmentReference($nameToken->getData(), new Location($nameToken->getLine(), $nameToken->getColumn()));
257
258 11
        $this->data['fragmentReferences'][] = $fragmentReference;
259
260 11
        return $fragmentReference;
261
    }
262
263 101
    protected function eatIdentifierToken()
264
    {
265 101
        return $this->expectMulti([
266 101
            Token::TYPE_IDENTIFIER,
267 101
            Token::TYPE_MUTATION,
268 101
            Token::TYPE_QUERY,
269 101
            Token::TYPE_FRAGMENT,
270 101
        ]);
271
    }
272
273 101
    protected function parseBodyItem($type = Token::TYPE_QUERY, $highLevel = true)
274
    {
275 101
        $nameToken = $this->eatIdentifierToken();
276 101
        $alias     = null;
277
278 101
        if ($this->eat(Token::TYPE_COLON)) {
279 20
            $alias     = $nameToken->getData();
280 20
            $nameToken = $this->eatIdentifierToken();
281 20
        }
282
283 101
        $bodyLocation = new Location($nameToken->getLine(), $nameToken->getColumn());
284 101
        $arguments    = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
285 94
        $directives   = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
286
287 94
        if ($this->match(Token::TYPE_LBRACE)) {
288 65
            $fields = $this->parseBody($type === Token::TYPE_TYPED_FRAGMENT ? Token::TYPE_QUERY : $type, false);
289
290 64
            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 63 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 61
                return new Query($nameToken->getData(), $alias, $arguments, $fields, $directives, $bodyLocation);
296 7
            } elseif ($type === Token::TYPE_TYPED_FRAGMENT) {
297 4
                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 92
            if ($highLevel && $type === Token::TYPE_MUTATION) {
303 10
                return new Mutation($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
304 83
            } elseif ($highLevel && $type === Token::TYPE_QUERY) {
305 24
                return new Query($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
306
            }
307
308 63
            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 55
        }
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 33
            case Token::TYPE_STRING:
378 33
            case Token::TYPE_IDENTIFIER:
379 33
            case Token::TYPE_NULL:
380 33
            case Token::TYPE_TRUE:
381 33
            case Token::TYPE_FALSE:
382 32
                $token = $this->lex();
383
384 32
                return new Literal($token->getData(), new Location($token->getLine(), $token->getColumn()));
385 1
        }
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 12
        }
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 19
            case Token::TYPE_STRING:
411 19
            case Token::TYPE_TRUE:
412 19
            case Token::TYPE_FALSE:
413 19
            case Token::TYPE_NULL:
414 19
            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 1
        }
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 9
        }
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 11
    protected function parseFragment()
451
    {
452 11
        $this->lex();
453 11
        $nameToken = $this->eatIdentifierToken();
454
455 11
        $this->eat(Token::TYPE_ON);
456
457 11
        $model = $this->eatIdentifierToken();
458
459 11
        $directives = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
460
461 11
        $fields = $this->parseBody(Token::TYPE_QUERY, false);
462
463 11
        return new Fragment($nameToken->getData(), $model->getData(), $directives, $fields, new Location($nameToken->getLine(), $nameToken->getColumn()));
464
    }
465
466 103
    protected function eat($type)
467
    {
468 103
        if ($this->match($type)) {
469 55
            return $this->lex();
470
        }
471
472 101
        return null;
473
    }
474
475 101
    protected function eatMulti($types)
476
    {
477 101
        if ($this->matchMulti($types)) {
478
            return $this->lex();
479
        }
480
481 101
        return null;
482
    }
483
484 103
    protected function matchMulti($types)
485
    {
486 103
        foreach ((array) $types as $type) {
487 103
            if ($this->peek()->getType() === $type) {
488 103
                return true;
489
            }
490 101
        }
491
492 101
        return false;
493
    }
494
}
495