Passed
Pull Request — master (#52)
by Markus
03:03
created

HandlesGraphqlRequests   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 351
Duplicated Lines 0 %

Test Coverage

Coverage 95.18%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 68
eloc 156
c 3
b 0
f 0
dl 0
loc 351
ccs 158
cts 166
cp 0.9518
rs 2.96

30 Methods

Rating   Name   Duplication   Size   Complexity  
A schemaPath() 0 3 1
A resolveClassName() 0 11 3
A returnTypeIsLeaf() 0 7 2
A namespace() 0 3 1
A resolveField() 0 14 4
A beforeExecutionHook() 0 7 1
A fieldFromArray() 0 15 4
A queriesNamespace() 0 3 1
A typeFromArray() 0 4 3
B errorFormatter() 0 44 7
A typesNamespace() 0 3 1
A decorateResponse() 0 6 3
A mutationsNamespace() 0 3 1
A fieldFromObject() 0 15 3
A typeFromParentResolver() 0 8 3
A reportException() 0 3 1
A __invoke() 0 43 2
A fieldIsBackedEnum() 0 3 2
A resolveType() 0 6 1
A propertyNames() 0 7 1
A typeFromBaseClass() 0 4 2
A debugFlags() 0 10 3
A fieldFromResolver() 0 8 3
A resolveTypeMethodName() 0 7 2
A decorateTypeConfig() 0 6 2
A schema() 0 3 1
A make() 0 11 4
A typeFromObject() 0 4 2
A resolveFieldMethodName() 0 7 2
A shouldDecorateWithResolveType() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like HandlesGraphqlRequests 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.

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 HandlesGraphqlRequests, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Butler\Graphql\Concerns;
4
5
use Butler\Graphql\DataLoader;
6
use Exception;
7
use GraphQL\Error\DebugFlag;
8
use GraphQL\Error\Error as GraphqlError;
9
use GraphQL\Error\FormattedError;
10
use GraphQL\Executor\ExecutionResult;
11
use GraphQL\Executor\Promise\PromiseAdapter;
12
use GraphQL\GraphQL;
13
use GraphQL\Language\AST\DocumentNode;
14
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
15
use GraphQL\Language\AST\TypeDefinitionNode;
16
use GraphQL\Language\AST\UnionTypeDefinitionNode;
17
use GraphQL\Language\Parser;
18
use GraphQL\Type\Definition\LeafType;
19
use GraphQL\Type\Definition\ResolveInfo;
20
use GraphQL\Type\Definition\WrappingType;
21
use GraphQL\Type\Schema;
22
use GraphQL\Utils\BuildSchema;
23
use Illuminate\Contracts\Debug\ExceptionHandler;
24
use Illuminate\Database\Eloquent\MissingAttributeException;
25
use Illuminate\Database\Eloquent\ModelNotFoundException;
26
use Illuminate\Http\Request;
27
use Illuminate\Support\Str;
28
use Illuminate\Validation\ValidationException;
29
use Symfony\Component\HttpKernel\Exception\HttpException;
30
31
use function Amp\call;
32
33
trait HandlesGraphqlRequests
34
{
35
    private $classCache;
36
    private $namespaceCache;
37
38
    /**
39
     * Invoke the Graphql request handler.
40
     *
41
     * @param  \Illuminate\Http\Request  $request
42
     * @return array
43
     */
44 29
    public function __invoke(Request $request)
45
    {
46 29
        $this->classCache = [];
47 29
        $this->namespaceCache = null;
48
49 29
        $loader = app(DataLoader::class);
50
51 29
        $query = $request->input('query');
52 29
        $variables = $request->input('variables');
53 29
        $operationName = $request->input('operationName');
54
55
        try {
56 29
            $schema = BuildSchema::build($this->schema(), [$this, 'decorateTypeConfig']);
57
58 28
            $source = Parser::parse($query);
59
60 27
            $this->beforeExecutionHook($schema, $source, $operationName, $variables);
61
62
            /** @var \GraphQL\Executor\ExecutionResult */
63 27
            $result = null;
64
65 27
            GraphQL::promiseToExecute(
66 27
                app(PromiseAdapter::class),
67
                $schema,
68
                $source,
69
                null, // root
70 27
                compact('loader'), // context
71
                $variables,
72
                $operationName,
73 27
                [$this, 'resolveField'],
74
                null // validationRules
75 27
            )->then(function ($value) use (&$result) {
76 27
                $result = $value;
77
            });
78
79 27
            $loader->run();
80 2
        } catch (GraphqlError $e) {
81 2
            $result = new ExecutionResult(null, [$e]);
82
        }
83
84 29
        $result->setErrorFormatter([$this, 'errorFormatter']);
85
86 29
        return $this->decorateResponse($result->toArray($this->debugFlags()));
87
    }
88
89 26
    public function beforeExecutionHook(
90
        Schema $schema,
91
        DocumentNode $query,
92
        string $operationName = null,
93
        $variables = null
94
    ): void {
95 26
        return;
96
    }
97
98 13
    public function errorFormatter(GraphqlError $graphqlError)
99
    {
100 13
        $formattedError = FormattedError::createFromException($graphqlError);
101 13
        $throwable = $graphqlError->getPrevious();
102
103 13
        $this->reportException(
104 13
            $throwable instanceof Exception ? $throwable : $graphqlError
0 ignored issues
show
introduced by
$throwable is always a sub-type of Exception.
Loading history...
105
        );
106
107
        if (
108 13
            $throwable instanceof HttpException &&
109 13
            $throwable->getStatusCode() >= 400 &&
110 13
            $throwable->getStatusCode() < 500
111
        ) {
112 1
            return array_merge($formattedError, [
113 1
                'message' => $throwable->getMessage(),
114
                'extensions' => [
115 1
                    'category' => 'client',
116 1
                    'code' => $throwable->getStatusCode(),
117
                ],
118
            ]);
119
        }
120
121 12
        if ($throwable instanceof ModelNotFoundException) {
122 1
            return array_merge($formattedError, [
123 1
                'message' => class_basename($throwable->getModel()) . ' not found.',
124
                'extensions' => [
125
                    'category' => 'client',
126
                    'code' => 404,
127
                ],
128
            ]);
129
        }
130
131 11
        if ($throwable instanceof ValidationException) {
132 1
            return array_merge($formattedError, [
133 1
                'message' => $throwable->getMessage(),
134
                'extensions' => [
135 1
                    'category' => 'validation',
136 1
                    'validation' => $throwable->errors(),
137
                ],
138
            ]);
139
        }
140
141 10
        return $formattedError;
142
    }
143
144 13
    public function reportException(Exception $exception)
145
    {
146 13
        app(ExceptionHandler::class)->report($exception);
147
    }
148
149 27
    public function schema()
150
    {
151 27
        return file_get_contents($this->schemaPath());
152
    }
153
154 27
    public function schemaPath()
155
    {
156 27
        return config('butler.graphql.schema');
157
    }
158
159 28
    public function decorateTypeConfig(array $config, TypeDefinitionNode $typeDefinitionNode)
160
    {
161 28
        if ($this->shouldDecorateWithResolveType($typeDefinitionNode)) {
162 25
            $config['resolveType'] = [$this, 'resolveType'];
163
        }
164 28
        return $config;
165
    }
166
167 28
    protected function shouldDecorateWithResolveType(TypeDefinitionNode $typeDefinitionNode)
168
    {
169 28
        return $typeDefinitionNode instanceof InterfaceTypeDefinitionNode
170
            || $typeDefinitionNode instanceof UnionTypeDefinitionNode;
171
    }
172
173 29
    public function debugFlags()
174
    {
175 29
        $flags = 0;
176 29
        if (config('butler.graphql.include_debug_message')) {
177 8
            $flags |= DebugFlag::INCLUDE_DEBUG_MESSAGE;
178
        }
179 29
        if (config('butler.graphql.include_trace')) {
180 3
            $flags |= DebugFlag::INCLUDE_TRACE;
181
        }
182 29
        return $flags;
183
    }
184
185 26
    public function resolveField($source, $args, $context, ResolveInfo $info)
186
    {
187 26
        $field = $this->fieldFromResolver($source, $args, $context, $info)
188 11
            ?? $this->fieldFromArray($source, $args, $context, $info)
189 11
            ?? $this->fieldFromObject($source, $args, $context, $info);
190
191 16
        if ($this->fieldIsBackedEnum($field) && $this->returnTypeIsLeaf($info)) {
192
            $field = $field->value;
193
        }
194
195 16
        return call(static function () use ($field, $source, $args, $context, $info) {
196 16
            return $field instanceof \Closure
197 1
                ? $field($source, $args, $context, $info)
198 16
                : $field;
199
        });
200
    }
201
202 4
    public function resolveType($source, $context, ResolveInfo $info)
203
    {
204 4
        return $this->typeFromArray($source, $context, $info)
205 4
            ?? $this->typeFromObject($source, $context, $info)
206 4
            ?? $this->typeFromParentResolver($source, $context, $info)
207 4
            ?? $this->typeFromBaseClass($source, $context, $info);
208
    }
209
210 26
    public function fieldFromResolver($source, $args, $context, ResolveInfo $info)
211
    {
212 26
        $className = $this->resolveClassName($info);
213 26
        $methodName = $this->resolveFieldMethodName($info);
214
215 26
        if ($resolver = $this->make($className)) {
216 25
            if (method_exists($resolver, $methodName)) {
217 25
                return $resolver->{$methodName}($source, $args, $context, $info);
218
            }
219
        }
220
    }
221
222 11
    public function fieldFromArray($source, $args, $context, ResolveInfo $info)
223
    {
224 11
        if (is_array($source) || $source instanceof \ArrayAccess) {
225 9
            return collect($this->propertyNames($info))
0 ignored issues
show
Bug introduced by
$this->propertyNames($info) of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

225
            return collect(/** @scrutinizer ignore-type */ $this->propertyNames($info))
Loading history...
226 9
                ->map(function ($propertyName) use ($source) {
227
                    try {
228 9
                        return $source[$propertyName] ?? null;
229 1
                    } catch (MissingAttributeException) {
230 1
                        return null;
231
                    }
232
                })
233 9
                ->reject(function ($value) {
234 9
                    return is_null($value);
235
                })
236 9
                ->first();
237
        }
238
    }
239
240 7
    public function fieldFromObject($source, $args, $context, ResolveInfo $info)
241
    {
242 7
        if (is_object($source)) {
243 7
            return collect($this->propertyNames($info))
0 ignored issues
show
Bug introduced by
$this->propertyNames($info) of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

243
            return collect(/** @scrutinizer ignore-type */ $this->propertyNames($info))
Loading history...
244 7
                ->map(function ($propertyName) use ($source) {
245
                    try {
246 7
                        return $source->{$propertyName} ?? null;
247
                    } catch (MissingAttributeException) {
0 ignored issues
show
Unused Code introduced by
catch (\Illuminate\Datab...singAttributeException) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
248
                        return null;
249
                    }
250
                })
251 7
                ->reject(function ($value) {
252 7
                    return is_null($value);
253
                })
254 7
                ->first();
255
        }
256
    }
257
258 16
    public function fieldIsBackedEnum($field): bool
259
    {
260 16
        return function_exists('enum_exists') && $field instanceof \BackedEnum;
0 ignored issues
show
Bug introduced by
The type BackedEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
261
    }
262
263 4
    public function typeFromArray($source, $context, ResolveInfo $info)
264
    {
265 4
        if (is_array($source) || $source instanceof \ArrayAccess) {
266 4
            return $source['__typename'] ?? null;
267
        }
268
    }
269
270 4
    public function typeFromObject($source, $context, ResolveInfo $info)
271
    {
272 4
        if (is_object($source)) {
273 2
            return $source->__typename ?? null;
274
        }
275
    }
276
277 4
    public function typeFromParentResolver($source, $context, ResolveInfo $info)
278
    {
279 4
        $className = $this->resolveClassName($info);
280 4
        $methodName = $this->resolveTypeMethodName($info);
281
282 4
        if ($resolver = $this->make($className)) {
283 4
            if (method_exists($resolver, $methodName)) {
284 4
                return $resolver->{$methodName}($source, $context, $info);
285
            }
286
        }
287
    }
288
289 1
    public function typeFromBaseClass($source, $context, ResolveInfo $info)
290
    {
291 1
        if (is_object($source)) {
292 1
            return class_basename($source);
293
        }
294
    }
295
296 11
    public function propertyNames(ResolveInfo $info): array
297
    {
298 11
        return collect([
0 ignored issues
show
Bug introduced by
array(Illuminate\Support...mel($info->fieldName))) of type array<integer,string> is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

298
        return collect(/** @scrutinizer ignore-type */ [
Loading history...
299 11
            Str::snake($info->fieldName),
300 11
            Str::camel($info->fieldName),
301 11
            Str::kebab(Str::camel($info->fieldName)),
302 11
        ])->unique()->toArray();
303
    }
304
305 26
    protected function resolveClassName(ResolveInfo $info): string
306
    {
307 26
        if ($info->parentType->name === 'Query') {
308 25
            return $this->queriesNamespace() . Str::studly($info->fieldName);
309
        }
310
311 12
        if ($info->parentType->name === 'Mutation') {
312 1
            return $this->mutationsNamespace() . Str::studly($info->fieldName);
313
        }
314
315 12
        return $this->typesNamespace() . Str::studly($info->parentType->name);
316
    }
317
318 26
    public function resolveFieldMethodName(ResolveInfo $info): string
319
    {
320 26
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
321 26
            return '__invoke';
322
        }
323
324 12
        return Str::camel($info->fieldName);
325
    }
326
327 4
    public function resolveTypeMethodName(ResolveInfo $info): string
328
    {
329 4
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
330 2
            return 'resolveType';
331
        }
332
333 2
        return 'resolveTypeFor' . ucfirst(Str::camel($info->fieldName));
334
    }
335
336 26
    public function namespace(): string
337
    {
338 26
        return $this->namespaceCache ?? $this->namespaceCache = config('butler.graphql.namespace');
339
    }
340
341 25
    public function queriesNamespace(): string
342
    {
343 25
        return $this->namespace() . 'Queries\\';
344
    }
345
346 1
    public function mutationsNamespace(): string
347
    {
348 1
        return $this->namespace() . 'Mutations\\';
349
    }
350
351 12
    public function typesNamespace(): string
352
    {
353 12
        return $this->namespace() . 'Types\\';
354
    }
355
356
    public function returnTypeIsLeaf(ResolveInfo $info): bool
357
    {
358
         $returnType = $info->returnType instanceof WrappingType
359
            ? $info->returnType->getWrappedType(true)
360
            : $info->returnType;
361
362
         return $returnType instanceof LeafType;
363
    }
364
365 29
    public function decorateResponse(array $data): array
366
    {
367 29
        if (app()->bound('debugbar') && app('debugbar')->isEnabled()) {
368 1
            $data['debug'] = app('debugbar')->getData();
369
        }
370 29
        return $data;
371
    }
372
373 26
    protected function make(string $className)
374
    {
375 26
        if (array_key_exists($className, $this->classCache)) {
376 11
            return $this->classCache[$className];
377
        }
378
379 26
        $class = app()->has($className) || class_exists($className)
380 26
            ? app($className)
381 8
            : null;
382
383 25
        return $this->classCache[$className] = $class;
384
    }
385
}
386