Completed
Push — master ( 5bf303...8cd23b )
by Portey
03:14
created

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