Completed
Push — master ( 11eb7b...58afbd )
by Portey
04:04
created

Processor::parseArgumentsValues()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0176

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 13
ccs 7
cts 8
cp 0.875
rs 9.4286
cc 3
eloc 7
nc 3
nop 2
crap 3.0176
1
<?php
2
/*
3
* This file is a part of graphql-youshido project.
4
*
5
* @author Portey Vasil <[email protected]>
6
* @author Alexandr Viniychuk <[email protected]>
7
* created: 11/28/15 1:05 AM
8
*/
9
10
namespace Youshido\GraphQL;
11
12
use Symfony\Component\PropertyAccess\PropertyAccess;
13
use Symfony\Component\PropertyAccess\PropertyAccessor;
14
use Youshido\GraphQL\Definition\SchemaType;
15
use Youshido\GraphQL\Definition\TypeDefinitionType;
16
use Youshido\GraphQL\Parser\Ast\FragmentReference;
17
use Youshido\GraphQL\Parser\Ast\Mutation;
18
use Youshido\GraphQL\Parser\Ast\Query;
19
use Youshido\GraphQL\Parser\Ast\TypedFragmentReference;
20
use Youshido\GraphQL\Parser\Parser;
21
use Youshido\GraphQL\Type\Field\Field;
22
use Youshido\GraphQL\Type\Object\AbstractEnumType;
23
use Youshido\GraphQL\Type\Object\InputObjectType;
24
use Youshido\GraphQL\Type\Object\ObjectType;
25
use Youshido\GraphQL\Type\Scalar\AbstractScalarType;
26
use Youshido\GraphQL\Type\TypeInterface;
27
use Youshido\GraphQL\Type\TypeMap;
28
use Youshido\GraphQL\Validator\Exception\ResolveException;
29
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidatorInterface;
30
31
class Processor
32
{
33
34
    const TYPE_NAME_QUERY = '__typename';
35
36
    /** @var  array */
37
    protected $data;
38
39
    /** @var ResolveValidatorInterface */
40
    protected $resolveValidator;
41
42
    /** @var Schema */
43
    protected $schema;
44
45
    /** @var PropertyAccessor */
46
    protected $propertyAccessor;
47
48
    /** @var Request */
49
    protected $request;
50
51 18
    public function __construct(ResolveValidatorInterface $validator)
52
    {
53 18
        $this->resolveValidator = $validator;
54
55 18
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
56 18
    }
57
58 18
    public function processQuery($queryString, $variables = [])
59
    {
60 18
        $this->resolveValidator->clearErrors();
61 18
        $this->data = [];
62
63 1
        try {
64 18
            $this->parseAndCreateRequest($queryString, $variables);
65
66 18
            if ($this->request->hasQueries()) {
67 18
                foreach ($this->request->getQueries() as $query) {
68 18
                    if ($queryResult = $this->executeQuery($query, $this->getSchema()->getQueryType())) {
69 17
                        $this->data = array_merge($this->data, $queryResult);
70 17
                    };
71 18
                }
72 18
            }
73
74 18
            if ($this->request->hasMutations()) {
75
                foreach ($this->request->getMutations() as $mutation) {
76
                    if ($mutationResult = $this->executeMutation($mutation, $this->getSchema()->getMutationType())) {
77
                        $this->data = array_merge($this->data, $mutationResult);
78 1
                    }
79
                }
80
            }
81 18
        } catch (\Exception $e) {
82
            $this->resolveValidator->clearErrors();
83
84
            $this->resolveValidator->addError($e);
85
        }
86 18
    }
87
88
    /**
89
     * @param Mutation        $mutation
90
     * @param InputObjectType $objectType
91
     *
92
     * @return array|bool|mixed
93
     */
94
    protected function executeMutation($mutation, $objectType)
95
    {
96
        if (!$this->checkFieldExist($objectType, $mutation)) {
97
98
            return null;
99
        }
100
101
        /** @var Field $field */
102
        $field = $objectType->getConfig()->getField($mutation->getName());
103
104
        if (!$this->resolveValidator->validateArguments($field, $mutation, $this->request)) {
105
            return null;
106
        }
107
108
        $alias         = $mutation->hasAlias() ? $mutation->getAlias() : $mutation->getName();
109
        $resolvedValue = $this->resolveValue($field, null, $mutation);
110
111
        if (!$this->resolveValidator->validateResolvedValue($resolvedValue, $field->getType()->getKind())) {
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...
112
            $this->resolveValidator->addError(new ResolveException(sprintf('Not valid resolved value for mutation "%s"', $field->getType()->getName())));
113
114
            return [$alias => null];
115
        }
116
117
        $value = null;
118
        if ($mutation->hasFields()) {
119
            $outputType = $field->getType()->getConfig()->getOutputType();
120
121
            $value = $this->processQueryFields($mutation, $outputType, $resolvedValue, []);
122
        }
123
124
        return [$alias => $value];
125
    }
126
127 18
    protected function parseAndCreateRequest($query, $variables = [])
128
    {
129 18
        $parser = new Parser();
130
131 18
        $parser->setSource($query);
132 18
        $data = $parser->parse();
133
134 18
        $this->request = new Request($data);
135 18
        $this->request->setVariables($variables);
136 18
    }
137
138
    /**
139
     * @param Query|Field $query
140
     * @param ObjectType  $currentLevelSchema
141
     * @param null        $contextValue
142
     * @return array|bool|mixed
143
     */
144 18
    protected function executeQuery($query, $currentLevelSchema, $contextValue = null)
145
    {
146 18
        if (!$this->checkFieldExist($currentLevelSchema, $query)) {
147
            return null;
148
        }
149
150
        /** @var Field $field */
151 18
        $field = $currentLevelSchema->getConfig()->getField($query->getName());
152 18
        if (get_class($query) == 'Youshido\GraphQL\Parser\Ast\Field') {
153 16
            $alias            = $query->getName();
154 16
            $preResolvedValue = $this->getPreResolvedValue($contextValue, $query);
155
156 16
            if ($field->getConfig()->getType()->getKind() == TypeMap::KIND_LIST) {
157 1
                if(!is_array($preResolvedValue)){
158
                    $value = null;
0 ignored issues
show
Unused Code introduced by
$value is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
159
                    $this->resolveValidator->addError(new ResolveException('Not valid resolve value for list type'));
160
                }
161
162
163 1
                $listValue = [];
164 1
                foreach ($preResolvedValue as $resolvedValueItem) {
0 ignored issues
show
Bug introduced by
The expression $preResolvedValue of type object|integer|double|string|null|boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
165
                    /** @var TypeInterface $type */
166 1
                    $type = $field->getType()->getConfig()->getItem();
167
168 1
                    if ($type->getKind() == TypeMap::KIND_ENUM) {
169
                        /** @var $type AbstractEnumType */
170 1
                        if(!$type->isValidValue($resolvedValueItem)) {
171
                            $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
172
173
                            $listValue = null;
174
                            break;
175
                        }
176
177 1
                        $listValue[] = $type->resolve($resolvedValueItem);
178 1
                    } else {
179
                        /** @var AbstractScalarType $type */
180
                        $listValue[] = $type->serialize($preResolvedValue);
181
                    }
182 1
                }
183
184 1
                $value = $listValue;
185 1
            } else {
186 16 View Code Duplication
                if ($field->getType()->getKind() == TypeMap::KIND_ENUM) {
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...
187
                    if(!$field->getType()->isValidValue($preResolvedValue)) {
188
                        $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
189
                        $value = null;
190
                    } else {
191
                        $value = $field->getType()->resolve($preResolvedValue);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Youshido\GraphQL\Type\TypeInterface as the method resolve() does only exist in the following implementations of said interface: Youshido\GraphQL\Definition\DirectiveListType, Youshido\GraphQL\Definition\DirectiveType, Youshido\GraphQL\Definition\EnumValueListType, Youshido\GraphQL\Definition\EnumValueType, Youshido\GraphQL\Definition\FieldListType, Youshido\GraphQL\Definition\FieldType, Youshido\GraphQL\Definition\InputValueListType, Youshido\GraphQL\Definition\InputValueType, Youshido\GraphQL\Definition\InterfaceListType, Youshido\GraphQL\Definition\InterfaceType, Youshido\GraphQL\Definition\MutationType, Youshido\GraphQL\Definition\QueryListType, Youshido\GraphQL\Definition\QueryType, Youshido\GraphQL\Definition\SchemaType, Youshido\GraphQL\Definition\TypeDefinitionType, Youshido\GraphQL\Type\ListType\AbstractListType, Youshido\GraphQL\Type\ListType\ListType, Youshido\GraphQL\Type\Object\AbstractEnumType, Youshido\GraphQL\Type\Ob...AbstractInputObjectType, Youshido\GraphQL\Type\Object\AbstractObjectType, Youshido\GraphQL\Type\Object\EnumType, Youshido\GraphQL\Type\Object\InputObjectType, Youshido\GraphQL\Type\Object\ObjectType, Youshido\Tests\DataProvider\UserType, Youshido\Tests\StarWars\Schema\DroidType, Youshido\Tests\StarWars\Schema\EpisodeEnum, Youshido\Tests\StarWars\Schema\HumanType, Youshido\Tests\StarWars\Schema\QueryType, Youshido\Tests\Type\Union\Schema\FirstType, Youshido\Tests\Type\Union\Schema\QueryType, Youshido\Tests\Type\Union\Schema\SecondType.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
192
                    }
193
                } else {
194 16
                    $value = $field->getType()->serialize($preResolvedValue);
195
                }
196
            }
197 16
        } else {
198 18
            if (!$this->resolveValidator->validateArguments($field, $query, $this->request)) {
199 1
                return null;
200
            }
201
202 17
            $resolvedValue = $this->resolveValue($field, $contextValue, $query);
203 17
            $alias         = $query->hasAlias() ? $query->getAlias() : $query->getName();
0 ignored issues
show
Bug introduced by
The method hasAlias does only exist in Youshido\GraphQL\Parser\Ast\Query, but not in Youshido\GraphQL\Type\Field\Field.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
Bug introduced by
The method getAlias does only exist in Youshido\GraphQL\Parser\Ast\Query, but not in Youshido\GraphQL\Type\Field\Field.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
204
205 17
            if (!$this->resolveValidator->validateResolvedValue($resolvedValue, $field->getType()->getKind())) {
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...
206
                $this->resolveValidator->addError(new ResolveException(sprintf('Not valid resolved value for query "%s"', $field->getType()->getName())));
207
208
                return [$alias => null];
209
            }
210
211 17
            $value = [];
212 17
            if ($resolvedValue) {
213 16
                if ($field->getType()->getKind() == TypeMap::KIND_LIST) {
214 7
                    foreach ($resolvedValue as $resolvedValueItem) {
215 7
                        $value[] = [];
216 7
                        $index   = count($value) - 1;
217
218 7
                        if(in_array($field->getConfig()->getType()->getConfig()->getItem()->getKind(), [TypeMap::KIND_UNION, TypeMap::KIND_INTERFACE]) ) {
219 4
                            $type = $field->getConfig()->getType()->getConfig()->getItemConfig()->resolveType($resolvedValueItem);
220 4
                        } else {
221 3
                            $type = $field->getType();
222
                        }
223
224 7
                        $value[$index] = $this->processQueryFields($query, $type, $resolvedValueItem, $value[$index]);
225 7
                    }
226 7
                } else {
227 14
                    $value = $this->processQueryFields($query, $field->getType(), $resolvedValue, $value);
228
                }
229 16
            } else {
230 4
                $value = $resolvedValue;
231
            }
232
        }
233
234 17
        return [$alias => $value];
235
    }
236
237
    /**
238
     * @param $objectType InputObjectType|ObjectType
239
     * @param $query      Mutation|Query
240
     * @return null
241
     */
242 18
    private function checkFieldExist($objectType, $query)
243
    {
244 18 View Code Duplication
        if (!$objectType->getConfig()->hasField($query->getName())) {
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...
245
            if ($objectType->getKind() == TypeMap::KIND_LIST) {
246
                $name = $objectType->getConfig()->getItem()->getName();
247
            } else {
248
                $name = $objectType->getName();
249
            }
250
251
            $this->resolveValidator->addError(new ResolveException(sprintf('Field "%s" not found in type "%s"', $query->getName(), $name)));
252
253
            return false;
254
        }
255
256 18
        return true;
257
    }
258
259
    /**
260
     * @param $value
261
     * @param $query Field
262
     *
263
     * @throws \Exception
264
     *
265
     * @return mixed
266
     */
267 16
    protected function getPreResolvedValue($value, $query)
268
    {
269 16
        if (is_array($value)) {
270 11
            if (array_key_exists($query->getName(), $value)) {
271 11
                return $value[$query->getName()];
272
            } else {
273
                throw new \Exception('Not found in resolve result', $query->getName());
274
            }
275 5
        } elseif (is_object($value)) {
276 5
            return $this->propertyAccessor->getValue($value, $query->getName());
277
        }
278
279
        return $value;
280
    }
281
282
    /**
283
     * @param $field        Field
284
     * @param $contextValue mixed
285
     * @param $query        Query
286
     *
287
     * @return mixed
288
     */
289 17
    protected function resolveValue($field, $contextValue, $query)
290
    {
291 17
        $resolvedValue = $field->getConfig()->resolve($contextValue, $this->parseArgumentsValues($field, $query));
292
293 17
        if(in_array($field->getType()->getKind(), [TypeMap::KIND_UNION, TypeMap::KIND_INTERFACE])){
294 6
            $resolvedType = $field->getType()->resolveType($resolvedValue);
295 6
            $field->setType($resolvedType);
296 6
        }
297
298 17
        return $resolvedValue;
299
    }
300
301
    /**
302
     * @param $field     Field
303
     * @param $query     Query
304
     *
305
     * @return array
306
     */
307 17
    public function parseArgumentsValues($field, $query)
308
    {
309 17
        if ($query instanceof \Youshido\GraphQL\Parser\Ast\Field) {
310
            return [];
311
        }
312
313 17
        $args      = [];
314 17
        foreach ($query->getArguments() as $argument) {
315 5
            $args[$argument->getName()] = $field->getConfig()->getArgument($argument->getName())->getType()->parseValue($argument->getValue()->getValue());
316 17
        }
317
318 17
        return $args;
319
    }
320
321
    /**
322
     * @param $query         Query
323
     * @param $queryType     ObjectType|TypeInterface|Field
324
     * @param $resolvedValue mixed
325
     * @param $value         array
326
     *
327
     * @throws \Exception
328
     *
329
     * @return array
330
     */
331 16
    protected function processQueryFields($query, $queryType, $resolvedValue, $value)
332
    {
333 16
        foreach ($query->getFields() as $field) {
334 16
            if ($field instanceof FragmentReference) {
335 1
                if (!$fragment = $this->request->getFragment($field->getName())) {
336
                    throw new \Exception(sprintf('Fragment reference "%s" not found', $field->getName()));
337
                }
338
339 1
                if ($fragment->getModel() !== $queryType->getName()) {
340
                    throw new \Exception(sprintf('Fragment reference "%s" not found on model "%s"', $field->getName(), $queryType->getName()));
341
                }
342
343 1
                foreach ($fragment->getFields() as $fragmentField) {
344 1
                    $value = $this->collectValue($value, $this->executeQuery($fragmentField, $queryType, $resolvedValue));
345 1
                }
346 16
            } elseif ($field instanceof TypedFragmentReference) {
347 2
                if ($field->getTypeName() !== $queryType->getName()) {
348 1
                    continue;
349
                }
350
351 2
                foreach ($field->getFields() as $fragmentField) {
352 2
                    $value = $this->collectValue($value, $this->executeQuery($fragmentField, $queryType, $resolvedValue));
0 ignored issues
show
Documentation introduced by
$fragmentField is of type object<Youshido\GraphQL\Parser\Ast\Field>, but the function expects a object<Youshido\GraphQL\...aphQL\Type\Field\Field>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
353 2
                }
354 16
            } elseif ($field->getName() == self::TYPE_NAME_QUERY) {
355 1
                $value = $this->collectValue($value, [$field->getAlias() ?: $field->getName() => $queryType->getName()]);
356 1
            } else {
357 16
                $value = $this->collectValue($value, $this->executeQuery($field, $queryType, $resolvedValue));
358
            }
359 16
        }
360
361 16
        return $value;
362
    }
363
364 16
    protected function collectValue($value, $queryValue)
365
    {
366 16
        if ($queryValue && is_array($queryValue)) {
367 16
            $value = array_merge($value, $queryValue);
368 16
        } else {
369
            $value = $queryValue;
370
        }
371
372 16
        return $value;
373
    }
374
375 18
    public function getSchema()
376
    {
377 18
        return $this->schema;
378
    }
379
380 18
    public function setSchema(Schema $schema)
381
    {
382 18
        $this->schema = $schema;
383
384 18
        $__schema = new SchemaType();
385 18
        $__schema->setSchema($schema);
386
387 18
        $__type = new TypeDefinitionType();
388 18
        $__type->setSchema($schema);
389
390 18
        $this->schema->addQuery('__schema', $__schema);
391 18
        $this->schema->addQuery('__type', $__type);
392 18
    }
393
394
    /**
395
     * @return ResolveValidatorInterface
396
     */
397
    public function getResolveValidator()
398
    {
399
        return $this->resolveValidator;
400
    }
401
402 18
    public function getResponseData()
403
    {
404 18
        $result = [];
405
406 18
        if (!empty($this->data)) {
407 17
            $result['data'] = $this->data;
408 17
        }
409
410 18
        if ($this->resolveValidator->hasErrors()) {
411 1
            $result['errors'] = $this->resolveValidator->getErrorsArray();
412 1
        }
413
414 18
        return $result;
415
    }
416
}
417