Test Failed
Pull Request — master (#53)
by
unknown
12:29
created

HandlesGraphqlRequests::getFieldResolverCallback()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 10
cc 3
nc 3
nop 1
crap 3
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
        if ($resolver = $this->getFieldResolverCallback($info)) {
188 11
            $field = $resolver($source, $args, $context, $info);
189 11
        } else {
190
            $field = $this->fieldFromArray($source, $args, $context, $info)
191 16
                ?? $this->fieldFromObject($source, $args, $context, $info);
192
        }
193
194
        if ($this->fieldIsBackedEnum($field) && $this->returnTypeIsLeaf($info)) {
195 16
            $field = $field->value;
196 16
        }
197 1
198 16
        return call(static function () use ($field, $source, $args, $context, $info) {
199
            return $field instanceof \Closure
200
                ? $field($source, $args, $context, $info)
201
                : $field;
202 4
        });
203
    }
204 4
205 4
    public function resolveType($source, $context, ResolveInfo $info)
206 4
    {
207 4
        return $this->typeFromArray($source, $context, $info)
208
            ?? $this->typeFromObject($source, $context, $info)
209
            ?? $this->typeFromParentResolver($source, $context, $info)
210 26
            ?? $this->typeFromBaseClass($source, $context, $info);
211
    }
212 26
213 26
    public function getFieldResolverCallback(ResolveInfo $info)
214
    {
215 26
        $className = $this->resolveClassName($info);
216 25
        $methodName = $this->resolveFieldMethodName($info);
217 25
218
        if ($resolver = $this->make($className)) {
219
            if (method_exists($resolver, $methodName)) {
220
                return fn (...$args) => $resolver->{$methodName}(...$args);
221
            }
222 11
        }
223
    }
224 11
225 9
    public function fieldFromArray($source, $args, $context, ResolveInfo $info)
226 9
    {
227
        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 1
                ->map(function ($propertyName) use ($source) {
230 1
                    try {
231
                        return $source[$propertyName] ?? null;
232
                    } catch (MissingAttributeException) {
233 9
                        return null;
234 9
                    }
235
                })
236 9
                ->reject(function ($value) {
237
                    return is_null($value);
238
                })
239
                ->first();
240 7
        }
241
    }
242 7
243 7
    public function fieldFromObject($source, $args, $context, ResolveInfo $info)
244 7
    {
245
        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
                ->map(function ($propertyName) use ($source) {
248
                    try {
249
                        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 7
                        return null;
252 7
                    }
253
                })
254 7
                ->reject(function ($value) {
255
                    return is_null($value);
256
                })
257
                ->first();
258 16
        }
259
    }
260 16
261
    public function fieldIsBackedEnum($field): bool
262
    {
263 4
        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 4
266 4
    public function typeFromArray($source, $context, ResolveInfo $info)
267
    {
268
        if (is_array($source) || $source instanceof \ArrayAccess) {
269
            return $source['__typename'] ?? null;
270 4
        }
271
    }
272 4
273 2
    public function typeFromObject($source, $context, ResolveInfo $info)
274
    {
275
        if (is_object($source)) {
276
            return $source->__typename ?? null;
277 4
        }
278
    }
279 4
280 4
    public function typeFromParentResolver($source, $context, ResolveInfo $info)
281
    {
282 4
        $className = $this->resolveClassName($info);
283 4
        $methodName = $this->resolveTypeMethodName($info);
284 4
285
        if ($resolver = $this->make($className)) {
286
            if (method_exists($resolver, $methodName)) {
287
                return $resolver->{$methodName}($source, $context, $info);
288
            }
289 1
        }
290
    }
291 1
292 1
    public function typeFromBaseClass($source, $context, ResolveInfo $info)
293
    {
294
        if (is_object($source)) {
295
            return class_basename($source);
296 11
        }
297
    }
298 11
299 11
    public function propertyNames(ResolveInfo $info): array
300 11
    {
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
            Str::camel($info->fieldName),
304
            Str::kebab(Str::camel($info->fieldName)),
305 26
        ])->unique()->toArray();
306
    }
307 26
308 25
    protected function resolveClassName(ResolveInfo $info): string
309
    {
310
        if ($info->parentType->name === 'Query') {
311 12
            return $this->queriesNamespace() . Str::studly($info->fieldName);
312 1
        }
313
314
        if ($info->parentType->name === 'Mutation') {
315 12
            return $this->mutationsNamespace() . Str::studly($info->fieldName);
316
        }
317
318 26
        return $this->typesNamespace() . Str::studly($info->parentType->name);
319
    }
320 26
321 26
    public function resolveFieldMethodName(ResolveInfo $info): string
322
    {
323
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
324 12
            return '__invoke';
325
        }
326
327 4
        return Str::camel($info->fieldName);
328
    }
329 4
330 2
    public function resolveTypeMethodName(ResolveInfo $info): string
331
    {
332
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
333 2
            return 'resolveType';
334
        }
335
336 26
        return 'resolveTypeFor' . ucfirst(Str::camel($info->fieldName));
337
    }
338 26
339
    public function namespace(): string
340
    {
341 25
        return $this->namespaceCache ?? $this->namespaceCache = config('butler.graphql.namespace');
342
    }
343 25
344
    public function queriesNamespace(): string
345
    {
346 1
        return $this->namespace() . 'Queries\\';
347
    }
348 1
349
    public function mutationsNamespace(): string
350
    {
351 12
        return $this->namespace() . 'Mutations\\';
352
    }
353 12
354
    public function typesNamespace(): string
355
    {
356
        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 29
         return $returnType instanceof LeafType;
366
    }
367 29
368 1
    public function decorateResponse(array $data): array
369
    {
370 29
        if (app()->bound('debugbar') && app('debugbar')->isEnabled()) {
371
            $data['debug'] = app('debugbar')->getData();
372
        }
373 26
        return $data;
374
    }
375 26
376 11
    protected function make(string $className)
377
    {
378
        if (array_key_exists($className, $this->classCache)) {
379 26
            return $this->classCache[$className];
380 26
        }
381 8
382
        $class = app()->has($className) || class_exists($className)
383 25
            ? app($className)
384
            : null;
385
386
        return $this->classCache[$className] = $class;
387
    }
388
}
389