Completed
Push — master ( 9459ba...5c21ca )
by Portey
05:54
created

Parser::parseListValue()   C

Complexity

Conditions 10
Paths 10

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 10.0203

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 23
ccs 16
cts 17
cp 0.9412
rs 5.6534
cc 10
eloc 16
nc 10
nop 0
crap 10.0203

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