Passed
Push — master ( eaeb54...0cf254 )
by Markus
13:28
created

HandlesGraphqlRequests::returnTypeIsLeaf()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

224
            return collect(/** @scrutinizer ignore-type */ $this->propertyNames($info))
Loading history...
225 8
                ->map(function ($propertyName) use ($source) {
226 8
                    return $source[$propertyName] ?? null;
227
                })
228 8
                ->reject(function ($value) {
229 8
                    return is_null($value);
230
                })
231 8
                ->first();
232
        }
233
    }
234
235 7
    public function fieldFromObject($source, $args, $context, ResolveInfo $info)
236
    {
237 7
        if (is_object($source)) {
238 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

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

289
        return collect(/** @scrutinizer ignore-type */ [
Loading history...
290 10
            Str::snake($info->fieldName),
291 10
            Str::camel($info->fieldName),
292 10
            Str::kebab(Str::camel($info->fieldName)),
293 10
        ])->unique()->toArray();
294
    }
295
296 25
    protected function resolveClassName(ResolveInfo $info): string
297
    {
298 25
        if ($info->parentType->name === 'Query') {
299 24
            return $this->queriesNamespace() . Str::studly($info->fieldName);
300
        }
301
302 11
        if ($info->parentType->name === 'Mutation') {
303 1
            return $this->mutationsNamespace() . Str::studly($info->fieldName);
304
        }
305
306 11
        return $this->typesNamespace() . Str::studly($info->parentType->name);
307
    }
308
309 25
    public function resolveFieldMethodName(ResolveInfo $info): string
310
    {
311 25
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
312 25
            return '__invoke';
313
        }
314
315 11
        return Str::camel($info->fieldName);
316
    }
317
318 4
    public function resolveTypeMethodName(ResolveInfo $info): string
319
    {
320 4
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
321 2
            return 'resolveType';
322
        }
323
324 2
        return 'resolveTypeFor' . ucfirst(Str::camel($info->fieldName));
325
    }
326
327 25
    public function namespace(): string
328
    {
329 25
        return $this->namespaceCache ?? $this->namespaceCache = config('butler.graphql.namespace');
330
    }
331
332 24
    public function queriesNamespace(): string
333
    {
334 24
        return $this->namespace() . 'Queries\\';
335
    }
336
337 1
    public function mutationsNamespace(): string
338
    {
339 1
        return $this->namespace() . 'Mutations\\';
340
    }
341
342 11
    public function typesNamespace(): string
343
    {
344 11
        return $this->namespace() . 'Types\\';
345
    }
346
347
    public function returnTypeIsLeaf(ResolveInfo $info): bool
348
    {
349
         $returnType = $info->returnType instanceof WrappingType
350
            ? $info->returnType->getWrappedType(true)
351
            : $info->returnType;
352
353
         return $returnType instanceof LeafType;
354
    }
355
356 28
    public function decorateResponse(array $data): array
357
    {
358 28
        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

358
        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...
359 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

359
            $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...
360
        }
361 28
        return $data;
362
    }
363
364 25
    protected function make(string $className)
365
    {
366 25
        if (array_key_exists($className, $this->classCache)) {
367 10
            return $this->classCache[$className];
368
        }
369
370 25
        $class = app()->has($className) || class_exists($className)
371 25
            ? app($className)
372 7
            : null;
373
374 24
        return $this->classCache[$className] = $class;
375
    }
376
}
377