Completed
Pull Request — master (#161)
by Sam
04:21
created

Parser::parseObject()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 12
cts 12
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 11
nc 4
nop 1
crap 4
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 51
                    }
45
46 51
                    break;
47 50
                case Token::TYPE_QUERY:
48 29
                    $query = $this->parseOperation(Token::TYPE_QUERY);
49 27
                    if ($query instanceof Query) {
50 25
                        $this->data['queries'][] = $query;
51 25
                    }
52
53 27
                    break;
54 26
                case Token::TYPE_MUTATION:
55 17
                    $mutation = $this->parseOperation(Token::TYPE_MUTATION);
56 12
                    if ($mutation instanceof Mutation) {
57 12
                        $this->data['mutations'][] = $mutation;
58 12
                    }
59
60 12
                    break;
61
62 9
                case Token::TYPE_FRAGMENT:
63 8
                    $this->data['fragments'][] = $this->parseFragment();
64
65 8
                    break;
66
67 1
                default:
68 1
                    throw new SyntaxErrorException('Incorrect request syntax', $this->getLocation());
69 1
            }
70 89
        }
71
72 90
        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 102
            'queries'            => [],
81 102
            'mutations'          => [],
82 102
            'fragments'          => [],
83 102
            'fragmentReferences' => [],
84 102
            'variables'          => [],
85 102
            'variableReferences' => [],
86
        ];
87 102
    }
88
89 46
    protected function parseOperation($type = Token::TYPE_QUERY)
90
    {
91 46
        $operation  = null;
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 13
            }
102
103 46
            if ($this->match(Token::TYPE_AT)) {
104
                $directives = $this->parseDirectiveList();
105
            }
106
107 46
        }
108
109 46
        $this->lex();
110
111 46
        if (!$this->match(Token::TYPE_RBRACE)) {
112 44
            $operation = $this->parseBodyItem($type, true);
113 39
            $operation->setDirectives($directives);
114 39
        }
115
116 41
        $this->expect(Token::TYPE_RBRACE);
117
118
119 39
        return $operation;
120
    }
121
122 80
    protected function parseBody($token = Token::TYPE_QUERY, $highLevel = true)
123
    {
124 80
        $fields = [];
125
126 80
        $this->lex();
127
128 80
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
129 79
            $this->eatMulti([Token::TYPE_COMMA]);
130
131 79
            if ($this->match(Token::TYPE_FRAGMENT_REFERENCE)) {
132 10
                $this->lex();
133
134 10
                if ($this->eat(Token::TYPE_ON)) {
135 3
                    $fields[] = $this->parseBodyItem(Token::TYPE_TYPED_FRAGMENT, $highLevel);
136 3
                } else {
137 8
                    $fields[] = $this->parseFragmentReference();
138
                }
139 10
            } else {
140 78
                $fields[] = $this->parseBodyItem($token, $highLevel);
141
            }
142 75
        }
143
144 76
        $this->expect(Token::TYPE_RBRACE);
145
146 76
        return $fields;
147
    }
148
149 13
    protected function parseVariables()
150
    {
151 13
        $this->eat(Token::TYPE_LPAREN);
152
153 13
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
154 13
            $this->eat(Token::TYPE_COMMA);
155
156 13
            $variableToken = $this->eat(Token::TYPE_VARIABLE);
157 13
            $nameToken     = $this->eatIdentifierToken();
158 13
            $this->eat(Token::TYPE_COLON);
159
160 13
            $isArray              = false;
161 13
            $arrayElementNullable = true;
162
163 13
            if ($this->match(Token::TYPE_LSQUARE_BRACE)) {
164 3
                $isArray = true;
165
166 3
                $this->eat(Token::TYPE_LSQUARE_BRACE);
167 3
                $type = $this->eatIdentifierToken()->getData();
168
169 3
                if ($this->match(Token::TYPE_REQUIRED)) {
170 2
                    $arrayElementNullable = false;
171 2
                    $this->eat(Token::TYPE_REQUIRED);
172 2
                }
173
174 3
                $this->eat(Token::TYPE_RSQUARE_BRACE);
175 3
            } else {
176 12
                $type = $this->eatIdentifierToken()->getData();
177
            }
178
179 13
            $required = false;
180 13
            if ($this->match(Token::TYPE_REQUIRED)) {
181 2
                $required = true;
182 2
                $this->eat(Token::TYPE_REQUIRED);
183 2
            }
184
185 13
            $variable = new Variable(
186 13
                $nameToken->getData(),
187 13
                $type,
188 13
                $required,
189 13
                $isArray,
190 13
                $arrayElementNullable,
191 13
                new Location($variableToken->getLine(), $variableToken->getColumn())
192 13
            );
193
194 13
            if ($this->match(Token::TYPE_EQUAL)) {
195 1
                $this->eat(Token::TYPE_EQUAL);
196 1
                $variable->setDefaultValue($this->parseValue());
197 1
            }
198
199 13
            $this->data['variables'][] = $variable;
200 13
        }
201
202 13
        $this->expect(Token::TYPE_RPAREN);
203 13
    }
204
205 97
    protected function expectMulti($types)
206
    {
207 97
        if ($this->matchMulti($types)) {
208 97
            return $this->lex();
209
        }
210
211 2
        throw $this->createUnexpectedException($this->peek());
212
    }
213
214 12
    protected function parseVariableReference()
215
    {
216 12
        $startToken = $this->expectMulti([Token::TYPE_VARIABLE]);
217
218 12
        if ($this->match(Token::TYPE_NUMBER) || $this->match(Token::TYPE_IDENTIFIER) || $this->match(Token::TYPE_QUERY)) {
219 12
            $name = $this->lex()->getData();
220
221 12
            $variable = $this->findVariable($name);
222 12
            if ($variable) {
223 12
                $variable->setUsed(true);
224 12
            }
225
226 12
            $variableReference = new VariableReference($name, $variable, new Location($startToken->getLine(), $startToken->getColumn()));
227
228 12
            $this->data['variableReferences'][] = $variableReference;
229
230 12
            return $variableReference;
231
        }
232
233
        throw $this->createUnexpectedException($this->peek());
234
    }
235
236 12
    protected function findVariable($name)
237
    {
238 12
        foreach ((array) $this->data['variables'] as $variable) {
239
            /** @var $variable Variable */
240 12
            if ($variable->getName() === $name) {
241 12
                return $variable;
242
            }
243 3
        }
244
245
        return null;
246
    }
247
248 8
    protected function parseFragmentReference()
249
    {
250 8
        $nameToken         = $this->eatIdentifierToken();
251 8
        $fragmentReference = new FragmentReference($nameToken->getData(), new Location($nameToken->getLine(), $nameToken->getColumn()));
252
253 8
        $this->data['fragmentReferences'][] = $fragmentReference;
254
255 8
        return $fragmentReference;
256
    }
257
258 97
    protected function eatIdentifierToken()
259
    {
260 97
        return $this->expectMulti([
261 97
            Token::TYPE_IDENTIFIER,
262 97
            Token::TYPE_MUTATION,
263 97
            Token::TYPE_QUERY,
264 97
            Token::TYPE_FRAGMENT,
265 97
        ]);
266
    }
267
268 97
    protected function parseBodyItem($type = Token::TYPE_QUERY, $highLevel = true)
269
    {
270 97
        $nameToken = $this->eatIdentifierToken();
271 97
        $alias     = null;
272
273 97
        if ($this->eat(Token::TYPE_COLON)) {
274 20
            $alias     = $nameToken->getData();
275 20
            $nameToken = $this->eatIdentifierToken();
276 20
        }
277
278 97
        $bodyLocation = new Location($nameToken->getLine(), $nameToken->getColumn());
279 97
        $arguments    = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
280 90
        $directives   = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
281
282 90
        if ($this->match(Token::TYPE_LBRACE)) {
283 61
            $fields = $this->parseBody($type === Token::TYPE_TYPED_FRAGMENT ? Token::TYPE_QUERY : $type, false);
284
285 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...
286 3
                throw $this->createUnexpectedTokenTypeException($this->lookAhead->getType());
287
            }
288
289 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...
290 57
                return new Query($nameToken->getData(), $alias, $arguments, $fields, $directives, $bodyLocation);
291 6
            } elseif ($type === Token::TYPE_TYPED_FRAGMENT) {
292 3
                return new TypedFragmentReference($nameToken->getData(), $fields, $directives, $bodyLocation);
293
            } else {
294 3
                return new Mutation($nameToken->getData(), $alias, $arguments, $fields, $directives, $bodyLocation);
295
            }
296 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...
297 88
            if ($highLevel && $type === Token::TYPE_MUTATION) {
298 10
                return new Mutation($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
299 79
            } elseif ($highLevel && $type === Token::TYPE_QUERY) {
300 24
                return new Query($nameToken->getData(), $alias, $arguments, [], $directives, $bodyLocation);
301
            }
302
303 59
            return new Field($nameToken->getData(), $alias, $arguments, $directives, $bodyLocation);
304
        }
305
    }
306
307 60
    protected function parseArgumentList()
308
    {
309 60
        $args = [];
310
311 60
        $this->expect(Token::TYPE_LPAREN);
312
313 60
        while (!$this->match(Token::TYPE_RPAREN) && !$this->end()) {
314 60
            $this->eat(Token::TYPE_COMMA);
315 60
            $args[] = $this->parseArgument();
316 55
        }
317
318 54
        $this->expect(Token::TYPE_RPAREN);
319
320 53
        return $args;
321
    }
322
323 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...
324
    {
325 60
        $nameToken = $this->eatIdentifierToken();
326 60
        $this->expect(Token::TYPE_COLON);
327 59
        $value = $this->parseValue();
328
329 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 327 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...
330
    }
331
332
    protected function parseDirectiveList()
333
    {
334
        $directives = [];
335
336
        while ($this->match(Token::TYPE_AT)) {
337
            $directives[] = $this->parseDirective();
338
            $this->eat(Token::TYPE_COMMA);
339
        }
340
341
        return $directives;
342
    }
343
344 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...
345
    {
346
        $this->expect(Token::TYPE_AT);
347
348
        $nameToken = $this->eatIdentifierToken();
349
        $args      = $this->match(Token::TYPE_LPAREN) ? $this->parseArgumentList() : [];
350
351
        return new Directive($nameToken->getData(), $args, new Location($nameToken->getLine(), $nameToken->getColumn()));
352
    }
353
354
    /**
355
     * @return array|InputList|InputObject|Literal|VariableReference
356
     *
357
     * @throws SyntaxErrorException
358
     */
359 59
    protected function parseValue()
360
    {
361 59
        switch ($this->lookAhead->getType()) {
362 59
            case Token::TYPE_LSQUARE_BRACE:
363 11
                return $this->parseList();
364
365 48
            case Token::TYPE_LBRACE:
366 8
                return $this->parseObject();
367
368 40
            case Token::TYPE_VARIABLE:
369 12
                return $this->parseVariableReference();
370
371 33
            case Token::TYPE_NUMBER:
372 33
            case Token::TYPE_STRING:
373 33
            case Token::TYPE_IDENTIFIER:
374 33
            case Token::TYPE_NULL:
375 33
            case Token::TYPE_TRUE:
376 33
            case Token::TYPE_FALSE:
377 32
                $token = $this->lex();
378
379 32
                return new Literal($token->getData(), new Location($token->getLine(), $token->getColumn()));
380 1
        }
381
382 1
        throw $this->createUnexpectedException($this->lookAhead);
383
    }
384
385 13
    protected function parseList($createType = true)
386
    {
387 13
        $startToken = $this->eat(Token::TYPE_LSQUARE_BRACE);
388
389 13
        $list = [];
390 13
        while (!$this->match(Token::TYPE_RSQUARE_BRACE) && !$this->end()) {
391 13
            $list[] = $this->parseListValue();
392
393 12
            $this->eat(Token::TYPE_COMMA);
394 12
        }
395
396 12
        $this->expect(Token::TYPE_RSQUARE_BRACE);
397
398 12
        return $createType ? new InputList($list, new Location($startToken->getLine(), $startToken->getColumn())) : $list;
399
    }
400
401 19
    protected function parseListValue()
402
    {
403 19
        switch ($this->lookAhead->getType()) {
404 19
            case Token::TYPE_NUMBER:
405 19
            case Token::TYPE_STRING:
406 19
            case Token::TYPE_TRUE:
407 19
            case Token::TYPE_FALSE:
408 19
            case Token::TYPE_NULL:
409 19
            case Token::TYPE_IDENTIFIER:
410 18
                return $this->expect($this->lookAhead->getType())->getData();
411
412 5
            case Token::TYPE_VARIABLE:
413
                return $this->parseVariableReference();
414
415 5
            case Token::TYPE_LBRACE:
416 4
                return $this->parseObject(true);
417
418 3
            case Token::TYPE_LSQUARE_BRACE:
419 2
                return $this->parseList(false);
420 1
        }
421
422 1
        throw new SyntaxErrorException('Can\'t parse argument', $this->getLocation());
423
    }
424
425 9
    protected function parseObject($createType = true)
426
    {
427 9
        $startToken = $this->eat(Token::TYPE_LBRACE);
428
429 9
        $object = [];
430 9
        while (!$this->match(Token::TYPE_RBRACE) && !$this->end()) {
431 9
            $key = $this->expectMulti([Token::TYPE_STRING, Token::TYPE_IDENTIFIER])->getData();
432 9
            $this->expect(Token::TYPE_COLON);
433 9
            $value = $this->parseListValue();
434
435 9
            $this->eat(Token::TYPE_COMMA);
436
437 9
            $object[$key] = $value;
438 9
        }
439
440 7
        $this->eat(Token::TYPE_RBRACE);
441
442 7
        return $createType ? new InputObject($object, new Location($startToken->getLine(), $startToken->getColumn())) : $object;
443
    }
444
445 8
    protected function parseFragment()
446
    {
447 8
        $this->lex();
448 8
        $nameToken = $this->eatIdentifierToken();
449
450 8
        $this->eat(Token::TYPE_ON);
451
452 8
        $model = $this->eatIdentifierToken();
453
454 8
        $directives = $this->match(Token::TYPE_AT) ? $this->parseDirectiveList() : [];
455
456 8
        $fields = $this->parseBody(Token::TYPE_QUERY, false);
457
458 8
        return new Fragment($nameToken->getData(), $model->getData(), $directives, $fields, new Location($nameToken->getLine(), $nameToken->getColumn()));
459
    }
460
461 99
    protected function eat($type)
462
    {
463 99
        if ($this->match($type)) {
464 52
            return $this->lex();
465
        }
466
467 97
        return null;
468
    }
469
470 79
    protected function eatMulti($types)
471
    {
472 79
        if ($this->matchMulti($types)) {
473
            return $this->lex();
474
        }
475
476 79
        return null;
477
    }
478
479 99
    protected function matchMulti($types)
480
    {
481 99
        foreach ((array) $types as $type) {
482 99
            if ($this->peek()->getType() === $type) {
483 99
                return true;
484
            }
485 92
        }
486
487 80
        return false;
488
    }
489
}
490