Completed
Pull Request — master (#150)
by
unknown
06:33
created

Parser::init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1

Importance

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