Completed
Push — master ( ea66d0...a94821 )
by Alexandr
03:49
created

Processor::processFieldAST()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4.074

Importance

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