Passed
Pull Request — master (#39)
by Markus
07:48
created

HandlesGraphqlRequests   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 266
Duplicated Lines 0 %

Test Coverage

Coverage 98.65%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 116
c 2
b 0
f 0
dl 0
loc 266
ccs 146
cts 148
cp 0.9865
rs 7.44
wmc 52

25 Methods

Rating   Name   Duplication   Size   Complexity  
A schemaPath() 0 3 1
A resolveClassName() 0 11 3
A namespace() 0 3 1
A resolveField() 0 9 2
A queriesNamespace() 0 3 1
A typeFromArray() 0 4 3
A errorFormatter() 0 31 4
A typesNamespace() 0 3 1
A decorateResponse() 0 6 3
A typeFromParentResolver() 0 9 4
A mutationsNamespace() 0 3 1
A reportException() 0 3 1
A propertyNames() 0 7 1
A resolveType() 0 6 1
A typeFromBaseClass() 0 4 2
A resolveTypeMethodName() 0 7 2
A fieldFromResolver() 0 9 4
A decorateTypeConfig() 0 6 2
A typeFromObject() 0 4 2
A resolveFieldMethodName() 0 7 2
A shouldDecorateWithResolveType() 0 4 2
A debugFlags() 0 10 3
A fieldFromArray() 0 11 3
A fieldFromObject() 0 11 2
A __invoke() 0 27 1

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\Promise\PromiseAdapter;
11
use GraphQL\GraphQL;
12
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
13
use GraphQL\Language\AST\TypeDefinitionNode;
14
use GraphQL\Language\AST\UnionTypeDefinitionNode;
15
use GraphQL\Type\Definition\ResolveInfo;
16
use GraphQL\Utils\BuildSchema;
17
use Illuminate\Contracts\Debug\ExceptionHandler;
18
use Illuminate\Database\Eloquent\ModelNotFoundException;
19
use Illuminate\Http\Request;
20
use Illuminate\Support\Str;
21
use Illuminate\Validation\ValidationException;
22
23
trait HandlesGraphqlRequests
24
{
25
    /**
26
     * Invoke the Graphql request handler.
27
     *
28
     * @param  \Illuminate\Http\Request  $request
29
     * @return array
30
     */
31 23
    public function __invoke(Request $request)
32
    {
33 23
        $loader = app(DataLoader::class);
34 23
        $schema = BuildSchema::build(file_get_contents($this->schemaPath()), [$this, 'decorateTypeConfig']);
35
36
        /** @var \GraphQL\Executor\ExecutionResult */
37 23
        $result = null;
38
39 23
        GraphQL::promiseToExecute(
40 23
            app(PromiseAdapter::class),
41
            $schema,
42 23
            $request->input('query'),
43 23
            null, // root
44 23
            compact('loader'), // context
45 23
            $request->input('variables'),
46 23
            $request->input('operationName'),
47 23
            [$this, 'resolveField'],
48 23
            null // validationRules
49 23
        )->then(function ($value) use (&$result) {
50 23
            $result = $value;
51 23
        });
52
53 23
        $loader->run();
54
55 23
        $result->setErrorFormatter([$this, 'errorFormatter']);
56
57 23
        return $this->decorateResponse($result->toArray($this->debugFlags()));
58
    }
59
60 9
    public function errorFormatter(GraphqlError $graphqlError)
61
    {
62 9
        $formattedError = FormattedError::createFromException($graphqlError);
63 9
        $throwable = $graphqlError->getPrevious();
64
65 9
        $this->reportException(
66 9
            $throwable instanceof Exception
0 ignored issues
show
introduced by
$throwable is always a sub-type of Exception.
Loading history...
67 7
            ? $throwable
68 9
            : $graphqlError
69
        );
70
71 9
        if ($throwable instanceof ModelNotFoundException) {
72 1
            return array_merge($formattedError, [
73 1
                'message' => class_basename($throwable->getModel()) . ' not found.',
74
                'extensions' => [
75
                    'category' => 'client',
76
                ],
77
            ]);
78
        }
79
80 8
        if ($throwable instanceof ValidationException) {
81 1
            return array_merge($formattedError, [
82 1
                'message' => $throwable->getMessage(),
83
                'extensions' => [
84 1
                    'category' => 'validation',
85 1
                    'validation' => $throwable->errors(),
86
                ],
87
            ]);
88
        }
89
90 7
        return $formattedError;
91
    }
92
93 9
    public function reportException(Exception $exception)
94
    {
95 9
        app(ExceptionHandler::class)->report($exception);
96 9
    }
97
98 23
    public function schemaPath()
99
    {
100 23
        return config('butler.graphql.schema');
101
    }
102
103 23
    public function decorateTypeConfig(array $config, TypeDefinitionNode $typeDefinitionNode)
104
    {
105 23
        if ($this->shouldDecorateWithResolveType($typeDefinitionNode)) {
106 21
            $config['resolveType'] = [$this, 'resolveType'];
107
        }
108 23
        return $config;
109
    }
110
111 23
    protected function shouldDecorateWithResolveType(TypeDefinitionNode $typeDefinitionNode)
112
    {
113 23
        return $typeDefinitionNode instanceof InterfaceTypeDefinitionNode
114 23
            || $typeDefinitionNode instanceof UnionTypeDefinitionNode;
115
    }
116
117 23
    public function debugFlags()
118
    {
119 23
        $flags = 0;
120 23
        if (config('butler.graphql.include_debug_message')) {
121 7
            $flags |= DebugFlag::INCLUDE_DEBUG_MESSAGE;
122
        }
123 23
        if (config('butler.graphql.include_trace')) {
124 3
            $flags |= DebugFlag::INCLUDE_TRACE;
125
        }
126 23
        return $flags;
127
    }
128
129 22
    public function resolveField($source, $args, $context, ResolveInfo $info)
130
    {
131 22
        $field = $this->fieldFromResolver($source, $args, $context, $info)
132 10
            ?? $this->fieldFromArray($source, $args, $context, $info)
133 14
            ?? $this->fieldFromObject($source, $args, $context, $info);
134
135 14
        return $field instanceof \Closure
136 1
            ? $field($source, $args, $context, $info)
137 14
            : $field;
138
    }
139
140 4
    public function resolveType($source, $context, ResolveInfo $info)
141
    {
142 4
        return $this->typeFromArray($source, $context, $info)
143 4
            ?? $this->typeFromObject($source, $context, $info)
144 4
            ?? $this->typeFromParentResolver($source, $context, $info)
145 4
            ?? $this->typeFromBaseClass($source, $context, $info);
146
    }
147
148 22
    public function fieldFromResolver($source, $args, $context, ResolveInfo $info)
149
    {
150 22
        $className = $this->resolveClassName($info);
151 22
        $methodName = $this->resolveFieldMethodName($info);
152
153 22
        if (app()->has($className) || class_exists($className)) {
154 22
            $resolver = app($className);
155 21
            if (method_exists($resolver, $methodName)) {
156 21
                return $resolver->{$methodName}($source, $args, $context, $info);
157
            }
158
        }
159 10
    }
160
161 10
    public function fieldFromArray($source, $args, $context, ResolveInfo $info)
162
    {
163 10
        if (is_array($source) || $source instanceof \ArrayAccess) {
164 8
            return collect($this->propertyNames($info))
165 8
                ->map(function ($propertyName) use ($source) {
166 8
                    return $source[$propertyName] ?? null;
167 8
                })
168 8
                ->reject(function ($value) {
169 8
                    return is_null($value);
170 8
                })
171 8
                ->first();
172
        }
173 7
    }
174
175 7
    public function fieldFromObject($source, $args, $context, ResolveInfo $info)
176
    {
177 7
        if (is_object($source)) {
178 7
            return collect($this->propertyNames($info))
179 7
                ->map(function ($propertyName) use ($source) {
180 7
                    return $source->{$propertyName} ?? null;
181 7
                })
182 7
                ->reject(function ($value) {
183 7
                    return is_null($value);
184 7
                })
185 7
                ->first();
186
        }
187 1
    }
188
189 4
    public function typeFromArray($source, $context, ResolveInfo $info)
190
    {
191 4
        if (is_array($source) || $source instanceof \ArrayAccess) {
192 4
            return $source['__typename'] ?? null;
193
        }
194 2
    }
195
196 4
    public function typeFromObject($source, $context, ResolveInfo $info)
197
    {
198 4
        if (is_object($source)) {
199 2
            return $source->__typename ?? null;
200
        }
201 4
    }
202
203 4
    public function typeFromParentResolver($source, $context, ResolveInfo $info)
204
    {
205 4
        $className = $this->resolveClassName($info);
206 4
        $methodName = $this->resolveTypeMethodName($info);
207
208 4
        if (app()->has($className) || class_exists($className)) {
209 4
            $resolver = app($className);
210 4
            if (method_exists($resolver, $methodName)) {
211 4
                return $resolver->{$methodName}($source, $context, $info);
212
            }
213
        }
214
    }
215
216 1
    public function typeFromBaseClass($source, $context, ResolveInfo $info)
217
    {
218 1
        if (is_object($source)) {
219 1
            return class_basename($source);
220
        }
221
    }
222
223 10
    public function propertyNames(ResolveInfo $info): array
224
    {
225 10
        return collect([
226 10
            Str::snake($info->fieldName),
227 10
            Str::camel($info->fieldName),
228 10
            Str::kebab(Str::camel($info->fieldName)),
229 10
        ])->unique()->toArray();
230
    }
231
232 22
    protected function resolveClassName(ResolveInfo $info): string
233
    {
234 22
        if ($info->parentType->name === 'Query') {
235 21
            return $this->queriesNamespace() . Str::studly($info->fieldName);
236
        }
237
238 11
        if ($info->parentType->name === 'Mutation') {
239 1
            return $this->mutationsNamespace() . Str::studly($info->fieldName);
240
        }
241
242 11
        return $this->typesNamespace() . Str::studly($info->parentType->name);
243
    }
244
245 22
    public function resolveFieldMethodName(ResolveInfo $info): string
246
    {
247 22
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
248 22
            return '__invoke';
249
        }
250
251 11
        return Str::camel($info->fieldName);
252
    }
253
254 4
    public function resolveTypeMethodName(ResolveInfo $info): string
255
    {
256 4
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
257 2
            return 'resolveType';
258
        }
259
260 2
        return 'resolveTypeFor' . ucfirst(Str::camel($info->fieldName));
261
    }
262
263 22
    public function namespace(): string
264
    {
265 22
        return config('butler.graphql.namespace');
266
    }
267
268 21
    public function queriesNamespace(): string
269
    {
270 21
        return $this->namespace() . 'Queries\\';
271
    }
272
273 1
    public function mutationsNamespace(): string
274
    {
275 1
        return $this->namespace() . 'Mutations\\';
276
    }
277
278 11
    public function typesNamespace(): string
279
    {
280 11
        return $this->namespace() . 'Types\\';
281
    }
282
283 23
    public function decorateResponse(array $data): array
284
    {
285 23
        if (app()->bound('debugbar') && app('debugbar')->isEnabled()) {
0 ignored issues
show
Bug introduced by
The method isEnabled() does not exist on Illuminate\Contracts\Foundation\Application. ( Ignorable by Annotation )

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

285
        if (app()->bound('debugbar') && app('debugbar')->/** @scrutinizer ignore-call */ isEnabled()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
286 1
            $data['debug'] = app('debugbar')->getData();
0 ignored issues
show
Bug introduced by
The method getData() does not exist on Illuminate\Contracts\Foundation\Application. ( Ignorable by Annotation )

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

286
            $data['debug'] = app('debugbar')->/** @scrutinizer ignore-call */ getData();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
287
        }
288 23
        return $data;
289
    }
290
}
291