Completed
Push — master ( b4459a...5882cd )
by Alexandr
03:37
created

Parser::expectMulti()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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