Completed
Push — master ( 24a695...b4f57b )
by Portey
06:28
created

Processor::executeMutation()   C

Complexity

Conditions 8
Paths 10

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 6
Bugs 2 Features 1
Metric Value
c 6
b 2
f 1
dl 0
loc 36
ccs 0
cts 20
cp 0
rs 5.3846
cc 8
eloc 18
nc 10
nop 2
crap 72
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
        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
            if ($outputType && in_array($outputType->getKind(), [TypeMap::KIND_INTERFACE, TypeMap::KIND_UNION])) {
122
                $outputType = $outputType->getConfig()->resolveType($resolvedValue);
123
            }
124
125
            $value = $this->processQueryFields($mutation, $outputType, $resolvedValue, []);
126
        }
127
128
        return [$alias => $value];
129
    }
130
131 18
    protected function parseAndCreateRequest($query, $variables = [])
132
    {
133 18
        $parser = new Parser();
134
135 18
        $parser->setSource($query);
136 18
        $data = $parser->parse();
137
138 18
        $this->request = new Request($data);
139 18
        $this->request->setVariables($variables);
140 18
    }
141
142
    /**
143
     * @param Query|Field $query
144
     * @param ObjectType  $currentLevelSchema
145
     * @param null        $contextValue
146
     * @return array|bool|mixed
147
     */
148 18
    protected function executeQuery($query, $currentLevelSchema, $contextValue = null)
149
    {
150 18
        if (!$this->checkFieldExist($currentLevelSchema, $query)) {
151
            return null;
152
        }
153
154
        /** @var Field $field */
155 18
        $field = $currentLevelSchema->getConfig()->getField($query->getName());
156 18
        if (get_class($query) == 'Youshido\GraphQL\Parser\Ast\Field') {
157 16
            $alias            = $query->getName();
158 16
            $preResolvedValue = $this->getPreResolvedValue($contextValue, $query);
159
160 16
            if ($field->getConfig()->getType()->getKind() == TypeMap::KIND_LIST) {
161 1
                if(!is_array($preResolvedValue)){
162
                    $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...
163
                    $this->resolveValidator->addError(new ResolveException('Not valid resolve value for list type'));
164
                }
165
166
167 1
                $listValue = [];
168 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...
169
                    /** @var TypeInterface $type */
170 1
                    $type = $field->getType()->getConfig()->getItem();
171
172 1
                    if ($type->getKind() == TypeMap::KIND_ENUM) {
173
                        /** @var $type AbstractEnumType */
174 1
                        if(!$type->isValidValue($resolvedValueItem)) {
175
                            $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
176
177
                            $listValue = null;
178
                            break;
179
                        }
180
181 1
                        $listValue[] = $type->resolve($resolvedValueItem);
182 1
                    } else {
183
                        /** @var AbstractScalarType $type */
184
                        $listValue[] = $type->serialize($preResolvedValue);
185
                    }
186 1
                }
187
188 1
                $value = $listValue;
189 1
            } else {
190 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...
191
                    if(!$field->getType()->isValidValue($preResolvedValue)) {
192
                        $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
193
                        $value = null;
194
                    } else {
195
                        $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\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\Object\AbstractObjectType, Youshido\GraphQL\Type\Object\AbstractUnionType, 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, Youshido\Tests\Type\Union\Schema\TestUnionType.

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...
196
                    }
197
                } else {
198 16
                    $value = $field->getType()->serialize($preResolvedValue);
199
                }
200
            }
201 16
        } else {
202 18
            if (!$this->resolveValidator->validateArguments($field, $query, $this->request)) {
203 1
                return null;
204
            }
205
206 17
            $resolvedValue = $this->resolveValue($field, $contextValue, $query);
207 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...
208
209 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...
210
                $this->resolveValidator->addError(new ResolveException(sprintf('Not valid resolved value for query "%s"', $field->getType()->getName())));
211
212
                return [$alias => null];
213
            }
214
215 17
            $value = [];
216 17
            if ($resolvedValue) {
217 16
                if ($field->getType()->getKind() == TypeMap::KIND_LIST) {
218 7
                    foreach ($resolvedValue as $resolvedValueItem) {
219 7
                        $value[] = [];
220 7
                        $index   = count($value) - 1;
221
222 7
                        if(in_array($field->getConfig()->getType()->getConfig()->getItem()->getKind(), [TypeMap::KIND_UNION, TypeMap::KIND_INTERFACE]) ) {
223 4
                            $type = $field->getConfig()->getType()->getConfig()->getItemConfig()->resolveType($resolvedValueItem);
224 4
                        } else {
225 3
                            $type = $field->getType();
226
                        }
227
228 7
                        $value[$index] = $this->processQueryFields($query, $type, $resolvedValueItem, $value[$index]);
229 7
                    }
230 7
                } else {
231 14
                    $value = $this->processQueryFields($query, $field->getType(), $resolvedValue, $value);
232
                }
233 16
            } else {
234 4
                $value = $resolvedValue;
235
            }
236
        }
237
238 17
        return [$alias => $value];
239
    }
240
241
    /**
242
     * @param $objectType InputObjectType|ObjectType
243
     * @param $query      Mutation|Query
244
     * @return null
245
     */
246 18
    private function checkFieldExist($objectType, $query)
247
    {
248 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...
249
            if ($objectType->getKind() == TypeMap::KIND_LIST) {
250
                $name = $objectType->getConfig()->getItem()->getName();
251
            } else {
252
                $name = $objectType->getName();
253
            }
254
255
            $this->resolveValidator->addError(new ResolveException(sprintf('Field "%s" not found in type "%s"', $query->getName(), $name)));
256
257
            return false;
258
        }
259
260 18
        return true;
261
    }
262
263
    /**
264
     * @param $value
265
     * @param $query Field
266
     *
267
     * @throws \Exception
268
     *
269
     * @return mixed
270
     */
271 16
    protected function getPreResolvedValue($value, $query)
272
    {
273 16
        if (is_array($value)) {
274 11
            if (array_key_exists($query->getName(), $value)) {
275 11
                return $value[$query->getName()];
276
            } else {
277
                throw new \Exception('Not found in resolve result', $query->getName());
278
            }
279 5
        } elseif (is_object($value)) {
280 5
            return $this->propertyAccessor->getValue($value, $query->getName());
281
        }
282
283
        return $value;
284
    }
285
286
    /**
287
     * @param $field        Field
288
     * @param $contextValue mixed
289
     * @param $query        Query
290
     *
291
     * @return mixed
292
     */
293 17
    protected function resolveValue($field, $contextValue, $query)
294
    {
295 17
        $resolvedValue = $field->getConfig()->resolve($contextValue, $this->parseArgumentsValues($field, $query));
296
297 17
        if(in_array($field->getType()->getKind(), [TypeMap::KIND_UNION, TypeMap::KIND_INTERFACE])){
298 6
            $resolvedType = $field->getType()->resolveType($resolvedValue);
299 6
            $field->setType($resolvedType);
300 6
        }
301
302 17
        return $resolvedValue;
303
    }
304
305
    /**
306
     * @param $field     Field
307
     * @param $query     Query
308
     *
309
     * @return array
310
     */
311 17
    public function parseArgumentsValues($field, $query)
312
    {
313 17
        if ($query instanceof \Youshido\GraphQL\Parser\Ast\Field) {
314
            return [];
315
        }
316
317 17
        $args      = [];
318 17
        foreach ($query->getArguments() as $argument) {
319 5
            $args[$argument->getName()] = $field->getConfig()->getArgument($argument->getName())->getType()->parseValue($argument->getValue()->getValue());
320 17
        }
321
322 17
        return $args;
323
    }
324
325
    /**
326
     * @param $query         Query
327
     * @param $queryType     ObjectType|TypeInterface|Field
328
     * @param $resolvedValue mixed
329
     * @param $value         array
330
     *
331
     * @throws \Exception
332
     *
333
     * @return array
334
     */
335 16
    protected function processQueryFields($query, $queryType, $resolvedValue, $value)
336
    {
337 16
        foreach ($query->getFields() as $field) {
338 16
            if ($field instanceof FragmentReference) {
339 1
                if (!$fragment = $this->request->getFragment($field->getName())) {
340
                    throw new \Exception(sprintf('Fragment reference "%s" not found', $field->getName()));
341
                }
342
343 1
                if ($fragment->getModel() !== $queryType->getName()) {
344
                    throw new \Exception(sprintf('Fragment reference "%s" not found on model "%s"', $field->getName(), $queryType->getName()));
345
                }
346
347 1
                foreach ($fragment->getFields() as $fragmentField) {
348 1
                    $value = $this->collectValue($value, $this->executeQuery($fragmentField, $queryType, $resolvedValue));
349 1
                }
350 16
            } elseif ($field instanceof TypedFragmentReference) {
351 2
                if ($field->getTypeName() !== $queryType->getName()) {
352 1
                    continue;
353
                }
354
355 2
                foreach ($field->getFields() as $fragmentField) {
356 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...
357 2
                }
358 16
            } elseif ($field->getName() == self::TYPE_NAME_QUERY) {
359 1
                $value = $this->collectValue($value, [$field->getAlias() ?: $field->getName() => $queryType->getName()]);
360 1
            } else {
361 16
                $value = $this->collectValue($value, $this->executeQuery($field, $queryType, $resolvedValue));
362
            }
363 16
        }
364
365 16
        return $value;
366
    }
367
368 16
    protected function collectValue($value, $queryValue)
369
    {
370 16
        if ($queryValue && is_array($queryValue)) {
371 16
            $value = array_merge(is_array($value) ? $value : [], $queryValue);
372 16
        } else {
373
            $value = $queryValue;
374
        }
375
376 16
        return $value;
377
    }
378
379 18
    public function getSchema()
380
    {
381 18
        return $this->schema;
382
    }
383
384 18
    public function setSchema(Schema $schema)
385
    {
386 18
        $this->schema = $schema;
387
388 18
        $__schema = new SchemaType();
389 18
        $__schema->setSchema($schema);
390
391 18
        $__type = new TypeDefinitionType();
392
393 18
        $this->schema->addQuery('__schema', $__schema);
394 18
        $this->schema->addQuery('__type', $__type);
395 18
    }
396
397
    /**
398
     * @return ResolveValidatorInterface
399
     */
400
    public function getResolveValidator()
401
    {
402
        return $this->resolveValidator;
403
    }
404
405 18
    public function getResponseData()
406
    {
407 18
        $result = [];
408
409 18
        if (!empty($this->data)) {
410 17
            $result['data'] = $this->data;
411 17
        }
412
413 18
        if ($this->resolveValidator->hasErrors()) {
414 1
            $result['errors'] = $this->resolveValidator->getErrorsArray();
415 1
        }
416
417 18
        return $result;
418
    }
419
}
420