Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
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 |
||
39 | class Processor |
||
40 | { |
||
41 | |||
42 | const TYPE_NAME_QUERY = '__typename'; |
||
43 | |||
44 | /** @var array */ |
||
45 | protected $data; |
||
46 | |||
47 | /** @var ResolveValidatorInterface */ |
||
48 | protected $resolveValidator; |
||
49 | |||
50 | /** @var ExecutionContext */ |
||
51 | protected $executionContext; |
||
52 | |||
53 | /** @var int */ |
||
54 | protected $maxComplexity; |
||
55 | |||
56 | 31 | public function __construct(AbstractSchema $schema) |
|
57 | { |
||
58 | /** |
||
59 | * This will be removed in 1.4 when __construct signature is changed to accept ExecutionContext |
||
60 | */ |
||
61 | 31 | if (empty($this->executionContext)) { |
|
62 | 31 | $this->executionContext = new ExecutionContext($schema); |
|
63 | 31 | $this->executionContext->setContainer(new Container()); |
|
64 | } |
||
65 | 31 | $this->resolveValidator = new ResolveValidator($this->executionContext); |
|
66 | 31 | } |
|
67 | |||
68 | 29 | public function processPayload($payload, $variables = [], $reducers = []) |
|
69 | { |
||
70 | 29 | $this->data = []; |
|
71 | |||
72 | try { |
||
73 | 29 | $this->parseAndCreateRequest($payload, $variables); |
|
74 | |||
75 | 29 | $queryType = $this->executionContext->getSchema()->getQueryType(); |
|
76 | 29 | $mutationType = $this->executionContext->getSchema()->getMutationType(); |
|
77 | |||
78 | 29 | if ($this->maxComplexity) { |
|
79 | 1 | $reducers[] = new MaxComplexityQueryVisitor($this->maxComplexity); |
|
80 | } |
||
81 | |||
82 | 29 | $this->reduceQuery($queryType, $mutationType, $reducers); |
|
83 | |||
84 | 29 | foreach ($this->executionContext->getRequest()->getOperationsInOrder() as $operation) { |
|
85 | 29 | if ($operationResult = $this->executeOperation($operation, $operation instanceof Mutation ? $mutationType : $queryType)) { |
|
86 | 29 | $this->data = array_merge($this->data, $operationResult); |
|
87 | }; |
||
88 | } |
||
89 | |||
90 | 4 | } catch (\Exception $e) { |
|
91 | 4 | $this->executionContext->addError($e); |
|
92 | } |
||
93 | |||
94 | 29 | return $this; |
|
95 | } |
||
96 | |||
97 | 29 | protected function parseAndCreateRequest($payload, $variables = []) |
|
98 | { |
||
99 | 29 | if (empty($payload)) { |
|
100 | 1 | throw new \Exception('Must provide an operation.'); |
|
101 | } |
||
102 | |||
103 | 29 | $parser = new Parser(); |
|
104 | 29 | $request = new Request($parser->parse($payload), $variables); |
|
105 | |||
106 | 29 | (new RequestValidator())->validate($request); |
|
107 | |||
108 | 29 | $this->executionContext->setRequest($request); |
|
109 | 29 | } |
|
110 | |||
111 | /** |
||
112 | * @param Query|Field $query |
||
113 | * @param AbstractObjectType $currentLevelSchema |
||
114 | * |
||
115 | * @return array|bool|mixed |
||
116 | */ |
||
117 | 29 | protected function executeOperation(Query $query, $currentLevelSchema) |
|
133 | |||
134 | /** |
||
135 | * @param Query $query |
||
136 | * @param FieldInterface $field |
||
137 | * @param $contextValue |
||
138 | * |
||
139 | * @return array|mixed|null |
||
140 | */ |
||
141 | 26 | protected function processQueryAST(Query $query, FieldInterface $field, $contextValue = null) |
|
142 | { |
||
143 | 26 | if (!$this->resolveValidator->validateArguments($field, $query, $this->executionContext->getRequest())) { |
|
144 | return null; |
||
145 | } |
||
146 | |||
147 | 26 | $resolvedValue = $this->resolveFieldValue($field, $contextValue, $query->getFields(), $this->parseArgumentsValues($field, $query)); |
|
148 | |||
149 | 26 | if (!$this->resolveValidator->isValidValueForField($field, $resolvedValue)) { |
|
150 | 2 | return null; |
|
151 | } |
||
152 | |||
153 | 26 | return $this->collectValueForQueryWithType($query, $field->getType(), $resolvedValue); |
|
154 | } |
||
155 | |||
156 | /** |
||
157 | * @param Query|Mutation $query |
||
158 | * @param AbstractType $fieldType |
||
159 | * @param mixed $resolvedValue |
||
160 | * |
||
161 | * @return array|mixed |
||
162 | * @throws ResolveException |
||
163 | */ |
||
164 | 26 | protected function collectValueForQueryWithType(Query $query, AbstractType $fieldType, $resolvedValue) |
|
165 | { |
||
166 | 26 | if (is_null($resolvedValue)) { |
|
167 | 7 | return null; |
|
168 | } |
||
169 | |||
170 | 24 | $value = []; |
|
171 | |||
172 | 24 | $fieldType = $fieldType->getNullableType(); |
|
173 | |||
174 | 24 | if (!$query->hasFields()) { |
|
175 | 5 | $fieldType = $this->resolveValidator->resolveTypeIfAbstract($fieldType, $resolvedValue); |
|
176 | |||
177 | 5 | if (!TypeService::isLeafType($fieldType->getNamedType())) { |
|
178 | throw new ResolveException(sprintf('You have to specify fields for "%s"', $query->getName())); |
||
179 | } |
||
180 | 5 | if (TypeService::isScalarType($fieldType)) { |
|
181 | 5 | return $this->getOutputValue($fieldType, $resolvedValue); |
|
182 | } |
||
183 | } |
||
184 | |||
185 | 21 | if ($fieldType->getKind() == TypeMap::KIND_LIST) { |
|
186 | 10 | if (!$this->resolveValidator->hasArrayAccess($resolvedValue)) return null; |
|
187 | |||
188 | 10 | $namedType = $fieldType->getNamedType(); |
|
189 | 10 | $validItemStructure = false; |
|
190 | |||
191 | 10 | foreach ($resolvedValue as $resolvedValueItem) { |
|
|
|||
192 | 9 | $value[] = []; |
|
193 | 9 | $index = count($value) - 1; |
|
194 | |||
195 | 9 | $namedType = $this->resolveValidator->resolveTypeIfAbstract($namedType, $resolvedValueItem); |
|
196 | |||
197 | 9 | if (!$validItemStructure) { |
|
198 | 9 | if (!$namedType->isValidValue($resolvedValueItem)) { |
|
199 | 1 | $this->executionContext->addError(new ResolveException(sprintf('Not valid resolve value in %s field', $query->getName()))); |
|
200 | 1 | $value[$index] = null; |
|
201 | 1 | continue; |
|
202 | } |
||
203 | 8 | $validItemStructure = true; |
|
204 | } |
||
205 | |||
206 | 10 | $value[$index] = $this->processQueryFields($query, $namedType, $resolvedValueItem, $value[$index]); |
|
207 | } |
||
208 | } else { |
||
209 | 21 | $value = $this->processQueryFields($query, $fieldType, $resolvedValue, $value); |
|
210 | } |
||
211 | |||
212 | 21 | return $value; |
|
213 | } |
||
214 | |||
215 | /** |
||
216 | * @param FieldAst $fieldAst |
||
217 | * @param FieldInterface $field |
||
218 | * |
||
219 | * @param mixed $contextValue |
||
220 | * |
||
221 | * @return array|mixed|null |
||
222 | * @throws ResolveException |
||
223 | * @throws \Exception |
||
224 | */ |
||
225 | 20 | protected function processFieldAST(FieldAst $fieldAst, FieldInterface $field, $contextValue) |
|
246 | |||
247 | 26 | protected function createResolveInfo($field, $fields) |
|
251 | |||
252 | /** |
||
253 | * @param $contextValue |
||
254 | * @param FieldAst $fieldAst |
||
255 | * @param FieldInterface $field |
||
256 | * |
||
257 | * @throws \Exception |
||
258 | * |
||
259 | * @return mixed |
||
260 | */ |
||
261 | 20 | protected function getPreResolvedValue($contextValue, FieldAst $fieldAst, FieldInterface $field) |
|
262 | { |
||
263 | 20 | if ($field->hasArguments() && !$this->resolveValidator->validateArguments($field, $fieldAst, $this->executionContext->getRequest())) { |
|
264 | return null; |
||
265 | } |
||
266 | |||
267 | 20 | return $this->resolveFieldValue($field, $contextValue, [$fieldAst], $fieldAst->getKeyValueArguments()); |
|
268 | |||
269 | } |
||
270 | |||
271 | 26 | protected function resolveFieldValue(FieldInterface $field, $contextValue, array $fields, array $args) |
|
275 | |||
276 | /** |
||
277 | * @param $field FieldInterface |
||
278 | * @param $query Query |
||
279 | * |
||
280 | * @return array |
||
281 | */ |
||
282 | 26 | protected function parseArgumentsValues(FieldInterface $field, Query $query) |
|
293 | |||
294 | /** |
||
295 | * @param $query Query|FragmentInterface |
||
296 | * @param $queryType AbstractObjectType|TypeInterface|Field|AbstractType |
||
297 | * @param $resolvedValue mixed |
||
298 | * @param $value array |
||
299 | * |
||
300 | * @throws \Exception |
||
301 | * |
||
302 | * @return array |
||
303 | */ |
||
304 | 21 | protected function processQueryFields($query, AbstractType $queryType, $resolvedValue, $value) |
|
376 | |||
377 | protected function getFieldValidatedValue(FieldInterface $field, $value) |
||
381 | |||
382 | 23 | protected function getOutputValue(AbstractType $type, $value) |
|
386 | |||
387 | 29 | public function getResponseData() |
|
401 | |||
402 | /** |
||
403 | * You can access ExecutionContext to check errors and inject dependencies |
||
404 | * |
||
405 | * @return ExecutionContext |
||
406 | */ |
||
407 | 9 | public function getExecutionContext() |
|
411 | |||
412 | /** |
||
413 | * Convenience function for attaching a MaxComplexityQueryVisitor($max) to the next processor run |
||
414 | * |
||
415 | * @param int $max |
||
416 | */ |
||
417 | 1 | public function setMaxComplexity($max) |
|
421 | |||
422 | /** |
||
423 | * Apply all of $reducers to this query. Example reducer operations: checking for maximum query complexity, |
||
424 | * performing look-ahead query planning, etc. |
||
425 | * |
||
426 | * @param AbstractType $queryType |
||
427 | * @param AbstractType $mutationType |
||
428 | * @param AbstractQueryVisitor[] $reducers |
||
429 | */ |
||
430 | 29 | protected function reduceQuery($queryType, $mutationType, array $reducers) |
|
438 | |||
439 | /** |
||
440 | * Entry point for the `walkQuery` routine. Execution bounces between here, where the reducer's ->visit() method |
||
441 | * is invoked, and `walkQuery` where we send in the scores from the `visit` call. |
||
442 | * |
||
443 | * @param Query $query |
||
444 | * @param AbstractType $currentLevelSchema |
||
445 | * @param AbstractQueryVisitor $reducer |
||
446 | */ |
||
447 | 2 | protected function doVisit(Query $query, $currentLevelSchema, $reducer) |
|
474 | |||
475 | /** |
||
476 | * Coroutine to walk the query and schema in DFS manner (see AbstractQueryVisitor docs for more info) and yield a |
||
477 | * tuple of (queryNode, schemaNode, childScore) |
||
478 | * |
||
479 | * childScore costs are accumulated via values sent into the coroutine. |
||
480 | * |
||
481 | * Most of the branching in this function is just to handle the different types in a query: Queries, Unions, |
||
482 | * Fragments (anonymous and named), and Fields. The core of the function is simple: recurse until we hit the base |
||
483 | * case of a Field and yield that back up to the visitor up in `doVisit`. |
||
484 | * |
||
485 | * @param Query|Field|FragmentInterface $queryNode |
||
486 | * @param FieldInterface $currentLevelAST |
||
487 | * |
||
488 | * @return \Generator |
||
489 | */ |
||
490 | 2 | protected function walkQuery($queryNode, FieldInterface $currentLevelAST) |
|
543 | } |
||
544 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
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:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.