Completed
Push — master ( 150b90...d63187 )
by Portey
04:50
created

Parser::parseVariableReference()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

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