Completed
Push — master ( 17976d...325209 )
by Portey
07:00
created

Processor::collectValue()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0417

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 10
ccs 5
cts 6
cp 0.8333
rs 9.4286
cc 3
eloc 6
nc 2
nop 2
crap 3.0417
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\Parser;
20
use Youshido\GraphQL\Type\Field\Field;
21
use Youshido\GraphQL\Type\Object\AbstractEnumType;
22
use Youshido\GraphQL\Type\Object\InputObjectType;
23
use Youshido\GraphQL\Type\Object\ObjectType;
24
use Youshido\GraphQL\Type\Scalar\AbstractScalarType;
25
use Youshido\GraphQL\Type\TypeInterface;
26
use Youshido\GraphQL\Type\TypeMap;
27
use Youshido\GraphQL\Validator\Exception\ResolveException;
28
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidatorInterface;
29
30
class Processor
31
{
32
33
    const TYPE_NAME_QUERY = '__typename';
34
35
    /** @var  array */
36
    protected $data;
37
38
    /** @var ResolveValidatorInterface */
39
    protected $resolveValidator;
40
41
    /** @var Schema */
42
    protected $schema;
43
44
    /** @var PropertyAccessor */
45
    protected $propertyAccessor;
46
47
    /** @var Request */
48
    protected $request;
49
50 15
    public function __construct(ResolveValidatorInterface $validator)
51
    {
52 15
        $this->resolveValidator = $validator;
53
54 15
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
55 15
    }
56
57 15
    public function processQuery($queryString, $variables = [])
58
    {
59 15
        $this->resolveValidator->clearErrors();
60 15
        $this->data = [];
61
62
        try {
63 15
            $this->parseAndCreateRequest($queryString, $variables);
64
65 15
            if ($this->request->hasQueries()) {
66 15
                foreach ($this->request->getQueries() as $query) {
67 15
                    if ($queryResult = $this->executeQuery($query, $this->getSchema()->getQueryType())) {
68 14
                        $this->data = array_merge($this->data, $queryResult);
69 14
                    };
70 15
                }
71 15
            }
72
73 15
            if ($this->request->hasMutations()) {
74
                foreach ($this->request->getMutations() as $mutation) {
75
                    if ($mutationResult = $this->executeMutation($mutation, $this->getSchema()->getMutationType())) {
76
                        $this->data = array_merge($this->data, $mutationResult);
77
                    }
78 1
                }
79
            }
80 15
        } catch (\Exception $e) {
81
            $this->resolveValidator->clearErrors();
82
83
            $this->resolveValidator->addError($e);
84
        }
85 15
    }
86
87
    /**
88
     * @param Mutation        $mutation
89
     * @param InputObjectType $objectType
90
     *
91
     * @return array|bool|mixed
92
     */
93
    protected function executeMutation($mutation, $objectType)
94
    {
95
        if (!$this->checkFieldExist($objectType, $mutation)) {
96
97
            return null;
98
        }
99
100
        /** @var Field $field */
101
        $field = $objectType->getConfig()->getField($mutation->getName());
102
103
        if (!$this->resolveValidator->validateArguments($field, $mutation, $this->request)) {
104
            return null;
105
        }
106
107
        $alias         = $mutation->hasAlias() ? $mutation->getAlias() : $mutation->getName();
108
        $resolvedValue = $this->resolveValue($field, null, $mutation);
109
110
        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...
111
            $this->resolveValidator->addError(new ResolveException(sprintf('Not valid resolved value for mutation "%s"', $field->getType()->getName())));
112
113
            return [$alias => null];
114
        }
115
116
        $value = null;
117
        if ($mutation->hasFields()) {
118
            $outputType = $field->getType()->getConfig()->getOutputType();
119
120
            $value = $this->processQueryFields($mutation, $outputType, $resolvedValue, []);
121
        }
122
123
        return [$alias => $value];
124
    }
125
126 15
    protected function parseAndCreateRequest($query, $variables = [])
127
    {
128 15
        $parser = new Parser();
129
130 15
        $parser->setSource($query);
131 15
        $data = $parser->parse();
132
133 15
        $this->request = new Request($data);
134 15
        $this->request->setVariables($variables);
135 15
    }
136
137
    /**
138
     * @param Query|Field $query
139
     * @param ObjectType  $currentLevelSchema
140
     * @param null        $contextValue
141
     * @return array|bool|mixed
142
     */
143 15
    protected function executeQuery($query, $currentLevelSchema, $contextValue = null)
144
    {
145 15
        if (!$this->checkFieldExist($currentLevelSchema, $query)) {
146
            return null;
147
        }
148
149
        /** @var Field $field */
150 15
        $field = $currentLevelSchema->getConfig()->getField($query->getName());
151 15
        if (get_class($query) == 'Youshido\GraphQL\Parser\Ast\Field') {
152 13
            $alias            = $query->getName();
153 13
            $preResolvedValue = $this->getPreResolvedValue($contextValue, $query);
154
155 13
            if ($field->getConfig()->getType()->getKind() == TypeMap::KIND_LIST) {
156 1
                if(!is_array($preResolvedValue)){
157
                    $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...
158
                    $this->resolveValidator->addError(new ResolveException('Not valid resolve value for list type'));
159
                }
160
161
162 1
                $listValue = [];
163 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...
164
                    /** @var TypeInterface $type */
165 1
                    $type = $field->getType()->getConfig()->getItem();
166
167 1
                    if ($type->getKind() == TypeMap::KIND_ENUM) {
168
                        /** @var $type AbstractEnumType */
169 1
                        if(!$type->isValidValue($resolvedValueItem)) {
170
                            $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
171
172
                            $listValue = null;
173
                            break;
174
                        }
175
176 1
                        $listValue[] = $type->resolve($resolvedValueItem);
177 1
                    } else {
178
                        /** @var AbstractScalarType $type */
179
                        $listValue[] = $type->serialize($preResolvedValue);
180
                    }
181 1
                }
182
183 1
                $value = $listValue;
184 1
            } else {
185 13
                if ($field->getType()->getKind() == TypeMap::KIND_ENUM) {
186
                    if(!$field->getType()->isValidValue($preResolvedValue)) {
187
                        $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
188
                        $value = null;
189
                    } else {
190
                        $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\ArgumentListType, Youshido\GraphQL\Definition\ArgumentType, 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\PossibleOfListType, Youshido\GraphQL\Definition\PossibleOfType, 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...
191
                    }
192
                } else {
193 13
                    $value = $field->getType()->serialize($preResolvedValue);
194
                }
195
            }
196 13
        } else {
197 15
            if (!$this->resolveValidator->validateArguments($field, $query, $this->request)) {
198 1
                return null;
199
            }
200
201 14
            $resolvedValue = $this->resolveValue($field, $contextValue, $query);
202 14
            $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...
203
204 14
            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...
205
                $this->resolveValidator->addError(new ResolveException(sprintf('Not valid resolved value for query "%s"', $field->getType()->getName())));
206
207
                return [$alias => null];
208
            }
209
210 14
            $value = [];
211 14
            if ($resolvedValue) {
212 13
                if ($field->getType()->getKind() == TypeMap::KIND_LIST) {
213 5
                    foreach ($resolvedValue as $resolvedValueItem) {
214 5
                        $value[] = [];
215 5
                        $index   = count($value) - 1;
216
217 5
                        if(in_array($field->getConfig()->getType()->getConfig()->getItem()->getKind(), [TypeMap::KIND_UNION, TypeMap::KIND_INTERFACE]) ) {
218 3
                            $type = $field->getConfig()->getType()->getConfig()->getItemConfig()->resolveType($resolvedValueItem);
219 3
                        } else {
220 2
                            $type = $field->getType();
221
                        }
222
223 5
                        $value[$index] = $this->processQueryFields($query, $type, $resolvedValueItem, $value[$index]);
224 5
                    }
225 5
                } else {
226 12
                    $value = $this->processQueryFields($query, $field->getType(), $resolvedValue, $value);
227
                }
228 13
            } else {
229 3
                $value = $resolvedValue;
230
            }
231
        }
232
233 14
        return [$alias => $value];
234
    }
235
236
    /**
237
     * @param $objectType InputObjectType|ObjectType
238
     * @param $query      Mutation|Query
239
     * @return null
240
     */
241 15
    private function checkFieldExist($objectType, $query)
242
    {
243 15
        if (!$objectType->getConfig()->hasField($query->getName())) {
244
            $this->resolveValidator->addError(new ResolveException(sprintf('Field "%s" not found in schema', $query->getName())));
245
246
            return false;
247
        }
248
249 15
        return true;
250
    }
251
252
    /**
253
     * @param $value
254
     * @param $query Field
255
     *
256
     * @throws \Exception
257
     *
258
     * @return mixed
259
     */
260 13
    protected function getPreResolvedValue($value, $query)
261
    {
262 13
        if (is_array($value)) {
263 9
            if (array_key_exists($query->getName(), $value)) {
264 9
                return $value[$query->getName()];
265
            } else {
266
                throw new \Exception('Not found in resolve result', $query->getName());
267
            }
268 4
        } elseif (is_object($value)) {
269 4
            return $this->propertyAccessor->getValue($value, $query->getName());
270
        }
271
272
        return $value;
273
    }
274
275
    /**
276
     * @param $field        Field
277
     * @param $contextValue mixed
278
     * @param $query        Query
279
     *
280
     * @return mixed
281
     */
282 14
    protected function resolveValue($field, $contextValue, $query)
283
    {
284 14
        $resolvedValue = $field->getConfig()->resolve($contextValue, $this->parseArgumentsValues($field, $query));
285
286 14
        if(in_array($field->getType()->getKind(), [TypeMap::KIND_UNION, TypeMap::KIND_INTERFACE])){
287 5
            $resolvedType = $field->getType()->resolveType($resolvedValue);
288 5
            $field->setType($resolvedType);
289 5
        }
290
291 14
        return $resolvedValue;
292
    }
293
294
    /**
295
     * @param $field     Field
296
     * @param $query     Query
297
     *
298
     * @return array
299
     */
300 14
    public function parseArgumentsValues($field, $query)
301
    {
302 14
        if ($query instanceof \Youshido\GraphQL\Parser\Ast\Field) {
303
            return [];
304
        }
305
306 14
        $args      = [];
307 14
        foreach ($query->getArguments() as $argument) {
308 5
            $args[$argument->getName()] = $field->getConfig()->getArgument($argument->getName())->getType()->parseValue($argument->getValue()->getValue());
309 14
        }
310
311 14
        return $args;
312
    }
313
314
    /**
315
     * @param $query         Query
316
     * @param $queryType     ObjectType|TypeInterface|Field
317
     * @param $resolvedValue mixed
318
     * @param $value         array
319
     *
320
     * @throws \Exception
321
     *
322
     * @return array
323
     */
324 13
    protected function processQueryFields($query, $queryType, $resolvedValue, $value)
325
    {
326 13
        foreach ($query->getFields() as $field) {
327 13
            if ($field instanceof FragmentReference) {
328
                if (!$fragment = $this->request->getFragment($field->getName())) {
329
                    throw new \Exception(sprintf('Fragment reference "%s" not found', $field->getName()));
330
                }
331
332
                if ($fragment->getModel() !== $queryType->getName()) {
333
                    throw new \Exception(sprintf('Fragment reference "%s" not found on model "%s"', $field->getName(), $queryType->getName()));
334
                }
335
336
                foreach ($fragment->getFields() as $fragmentField) {
337
                    $value = $this->collectValue($value, $this->executeQuery($fragmentField, $queryType, $resolvedValue));
338
                }
339 13
            } else if($field->getName() == self::TYPE_NAME_QUERY) {
340 1
                $value = $this->collectValue($value, [$field->getAlias() ?: $field->getName() => $queryType->getName()]);
341 1
            } else {
342 13
                $value = $this->collectValue($value, $this->executeQuery($field, $queryType, $resolvedValue));
343
            }
344 13
        }
345
346 13
        return $value;
347
    }
348
349 13
    protected function collectValue($value, $queryValue)
350
    {
351 13
        if ($queryValue && is_array($queryValue)) {
352 13
            $value = array_merge($value, $queryValue);
353 13
        } else {
354
            $value = $queryValue;
355
        }
356
357 13
        return $value;
358
    }
359
360 15
    public function getSchema()
361
    {
362 15
        return $this->schema;
363
    }
364
365 15
    public function setSchema(Schema $schema)
366
    {
367 15
        $this->schema = $schema;
368
369 15
        $__schema = new SchemaType();
370 15
        $__schema->setSchema($schema);
371
372 15
        $__type = new TypeDefinitionType();
373 15
        $__type->setSchema($schema);
374
375 15
        $this->schema->addQuery('__schema', $__schema);
376 15
        $this->schema->addQuery('__type', $__type);
377 15
    }
378
379
    /**
380
     * @return ResolveValidatorInterface
381
     */
382
    public function getResolveValidator()
383
    {
384
        return $this->resolveValidator;
385
    }
386
387 15
    public function getResponseData()
388
    {
389 15
        $result = [];
390
391 15
        if (!empty($this->data)) {
392 14
            $result['data'] = $this->data;
393 14
        }
394
395 15
        if ($this->resolveValidator->hasErrors()) {
396 1
            $result['errors'] = $this->resolveValidator->getErrorsArray();
397 1
        }
398
399 15
        return $result;
400
    }
401
}
402