Completed
Pull Request — master (#19)
by Daniel
03:45
created

Processor::parseArgumentsValues()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
rs 9.4285
ccs 8
cts 8
cp 1
cc 3
eloc 6
nc 3
nop 2
crap 3
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\Execution;
11
12
use Youshido\GraphQL\Execution\Context\ExecutionContext;
13
use Youshido\GraphQL\Field\AbstractField;
14
use Youshido\GraphQL\Field\Field;
15
use Youshido\GraphQL\Introspection\Field\SchemaField;
16
use Youshido\GraphQL\Introspection\Field\TypeDefinitionField;
17
use Youshido\GraphQL\Parser\Ast\Field as FieldAst;
18
use Youshido\GraphQL\Parser\Ast\Fragment;
19
use Youshido\GraphQL\Parser\Ast\FragmentInterface;
20
use Youshido\GraphQL\Parser\Ast\FragmentReference;
21
use Youshido\GraphQL\Parser\Ast\Mutation;
22
use Youshido\GraphQL\Parser\Ast\Query;
23
use Youshido\GraphQL\Parser\Ast\TypedFragmentReference;
24
use Youshido\GraphQL\Parser\Parser;
25
use Youshido\GraphQL\Schema\AbstractSchema;
26
use Youshido\GraphQL\Type\AbstractType;
27
use Youshido\GraphQL\Type\Object\AbstractObjectType;
28
use Youshido\GraphQL\Type\TypeInterface;
29
use Youshido\GraphQL\Type\TypeMap;
30
use Youshido\GraphQL\Type\TypeService;
31
use Youshido\GraphQL\Validator\Exception\ResolveException;
32
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidator;
33
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidatorInterface;
34
use Youshido\GraphQL\Validator\SchemaValidator\SchemaValidator;
35
36
class Processor
37
{
38
39
    const TYPE_NAME_QUERY = '__typename';
40
41
    /** @var  array */
42
    protected $data;
43
44
    /** @var ResolveValidatorInterface */
45
    protected $resolveValidator;
46
47
    /** @var ExecutionContext */
48
    protected $executionContext;
49
50
    /** @var ResolveStringResolverInterface */
51
    protected $resolverStringResolver;
52
53 24
    public function __construct(AbstractSchema $schema, ResolveStringResolverInterface $resolveStringResolver = null)
54
    {
55 24
        (new SchemaValidator())->validate($schema);
56
57 23
        $this->introduceIntrospectionFields($schema);
58 23
        $this->executionContext = new ExecutionContext();
59 23
        $this->executionContext->setSchema($schema);
60
61 23
        $this->resolveValidator = new ResolveValidator($this->executionContext);
62 23
        $this->resolverStringResolver = $resolveStringResolver;
63 23
    }
64
65
66 23
    public function processPayload($payload, $variables = [])
67
    {
68 23
        if ($this->executionContext->hasErrors()) {
69 4
            $this->executionContext->clearErrors();
70 4
        }
71
72 23
        $this->data = [];
73
74
        try {
75 23
            $this->parseAndCreateRequest($payload, $variables);
76
77 23
            $queryType    = $this->executionContext->getSchema()->getQueryType();
78 23
            $mutationType = $this->executionContext->getSchema()->getMutationType();
79 23
            foreach ($this->executionContext->getRequest()->getOperationsInOrder() as $operation) {
80 23
                if ($operationResult = $this->executeOperation($operation, $operation instanceof Mutation ? $mutationType : $queryType)) {
81 21
                    $this->data = array_merge($this->data, $operationResult);
82 21
                };
83 23
            }
84
85 23
        } catch (\Exception $e) {
86 3
            $this->executionContext->addError($e);
87
        }
88
89 23
        return $this;
90
    }
91
92 23
    protected function parseAndCreateRequest($payload, $variables = [])
93
    {
94 23
        if (empty($payload)) {
95 1
            throw new \Exception('Must provide an operation.');
96
        }
97 23
        $parser = new Parser();
98
99 23
        $data = $parser->parse($payload);
100 23
        $this->executionContext->setRequest(new Request($data, $variables));
101 23
    }
102
103
    /**
104
     * @param Query|Field        $query
105
     * @param AbstractObjectType $currentLevelSchema
106
     * @return array|bool|mixed
107
     */
108 23
    protected function executeOperation(Query $query, $currentLevelSchema)
109
    {
110 23
        if (!$this->resolveValidator->objectHasField($currentLevelSchema, $query)) {
111 1
            return null;
112
        }
113
114
        /** @var AbstractField $field */
115 23
        $operationField = $currentLevelSchema->getField($query->getName());
116 23
        $alias          = $query->getAlias() ?: $query->getName();
117
118 23
        if (!$this->resolveValidator->validateArguments($operationField, $query, $this->executionContext->getRequest())) {
119 3
            return null;
120
        }
121
122 21
        return [$alias => $this->processQueryAST($query, $operationField)];
123
    }
124
125
    /**
126
     * @param Query         $query
127
     * @param AbstractField $field
128
     * @param               $contextValue
129
     * @return array|mixed|null
130
     */
131 21
    protected function processQueryAST(Query $query, AbstractField $field, $contextValue = null)
132
    {
133 21
        if (!$this->resolveValidator->validateArguments($field, $query, $this->executionContext->getRequest())) {
134
            return null;
135
        }
136
137 21
        $resolvedValue = $this->resolveFieldValue($field, $contextValue, $query);
138
139 21
        if (!$this->resolveValidator->isValidValueForField($field, $resolvedValue)) {
140 2
            return null;
141
        }
142
143 21
        return $this->collectValueForQueryWithType($query, $field->getType(), $resolvedValue);
144
    }
145
146
    /**
147
     * @param Query|Mutation $query
148
     * @param AbstractType   $fieldType
149
     * @param mixed          $resolvedValue
150
     * @return array|mixed
151
     */
152 21
    protected function collectValueForQueryWithType(Query $query, AbstractType $fieldType, $resolvedValue)
153
    {
154 21
        $fieldType = $this->resolveValidator->resolveTypeIfAbstract($fieldType, $resolvedValue);
155 21
        if (is_null($resolvedValue)) return null;
156
157 19
        $value = [];
158 19
        if ($fieldType->getKind() == TypeMap::KIND_LIST) {
159 8
            foreach ((array)$resolvedValue as $resolvedValueItem) {
160 7
                $value[] = [];
161 7
                $index   = count($value) - 1;
162
163
164 7
                $namedType = $fieldType->getNamedType();
165 7
                $namedType = $this->resolveValidator->resolveTypeIfAbstract($namedType, $resolvedValueItem);
166 7
                if (!$namedType->isValidValue($resolvedValueItem)) {
167 1
                    $this->executionContext->addError(new ResolveException(sprintf('Not valid resolve value in %s field', $query->getName())));
168 1
                    $value[$index] = null;
169 1
                    continue;
170
                }
171
172 6
                $value[$index] = $this->processQueryFields($query, $namedType, $resolvedValueItem, $value[$index]);
173 8
            }
174 8
        } else {
175 19
            if (!$query->hasFields()) {
176 3
                return $this->getOutputValue($fieldType, $resolvedValue);
177
            }
178
179 18
            $value = $this->processQueryFields($query, $fieldType, $resolvedValue, $value);
180
        }
181
182 18
        return $value;
183
    }
184
185
    /**
186
     * @param FieldAst      $fieldAst
187
     * @param AbstractField $field
188
     *
189
     * @param mixed         $contextValue
190
     * @return array|mixed|null
191
     * @throws ResolveException
192
     * @throws \Exception
193
     */
194 17
    protected function processFieldAST(FieldAst $fieldAst, AbstractField $field, $contextValue)
195
    {
196 17
        $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...
197 17
        $fieldType        = $field->getType();
198 17
        $preResolvedValue = $this->getPreResolvedValue($contextValue, $fieldAst, $field);
199
200 17
        if ($fieldType->getKind() == TypeMap::KIND_LIST) {
201 1
            $listValue = [];
202 1
            foreach ($preResolvedValue as $resolvedValueItem) {
203 1
                $type = $fieldType->getNamedType();
204
205 1
                if (!$type->isValidValue($resolvedValueItem)) {
206
                    $this->executionContext->addError(new ResolveException(sprintf('Not valid resolve value in %s field', $field->getName())));
207
208
                    $listValue = null;
209
                    break;
210
                }
211 1
                $listValue[] = $this->getOutputValue($type, $resolvedValueItem);
212 1
            }
213
214 1
            $value = $listValue;
215 1
        } else {
216 17
            $value = $this->getFieldValidatedValue($field, $preResolvedValue);
217
        }
218
219 17
        return $value;
220
    }
221
222
    /**
223
     * @param AbstractField $field
224
     * @param mixed         $contextValue
225
     * @param Query         $query
226
     *
227
     * @return mixed
228
     */
229 21
    protected function resolveFieldValue(AbstractField $field, $contextValue, Query $query)
230
    {
231 21
        $resolveInfo = new ResolveInfo($field, $query->getFields(), $field->getType(), $this->executionContext);
232
233 21
        if (($resolveFunc = $field->getConfig()->getResolveFunction()) || (
234 10
            $this->resolverStringResolver && ($resolveString = $field->getConfig()->getResolveString()) && ($resolveFunc = $this->resolverStringResolver->resolve($resolveString))
0 ignored issues
show
Bug introduced by
The variable $resolveString does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
235 21
            )) {
236 19
            return $resolveFunc($contextValue, $this->parseArgumentsValues($field, $query), $resolveInfo);
237
        }
238 9
        elseif ($propertyValue = TypeService::getPropertyValue($contextValue, $field->getName())) {
239 1
            return $propertyValue;
240
        } else {
241 8
            return $field->resolve($contextValue, $this->parseArgumentsValues($field, $query), $resolveInfo);
242
        }
243
    }
244
245
    /**
246
     * @param               $contextValue
247
     * @param FieldAst      $fieldAst
248
     * @param AbstractField $field
249
     *
250
     * @throws \Exception
251
     *
252
     * @return mixed
253
     */
254 17
    protected function getPreResolvedValue($contextValue, FieldAst $fieldAst, AbstractField $field)
255
    {
256 17
        $resolved      = false;
257 17
        $resolverValue = null;
258
259 17
        if (is_array($contextValue) && array_key_exists($fieldAst->getName(), $contextValue)) {
260 13
            $resolverValue = $contextValue[$fieldAst->getName()];
261 13
            $resolved      = true;
262 17
        } elseif (is_object($contextValue)) {
263 6
            $resolverValue = TypeService::getPropertyValue($contextValue, $fieldAst->getName());
264 6
            $resolved      = true;
265 6
        }
266
267 17
        if (!$resolved && $field->getType()->getNamedType()->getKind() == TypeMap::KIND_SCALAR) {
268 2
            $resolved = true;
269 2
        }
270
271 17
        if (($resolveFunction = $field->getConfig()->getResolveFunction()) || (
272 17
                $this->resolverStringResolver && ($resolveString = $field->getConfig()->getResolveString()) && ($resolveFunction = $this->resolverStringResolver->resolve($resolveString))
0 ignored issues
show
Bug introduced by
The variable $resolveString does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
273 17
            )) {
274 2
            $resolveInfo = new ResolveInfo($field, [$fieldAst], $field->getType(), $this->executionContext);
275
276 2
            if (!$this->resolveValidator->validateArguments($field, $fieldAst, $this->executionContext->getRequest())) {
277
                throw new \Exception(sprintf('Not valid arguments for the field "%s"', $fieldAst->getName()));
278
279
            } else {
280 2
                $resolverValue = $resolveFunction($resolved ? $resolverValue : $contextValue, $fieldAst->getKeyValueArguments(), $resolveInfo);
281
            }
282
283 2
        }
284
285 17
        if (!$resolverValue && !$resolved) {
286 1
            throw new \Exception(sprintf('Property "%s" not found in resolve result', $fieldAst->getName()));
287
        }
288
289 17
        return $resolverValue;
290
    }
291
292
    /**
293
     * @param $field     AbstractField
294
     * @param $query     Query
295
     *
296
     * @return array
297
     */
298 21
    protected function parseArgumentsValues(AbstractField $field, Query $query)
299
    {
300 21
        $args = [];
301 21
        foreach ($query->getArguments() as $argument) {
302 8
            if ($configArgument = $field->getConfig()->getArgument($argument->getName())) {
303 8
                $args[$argument->getName()] = $configArgument->getType()->parseValue($argument->getValue()->getValue());
304 8
            }
305 21
        }
306
307 21
        return $args;
308
    }
309
310
    /**
311
     * @param $query         Query|FragmentInterface
312
     * @param $queryType     AbstractObjectType|TypeInterface|Field|AbstractType
313
     * @param $resolvedValue mixed
314
     * @param $value         array
315
     *
316
     * @throws \Exception
317
     *
318
     * @return array
319
     */
320 18
    protected function processQueryFields($query, AbstractType $queryType, $resolvedValue, $value)
321
    {
322 18
        foreach ($query->getFields() as $fieldAst) {
323 18
            $fieldResolvedValue = null;
324
325 18
            if ($fieldAst instanceof FragmentInterface) {
326
                /** @var TypedFragmentReference $fragment */
327 3
                $fragment = $fieldAst;
328 3
                if ($fieldAst instanceof FragmentReference) {
329
                    /** @var Fragment $fragment */
330 2
                    $fragment = $this->executionContext->getRequest()->getFragment($fieldAst->getName());
331 2
                    $this->resolveValidator->assertValidFragmentForField($fragment, $fieldAst, $queryType);
332 3
                } elseif ($fragment->getTypeName() !== $queryType->getName()) {
333 1
                    continue;
334
                }
335
336 3
                $fragmentValue      = $this->processQueryFields($fragment, $queryType, $resolvedValue, $value);
337 3
                $fieldResolvedValue = is_array($fragmentValue) ? $fragmentValue : [];
338 3
            } else {
339 18
                $alias       = $fieldAst->getAlias() ?: $fieldAst->getName();
340 18
                $currentType = $queryType->getNullableType();
341
342 18
                if ($fieldAst->getName() == self::TYPE_NAME_QUERY) {
343 1
                    $fieldResolvedValue = [$alias => $queryType->getName()];
344 1
                } else {
345 18
                    if (!$this->resolveValidator->objectHasField($currentType, $fieldAst)) {
346 2
                        $fieldResolvedValue = null;
347 2
                    } else {
348 18
                        if ($fieldAst instanceof Query) {
349 10
                            $queryAst           = $currentType->getField($fieldAst->getName());
350 10
                            $fieldValue         = $queryAst ? $this->processQueryAST($fieldAst, $queryAst, $resolvedValue) : null;
351 10
                            $fieldResolvedValue = [$alias => $fieldValue];
352 18
                        } elseif ($fieldAst instanceof FieldAst) {
353
                            $fieldResolvedValue = [
354 17
                                $alias => $this->processFieldAST($fieldAst, $currentType->getField($fieldAst->getName()), $resolvedValue)
355 17
                            ];
356 17
                        }
357
                    }
358
359
360
                }
361
            }
362
363 18
            $value = $this->collectValue($value, $fieldResolvedValue);
364 18
        }
365
366 18
        return $value;
367
    }
368
369 17
    protected function getFieldValidatedValue(AbstractField $field, $value)
370
    {
371 17
        return ($this->resolveValidator->isValidValueForField($field, $value)) ? $this->getOutputValue($field->getType(), $value) : null;
372
    }
373
374 18
    protected function getOutputValue(AbstractType $type, $value)
375
    {
376 18
        return in_array($type->getKind(), [TypeMap::KIND_OBJECT, TypeMap::KIND_NON_NULL]) ? $value : $type->serialize($value);
377
    }
378
379 18
    protected function collectValue($value, $queryValue)
380
    {
381 18
        if ($queryValue && is_array($queryValue)) {
382 18
            $value = array_merge(is_array($value) ? $value : [], $queryValue);
383 18
        } else {
384 2
            $value = $queryValue;
385
        }
386
387 18
        return $value;
388
    }
389
390 23
    protected function introduceIntrospectionFields(AbstractSchema $schema)
391
    {
392 23
        $schemaField = new SchemaField();
393 23
        $schemaField->setSchema($schema);
394
395 23
        $schema->addQueryField($schemaField);
396 23
        $schema->addQueryField(new TypeDefinitionField());
397 23
    }
398
399 23
    public function getResponseData()
400
    {
401 23
        $result = [];
402
403 23
        if (!empty($this->data)) {
404 21
            $result['data'] = $this->data;
405 21
        }
406
407 23
        if ($this->executionContext->hasErrors()) {
408 6
            $result['errors'] = $this->executionContext->getErrorsArray();
409 6
        }
410
411 23
        return $result;
412
    }
413
}
414