Completed
Pull Request — master (#133)
by Adrien
04:00
created

Parser::parseArgument()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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