Completed
Push — master ( d52ee7...e7842c )
by Alexandr
03:54
created

Parser::parseBodyItem()   C

Complexity

Conditions 11
Paths 24

Size

Total Lines 32
Code Lines 21

Duplication

Lines 16
Ratio 50 %

Code Coverage

Tests 20
CRAP Score 11

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 16
loc 32
rs 5.2653
ccs 20
cts 20
cp 1
cc 11
eloc 21
nc 24
nop 2
crap 11

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