Passed
Pull Request — master (#49)
by
unknown
04:16
created

HandlesGraphqlRequests::make()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

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

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

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

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

346
        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...
347 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

347
            $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...
348
        }
349 28
        return $data;
350
    }
351
352 25
    protected function make(string $className)
353
    {
354 25
        if (array_key_exists($className, $this->classCache)) {
355 10
            return $this->classCache[$className];
356
        }
357
358 25
        $class = app()->has($className) || class_exists($className)
359 25
            ? app($className)
360 7
            : null;
361
362 24
        return $this->classCache[$className] = $class;
363
    }
364
}
365