Completed
Pull Request — master (#31)
by Sebastian
06:26 queued 02:32
created

Processor::processFieldAST()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4.1054

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 27
ccs 13
cts 16
cp 0.8125
rs 8.5806
cc 4
eloc 17
nc 3
nop 3
crap 4.1054
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\Scalar\AbstractScalarType;
29
use Youshido\GraphQL\Type\TypeInterface;
30
use Youshido\GraphQL\Type\TypeMap;
31
use Youshido\GraphQL\Type\TypeService;
32
use Youshido\GraphQL\Validator\Exception\ResolveException;
33
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidator;
34
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidatorInterface;
35
use Youshido\GraphQL\Validator\SchemaValidator\SchemaValidator;
36
37
class Processor
38
{
39
40
    const TYPE_NAME_QUERY = '__typename';
41
42
    /** @var  array */
43
    protected $data;
44
45
    /** @var ResolveValidatorInterface */
46
    protected $resolveValidator;
47
48
    /** @var ExecutionContext */
49
    protected $executionContext;
50
51 23
    public function __construct(AbstractSchema $schema)
52
    {
53 23
        (new SchemaValidator())->validate($schema);
54
55 22
        $this->introduceIntrospectionFields($schema);
56 22
        $this->executionContext = new ExecutionContext();
57 22
        $this->executionContext->setSchema($schema);
58
59 22
        $this->resolveValidator = new ResolveValidator($this->executionContext);
60 22
    }
61
62
63 22
    public function processPayload($payload, $variables = [])
64
    {
65 22
        if ($this->executionContext->hasErrors()) {
66 4
            $this->executionContext->clearErrors();
67
        }
68
69 22
        $this->data = [];
70
71
        try {
72 22
            $this->parseAndCreateRequest($payload, $variables);
73
74 22
            $queryType    = $this->executionContext->getSchema()->getQueryType();
75 22
            $mutationType = $this->executionContext->getSchema()->getMutationType();
76 22
            foreach ($this->executionContext->getRequest()->getOperationsInOrder() as $operation) {
77 22
                if ($operationResult = $this->executeOperation($operation, $operation instanceof Mutation ? $mutationType : $queryType)) {
78 22
                    $this->data = array_merge($this->data, $operationResult);
79
                };
80
            }
81
82 3
        } catch (\Exception $e) {
83 3
            $this->executionContext->addError($e);
84
        }
85
86 22
        return $this;
87
    }
88
89 22
    protected function parseAndCreateRequest($payload, $variables = [])
90
    {
91 22
        if (empty($payload)) {
92 1
            throw new \Exception('Must provide an operation.');
93
        }
94 22
        $parser = new Parser();
95
96 22
        $data = $parser->parse($payload);
97 22
        $this->executionContext->setRequest(new Request($data, $variables));
98 22
    }
99
100
    /**
101
     * @param Query|Field        $query
102
     * @param AbstractObjectType $currentLevelSchema
103
     * @return array|bool|mixed
104
     */
105 22
    protected function executeOperation(Query $query, $currentLevelSchema)
106
    {
107 22
        if (!$this->resolveValidator->objectHasField($currentLevelSchema, $query)) {
108 1
            return null;
109
        }
110
111
        /** @var AbstractField $field */
112 22
        $operationField = $currentLevelSchema->getField($query->getName());
113 22
        $alias          = $query->getAlias() ?: $query->getName();
114
115 22
        if (!$this->resolveValidator->validateArguments($operationField, $query, $this->executionContext->getRequest())) {
116 3
            return null;
117
        }
118
119 20
        return [$alias => $this->processQueryAST($query, $operationField)];
120
    }
121
122
    /**
123
     * @param Query         $query
124
     * @param AbstractField $field
125
     * @param               $contextValue
126
     * @return array|mixed|null
127
     */
128 20
    protected function processQueryAST(Query $query, AbstractField $field, $contextValue = null)
129
    {
130 20
        if (!$this->resolveValidator->validateArguments($field, $query, $this->executionContext->getRequest())) {
131
            return null;
132
        }
133
134 20
        $resolvedValue = $this->resolveFieldValue($field, $contextValue, $query);
135
136 20
        if (!$this->resolveValidator->isValidValueForField($field, $resolvedValue)) {
137 2
            return null;
138
        }
139
140 20
        return $this->collectValueForQueryWithType($query, $field->getType(), $resolvedValue);
141
    }
142
143
    /**
144
     * @param Query|Mutation $query
145
     * @param AbstractType   $fieldType
146
     * @param mixed          $resolvedValue
147
     * @return array|mixed
148
     */
149 20
    protected function collectValueForQueryWithType(Query $query, AbstractType $fieldType, $resolvedValue)
150
    {
151 20
        $fieldType = $this->resolveValidator->resolveTypeIfAbstract($fieldType, $resolvedValue);
152 20
        if (is_null($resolvedValue)) return null;
153
154 18
        $value = [];
155
156 18
        if ($fieldType->getKind() == TypeMap::KIND_LIST) {
157 9
            if (!$this->resolveValidator->hasArrayAccess($resolvedValue)) return null;
158 9
            foreach ($resolvedValue as $resolvedValueItem) {
0 ignored issues
show
Bug introduced by
The expression $resolvedValue of type object|integer|double|string|array|boolean 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...
159 8
                $value[] = [];
160 8
                $index   = count($value) - 1;
161
162
163 8
                $namedType = $fieldType->getNamedType();
164 8
                $namedType = $this->resolveValidator->resolveTypeIfAbstract($namedType, $resolvedValueItem);
165 8
                if (!$namedType->isValidValue($resolvedValueItem)) {
166 1
                    $this->executionContext->addError(new ResolveException(sprintf('Not valid resolve value in %s field', $query->getName())));
167 1
                    $value[$index] = null;
168 1
                    continue;
169
                }
170
171 9
                $value[$index] = $this->processQueryFields($query, $namedType, $resolvedValueItem, $value[$index]);
172
            }
173
        } else {
174 18
            if (!$query->hasFields()) {
175 2
                return $this->getOutputValue($fieldType, $resolvedValue);
176
            }
177
178 18
            $value = $this->processQueryFields($query, $fieldType, $resolvedValue, $value);
179
        }
180
181 18
        return $value;
182
    }
183
184
    /**
185
     * @param FieldAst      $fieldAst
186
     * @param AbstractField $field
187
     *
188
     * @param mixed         $contextValue
189
     * @return array|mixed|null
190
     * @throws ResolveException
191
     * @throws \Exception
192
     */
193 17
    protected function processFieldAST(FieldAst $fieldAst, AbstractField $field, $contextValue)
194
    {
195 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...
196 17
        $fieldType        = $field->getType();
197 17
        $preResolvedValue = $this->getPreResolvedValue($contextValue, $fieldAst, $field);
198
199 17
        if ($fieldType->getKind() == TypeMap::KIND_LIST) {
200 1
            $listValue = [];
201 1
            foreach ($preResolvedValue as $resolvedValueItem) {
202 1
                $type = $fieldType->getNamedType();
203
204 1
                if (!$type->isValidValue($resolvedValueItem)) {
205
                    $this->executionContext->addError(new ResolveException(sprintf('Not valid resolve value in %s field', $field->getName())));
206
207
                    $listValue = null;
208
                    break;
209
                }
210 1
                $listValue[] = $this->getOutputValue($type, $resolvedValueItem);
211
            }
212
213 1
            $value = $listValue;
214
        } else {
215 17
            $value = $this->getFieldValidatedValue($field, $preResolvedValue);
216
        }
217
218 17
        return $value;
219
    }
220
221
    /**
222
     * @param AbstractField $field
223
     * @param mixed         $contextValue
224
     * @param Query         $query
225
     *
226
     * @return mixed
227
     */
228 20
    protected function resolveFieldValue(AbstractField $field, $contextValue, Query $query)
229
    {
230 20
        $resolveInfo = new ResolveInfo($field, $query->getFields(), $field->getType(), $this->executionContext);
231
232 20
        if ($resolveFunc = $field->getConfig()->getResolveFunction()) {
233 18
            return $resolveFunc($contextValue, $this->parseArgumentsValues($field, $query), $resolveInfo);
234 9
        } elseif ($propertyValue = TypeService::getPropertyValue($contextValue, $field->getName())) {
235 1
            return $propertyValue;
236
        } else {
237 8
            return $field->resolve($contextValue, $this->parseArgumentsValues($field, $query), $resolveInfo);
238
        }
239
    }
240
241
    /**
242
     * @param               $contextValue
243
     * @param FieldAst      $fieldAst
244
     * @param AbstractField $field
245
     *
246
     * @throws \Exception
247
     *
248
     * @return mixed
249
     */
250 17
    protected function getPreResolvedValue($contextValue, FieldAst $fieldAst, AbstractField $field)
251
    {
252 17
        $resolved      = false;
253 17
        $resolverValue = null;
254
255 17
        if (is_array($contextValue) && array_key_exists($fieldAst->getName(), $contextValue)) {
256 13
            $resolverValue = $contextValue[$fieldAst->getName()];
257 13
            $resolved      = true;
258 8
        } elseif (is_object($contextValue)) {
259 6
            $resolverValue = TypeService::getPropertyValue($contextValue, $fieldAst->getName());
260 6
            $resolved      = true;
261
        }
262
263 17
        if (!$resolved && $field->getType()->getNamedType()->getKind() == TypeMap::KIND_SCALAR) {
264 2
            $resolved = true;
265
        }
266
267 17
        if ($resolveFunction = $field->getConfig()->getResolveFunction()) {
268 2
            $resolveInfo = new ResolveInfo($field, [$fieldAst], $field->getType(), $this->executionContext);
269
270 2
            if (!$this->resolveValidator->validateArguments($field, $fieldAst, $this->executionContext->getRequest())) {
271
                throw new \Exception(sprintf('Not valid arguments for the field "%s"', $fieldAst->getName()));
272
273
            } else {
274 2
                $resolverValue = $resolveFunction($resolved ? $resolverValue : $contextValue, $fieldAst->getKeyValueArguments(), $resolveInfo);
275
            }
276
277
        }
278
279 17
        if (!$resolverValue && !$resolved) {
280 1
            throw new \Exception(sprintf('Property "%s" not found in resolve result', $fieldAst->getName()));
281
        }
282
283 17
        return $resolverValue;
284
    }
285
286
    /**
287
     * @param $field     AbstractField
288
     * @param $query     Query
289
     *
290
     * @return array
291
     */
292 20
    protected function parseArgumentsValues(AbstractField $field, Query $query)
293
    {
294 20
        $args = [];
295 20
        foreach ($query->getArguments() as $argument) {
296 8
            if ($configArgument = $field->getConfig()->getArgument($argument->getName())) {
297 8
                $args[$argument->getName()] = $configArgument->getType()->parseValue($argument->getValue()->getValue());
298
            }
299
        }
300
301 20
        return $args;
302
    }
303
304
    /**
305
     * @param $query         Query|FragmentInterface
306
     * @param $queryType     AbstractObjectType|TypeInterface|Field|AbstractType
307
     * @param $resolvedValue mixed
308
     * @param $value         array
309
     *
310
     * @throws \Exception
311
     *
312
     * @return array
313
     */
314 18
    protected function processQueryFields($query, AbstractType $queryType, $resolvedValue, $value)
315
    {
316 18
        if ($queryType instanceof AbstractScalarType && !$query->hasFields()) {
317 1
            return $this->getOutputValue($queryType, $resolvedValue);
318
        }
319
320 18
        foreach ($query->getFields() as $fieldAst) {
321 18
            $fieldResolvedValue = null;
322
323 18
            if ($fieldAst instanceof FragmentInterface) {
324
                /** @var TypedFragmentReference $fragment */
325 3
                $fragment = $fieldAst;
326 3
                if ($fieldAst instanceof FragmentReference) {
327
                    /** @var Fragment $fragment */
328 2
                    $fragment = $this->executionContext->getRequest()->getFragment($fieldAst->getName());
329 2
                    $this->resolveValidator->assertValidFragmentForField($fragment, $fieldAst, $queryType);
330 1
                } elseif ($fragment->getTypeName() !== $queryType->getName()) {
331 1
                    continue;
332
                }
333
334 3
                $fragmentValue      = $this->processQueryFields($fragment, $queryType, $resolvedValue, $value);
335 3
                $fieldResolvedValue = is_array($fragmentValue) ? $fragmentValue : [];
336
            } else {
337 18
                $alias       = $fieldAst->getAlias() ?: $fieldAst->getName();
338 18
                $currentType = $queryType->getNullableType();
339
340 18
                if ($fieldAst->getName() == self::TYPE_NAME_QUERY) {
341 1
                    $fieldResolvedValue = [$alias => $queryType->getName()];
342
                } else {
343 18
                    if (!$this->resolveValidator->objectHasField($currentType, $fieldAst)) {
344 2
                        $fieldResolvedValue = null;
345
                    } else {
346 18
                        if ($fieldAst instanceof Query) {
347 10
                            $queryAst           = $currentType->getField($fieldAst->getName());
348 10
                            $fieldValue         = $queryAst ? $this->processQueryAST($fieldAst, $queryAst, $resolvedValue) : null;
349 10
                            $fieldResolvedValue = [$alias => $fieldValue];
350
                        } elseif ($fieldAst instanceof FieldAst) {
351
                            $fieldResolvedValue = [
352 17
                                $alias => $this->processFieldAST($fieldAst, $currentType->getField($fieldAst->getName()), $resolvedValue)
353
                            ];
354
                        }
355
                    }
356
357
358
                }
359
            }
360
361 18
            $value = $this->collectValue($value, $fieldResolvedValue);
362
        }
363
364 18
        return $value;
365
    }
366
367 17
    protected function getFieldValidatedValue(AbstractField $field, $value)
368
    {
369 17
        return ($this->resolveValidator->isValidValueForField($field, $value)) ? $this->getOutputValue($field->getType(), $value) : null;
370
    }
371
372 17
    protected function getOutputValue(AbstractType $type, $value)
373
    {
374 17
        return in_array($type->getKind(), [TypeMap::KIND_OBJECT, TypeMap::KIND_NON_NULL]) ? $value : $type->serialize($value);
375
    }
376
377 18
    protected function collectValue($value, $queryValue)
378
    {
379 18
        if ($queryValue && is_array($queryValue)) {
380 18
            $value = array_merge(is_array($value) ? $value : [], $queryValue);
381
        } else {
382 2
            $value = $queryValue;
383
        }
384
385 18
        return $value;
386
    }
387
388 22
    protected function introduceIntrospectionFields(AbstractSchema $schema)
389
    {
390 22
        $schemaField = new SchemaField();
391 22
        $schemaField->setSchema($schema);
392
393 22
        $schema->addQueryField($schemaField);
394 22
        $schema->addQueryField(new TypeDefinitionField());
395 22
    }
396
397 22
    public function getResponseData()
398
    {
399 22
        $result = [];
400
401 22
        if (!empty($this->data)) {
402 20
            $result['data'] = $this->data;
403
        }
404
405 22
        if ($this->executionContext->hasErrors()) {
406 6
            $result['errors'] = $this->executionContext->getErrorsArray();
407
        }
408
409 22
        return $result;
410
    }
411
}
412