HandlesGraphqlRequests   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 354
Duplicated Lines 0 %

Test Coverage

Coverage 94.01%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 69
eloc 158
c 2
b 0
f 0
dl 0
loc 354
ccs 157
cts 167
cp 0.9401
rs 2.88

30 Methods

Rating   Name   Duplication   Size   Complexity  
A schemaPath() 0 3 1
A beforeExecutionHook() 0 7 1
A reportException() 0 3 1
A debugFlags() 0 10 3
A decorateTypeConfig() 0 6 2
A schema() 0 3 1
A shouldDecorateWithResolveType() 0 4 2
B errorFormatter() 0 44 7
A __invoke() 0 43 2
A resolveClassName() 0 11 3
A returnTypeIsLeaf() 0 7 2
A namespace() 0 3 1
A resolveField() 0 17 5
A fieldFromArray() 0 15 4
A queriesNamespace() 0 3 1
A typeFromArray() 0 4 3
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 fieldIsBackedEnum() 0 3 2
A resolveType() 0 6 1
A propertyNames() 0 7 1
A typeFromBaseClass() 0 4 2
A resolveTypeMethodName() 0 7 2
A make() 0 11 4
A typeFromObject() 0 4 2
A resolveFieldMethodName() 0 7 2
A resolverForField() 0 8 3

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 30
    public function __invoke(Request $request)
45
    {
46 30
        $this->classCache = [];
47 30
        $this->namespaceCache = null;
48
49 30
        $loader = app(DataLoader::class);
50
51 30
        $query = $request->input('query');
52 30
        $variables = $request->input('variables');
53 30
        $operationName = $request->input('operationName');
54
55
        try {
56 30
            $schema = BuildSchema::build($this->schema(), [$this, 'decorateTypeConfig']);
57
58 29
            $source = Parser::parse($query);
59
60 28
            $this->beforeExecutionHook($schema, $source, $operationName, $variables);
61
62
            /** @var \GraphQL\Executor\ExecutionResult */
63 28
            $result = null;
64
65 28
            GraphQL::promiseToExecute(
66 28
                app(PromiseAdapter::class),
67
                $schema,
68
                $source,
69
                null, // root
70 28
                compact('loader'), // context
71
                $variables,
72
                $operationName,
73 28
                [$this, 'resolveField'],
74
                null // validationRules
75 28
            )->then(function ($value) use (&$result) {
76 28
                $result = $value;
77
            });
78
79 28
            $loader->run();
80 2
        } catch (GraphqlError $e) {
81 2
            $result = new ExecutionResult(null, [$e]);
82
        }
83
84 30
        $result->setErrorFormatter([$this, 'errorFormatter']);
85
86 30
        return $this->decorateResponse($result->toArray($this->debugFlags()));
87
    }
88
89 27
    public function beforeExecutionHook(
90
        Schema $schema,
91
        DocumentNode $query,
92
        string $operationName = null,
93
        $variables = null
94
    ): void {
95 27
        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 28
    public function schema()
150
    {
151 28
        return file_get_contents($this->schemaPath());
152
    }
153
154 28
    public function schemaPath()
155
    {
156 28
        return config('butler.graphql.schema');
157
    }
158
159 29
    public function decorateTypeConfig(array $config, TypeDefinitionNode $typeDefinitionNode)
160
    {
161 29
        if ($this->shouldDecorateWithResolveType($typeDefinitionNode)) {
162 26
            $config['resolveType'] = [$this, 'resolveType'];
163
        }
164 29
        return $config;
165
    }
166
167 29
    protected function shouldDecorateWithResolveType(TypeDefinitionNode $typeDefinitionNode)
168
    {
169 29
        return $typeDefinitionNode instanceof InterfaceTypeDefinitionNode
170
            || $typeDefinitionNode instanceof UnionTypeDefinitionNode;
171
    }
172
173 30
    public function debugFlags()
174
    {
175 30
        $flags = 0;
176 30
        if (config('butler.graphql.include_debug_message')) {
177 8
            $flags |= DebugFlag::INCLUDE_DEBUG_MESSAGE;
178
        }
179 30
        if (config('butler.graphql.include_trace')) {
180 3
            $flags |= DebugFlag::INCLUDE_TRACE;
181
        }
182 30
        return $flags;
183
    }
184
185 27
    public function resolveField($source, $args, $context, ResolveInfo $info)
186
    {
187 27
        if ($resolver = $this->resolverForField($info)) {
188 26
            $field = $resolver($source, $args, $context, $info);
189
        } else {
190 11
            $field = $this->fieldFromArray($source, $args, $context, $info)
191 7
                ?? $this->fieldFromObject($source, $args, $context, $info);
192
        }
193
194 17
        if ($this->fieldIsBackedEnum($field) && $this->returnTypeIsLeaf($info)) {
195
            $field = $field->value;
196
        }
197
198 17
        return call(static function () use ($field, $source, $args, $context, $info) {
199 17
            return $field instanceof \Closure
200 1
                ? $field($source, $args, $context, $info)
201 17
                : $field;
202
        });
203
    }
204
205 4
    public function resolveType($source, $context, ResolveInfo $info)
206
    {
207 4
        return $this->typeFromArray($source, $context, $info)
208 4
            ?? $this->typeFromObject($source, $context, $info)
209 4
            ?? $this->typeFromParentResolver($source, $context, $info)
210 4
            ?? $this->typeFromBaseClass($source, $context, $info);
211
    }
212
213 27
    public function resolverForField(ResolveInfo $info)
214
    {
215 27
        $className = $this->resolveClassName($info);
216 27
        $methodName = $this->resolveFieldMethodName($info);
217
218 27
        if ($resolver = $this->make($className)) {
219 26
            if (method_exists($resolver, $methodName)) {
220 26
                return fn (...$args) => $resolver->{$methodName}(...$args);
221
            }
222
        }
223
    }
224
225 11
    public function fieldFromArray($source, $args, $context, ResolveInfo $info)
226
    {
227 11
        if (is_array($source) || $source instanceof \ArrayAccess) {
228 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

228
            return collect(/** @scrutinizer ignore-type */ $this->propertyNames($info))
Loading history...
229 9
                ->map(function ($propertyName) use ($source) {
230
                    try {
231 9
                        return $source[$propertyName] ?? null;
232
                    } catch (MissingAttributeException) {
233
                        return null;
234
                    }
235
                })
236 9
                ->reject(function ($value) {
237 9
                    return is_null($value);
238
                })
239 9
                ->first();
240
        }
241
    }
242
243 7
    public function fieldFromObject($source, $args, $context, ResolveInfo $info)
244
    {
245 7
        if (is_object($source)) {
246 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

246
            return collect(/** @scrutinizer ignore-type */ $this->propertyNames($info))
Loading history...
247 7
                ->map(function ($propertyName) use ($source) {
248
                    try {
249 7
                        return $source->{$propertyName} ?? null;
250
                    } 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...
251
                        return null;
252
                    }
253
                })
254 7
                ->reject(function ($value) {
255 7
                    return is_null($value);
256
                })
257 7
                ->first();
258
        }
259
    }
260
261 17
    public function fieldIsBackedEnum($field): bool
262
    {
263 17
        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...
264
    }
265
266 4
    public function typeFromArray($source, $context, ResolveInfo $info)
267
    {
268 4
        if (is_array($source) || $source instanceof \ArrayAccess) {
269 4
            return $source['__typename'] ?? null;
270
        }
271
    }
272
273 4
    public function typeFromObject($source, $context, ResolveInfo $info)
274
    {
275 4
        if (is_object($source)) {
276 2
            return $source->__typename ?? null;
277
        }
278
    }
279
280 4
    public function typeFromParentResolver($source, $context, ResolveInfo $info)
281
    {
282 4
        $className = $this->resolveClassName($info);
283 4
        $methodName = $this->resolveTypeMethodName($info);
284
285 4
        if ($resolver = $this->make($className)) {
286 4
            if (method_exists($resolver, $methodName)) {
287 4
                return $resolver->{$methodName}($source, $context, $info);
288
            }
289
        }
290
    }
291
292 1
    public function typeFromBaseClass($source, $context, ResolveInfo $info)
293
    {
294 1
        if (is_object($source)) {
295 1
            return class_basename($source);
296
        }
297
    }
298
299 11
    public function propertyNames(ResolveInfo $info): array
300
    {
301 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

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