Completed
Push — master ( 75360a...120e4f )
by Portey
05:16
created

Processor   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 373
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 20

Test Coverage

Coverage 69.68%

Importance

Changes 30
Bugs 3 Features 7
Metric Value
wmc 61
c 30
b 3
f 7
lcom 1
cbo 20
dl 0
loc 373
ccs 108
cts 155
cp 0.6968
rs 2.1569

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A parseAndCreateRequest() 0 10 1
A checkFieldExist() 0 10 2
A getPreResolvedValue() 0 14 4
A getSchema() 0 4 1
A setSchema() 0 13 1
A getResolveValidator() 0 4 1
C processQuery() 0 29 8
B executeMutation() 0 32 6
D executeQuery() 0 94 17
A resolveValue() 0 10 2
A parseArgumentsValues() 0 13 3
C processQueryFields() 0 24 8
A collectValue() 0 10 3
A getResponseData() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like Processor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Processor, and based on these observations, apply Extract Interface, too.

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 13
    public function __construct(ResolveValidatorInterface $validator)
51
    {
52 13
        $this->resolveValidator = $validator;
53
54 13
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
55 13
    }
56
57 13
    public function processQuery($queryString, $variables = [])
58
    {
59 13
        $this->resolveValidator->clearErrors();
60 13
        $this->data = [];
61
62
        try {
63 13
            $this->parseAndCreateRequest($queryString, $variables);
64
65 13
            if ($this->request->hasQueries()) {
66 13
                foreach ($this->request->getQueries() as $query) {
67 13
                    if ($queryResult = $this->executeQuery($query, $this->getSchema()->getQueryType())) {
68 13
                        $this->data = array_merge($this->data, $queryResult);
69
                    };
70
                }
71
            }
72
73 13
            if ($this->request->hasMutations()) {
74
                foreach ($this->request->getMutations() as $mutation) {
75
                    if ($mutationResult = $this->executeMutation($mutation, $this->getSchema()->getMutationType())) {
76 13
                        $this->data = array_merge($this->data, $mutationResult);
77
                    }
78
                }
79
            }
80
        } catch (\Exception $e) {
81
            $this->resolveValidator->clearErrors();
82
83
            $this->resolveValidator->addError($e);
84
        }
85 13
    }
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 13
    protected function parseAndCreateRequest($query, $variables = [])
127
    {
128 13
        $parser = new Parser();
129
130 13
        $parser->setSource($query);
131 13
        $data = $parser->parse();
132
133 13
        $this->request = new Request($data);
134 13
        $this->request->setVariables($variables);
135 13
    }
136
137
    /**
138
     * @param Query|Field $query
139
     * @param ObjectType  $currentLevelSchema
140
     * @param null        $contextValue
141
     * @return array|bool|mixed
142
     */
143 13
    protected function executeQuery($query, $currentLevelSchema, $contextValue = null)
144
    {
145 13
        if (!$this->checkFieldExist($currentLevelSchema, $query)) {
146
147
            return null;
148
        }
149
150
        /** @var Field $field */
151 13
        $field = $currentLevelSchema->getConfig()->getField($query->getName());
152 13
        if (get_class($query) == 'Youshido\GraphQL\Parser\Ast\Field') {
153 11
            $alias            = $query->getName();
154 11
            $preResolvedValue = $this->getPreResolvedValue($contextValue, $query);
155
156 11
            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
                    } else {
179
                        /** @var AbstractScalarType $type */
180 1
                        $listValue[] = $type->serialize($preResolvedValue);
181
                    }
182
                }
183
184 1
                $value = $listValue;
185
            } else {
186 11
                if ($field->getType()->getKind() == TypeMap::KIND_ENUM) {
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\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\Schema\DroidType, Youshido\Tests\Schema\EpisodeEnum, Youshido\Tests\Schema\HumanType, Youshido\Tests\Schema\QueryType.

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