Passed
Pull Request — master (#52)
by Markus
11:50
created

configureEloquentStrictness()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 3
nc 2
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\Model;
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 27
            $disabledEloquentStrictness = $this->disableEloquentStrictness();
63
64
            /** @var \GraphQL\Executor\ExecutionResult */
65 27
            $result = null;
66
67 27
            GraphQL::promiseToExecute(
68 27
                app(PromiseAdapter::class),
69
                $schema,
70
                $source,
71
                null, // root
72 27
                compact('loader'), // context
73
                $variables,
74
                $operationName,
75 27
                [$this, 'resolveField'],
76
                null // validationRules
77 27
            )->then(function ($value) use (&$result) {
78 27
                $result = $value;
79
            });
80
81 27
            $loader->run();
82 2
        } catch (GraphqlError $e) {
83 2
            $result = new ExecutionResult(null, [$e]);
84 29
        } finally {
85 29
            $this->configureEloquentStrictness($disabledEloquentStrictness ?? false);
86
        }
87
88 29
        $result->setErrorFormatter([$this, 'errorFormatter']);
89
90 29
        return $this->decorateResponse($result->toArray($this->debugFlags()));
91
    }
92
93 26
    public function beforeExecutionHook(
94
        Schema $schema,
95
        DocumentNode $query,
96
        string $operationName = null,
97
        $variables = null
98
    ): void {
99 26
        return;
100
    }
101
102 27
    private function disableEloquentStrictness(): bool
103
    {
104
        if (
105 27
            method_exists(Model::class, 'preventsAccessingMissingAttributes') &&
106 27
            Model::preventsAccessingMissingAttributes()
107
        ) {
108 2
            Model::preventAccessingMissingAttributes(false);
109
110 2
            return true;
111
        }
112
113 25
        return false;
114
    }
115
116 29
    private function configureEloquentStrictness(bool $value): void
117
    {
118 29
        if ($value && method_exists(Model::class, 'preventsAccessingMissingAttributes')) {
119 2
            Model::preventAccessingMissingAttributes();
120
        }
121
    }
122
123 13
    public function errorFormatter(GraphqlError $graphqlError)
124
    {
125 13
        $formattedError = FormattedError::createFromException($graphqlError);
126 13
        $throwable = $graphqlError->getPrevious();
127
128 13
        $this->reportException(
129 13
            $throwable instanceof Exception ? $throwable : $graphqlError
0 ignored issues
show
introduced by
$throwable is always a sub-type of Exception.
Loading history...
130
        );
131
132
        if (
133 13
            $throwable instanceof HttpException &&
134 13
            $throwable->getStatusCode() >= 400 &&
135 13
            $throwable->getStatusCode() < 500
136
        ) {
137 1
            return array_merge($formattedError, [
138 1
                'message' => $throwable->getMessage(),
139
                'extensions' => [
140 1
                    'category' => 'client',
141 1
                    'code' => $throwable->getStatusCode(),
142
                ],
143
            ]);
144
        }
145
146 12
        if ($throwable instanceof ModelNotFoundException) {
147 1
            return array_merge($formattedError, [
148 1
                'message' => class_basename($throwable->getModel()) . ' not found.',
149
                'extensions' => [
150
                    'category' => 'client',
151
                    'code' => 404,
152
                ],
153
            ]);
154
        }
155
156 11
        if ($throwable instanceof ValidationException) {
157 1
            return array_merge($formattedError, [
158 1
                'message' => $throwable->getMessage(),
159
                'extensions' => [
160 1
                    'category' => 'validation',
161 1
                    'validation' => $throwable->errors(),
162
                ],
163
            ]);
164
        }
165
166 10
        return $formattedError;
167
    }
168
169 13
    public function reportException(Exception $exception)
170
    {
171 13
        app(ExceptionHandler::class)->report($exception);
172
    }
173
174 27
    public function schema()
175
    {
176 27
        return file_get_contents($this->schemaPath());
177
    }
178
179 27
    public function schemaPath()
180
    {
181 27
        return config('butler.graphql.schema');
182
    }
183
184 28
    public function decorateTypeConfig(array $config, TypeDefinitionNode $typeDefinitionNode)
185
    {
186 28
        if ($this->shouldDecorateWithResolveType($typeDefinitionNode)) {
187 25
            $config['resolveType'] = [$this, 'resolveType'];
188
        }
189 28
        return $config;
190
    }
191
192 28
    protected function shouldDecorateWithResolveType(TypeDefinitionNode $typeDefinitionNode)
193
    {
194 28
        return $typeDefinitionNode instanceof InterfaceTypeDefinitionNode
195
            || $typeDefinitionNode instanceof UnionTypeDefinitionNode;
196
    }
197
198 29
    public function debugFlags()
199
    {
200 29
        $flags = 0;
201 29
        if (config('butler.graphql.include_debug_message')) {
202 8
            $flags |= DebugFlag::INCLUDE_DEBUG_MESSAGE;
203
        }
204 29
        if (config('butler.graphql.include_trace')) {
205 3
            $flags |= DebugFlag::INCLUDE_TRACE;
206
        }
207 29
        return $flags;
208
    }
209
210 26
    public function resolveField($source, $args, $context, ResolveInfo $info)
211
    {
212 26
        $field = $this->fieldFromResolver($source, $args, $context, $info)
213 11
            ?? $this->fieldFromArray($source, $args, $context, $info)
214 11
            ?? $this->fieldFromObject($source, $args, $context, $info);
215
216 16
        if ($this->fieldIsBackedEnum($field) && $this->returnTypeIsLeaf($info)) {
217
            $field = $field->value;
218
        }
219
220 16
        return call(static function () use ($field, $source, $args, $context, $info) {
221 16
            return $field instanceof \Closure
222 1
                ? $field($source, $args, $context, $info)
223 16
                : $field;
224
        });
225
    }
226
227 4
    public function resolveType($source, $context, ResolveInfo $info)
228
    {
229 4
        return $this->typeFromArray($source, $context, $info)
230 4
            ?? $this->typeFromObject($source, $context, $info)
231 4
            ?? $this->typeFromParentResolver($source, $context, $info)
232 4
            ?? $this->typeFromBaseClass($source, $context, $info);
233
    }
234
235 26
    public function fieldFromResolver($source, $args, $context, ResolveInfo $info)
236
    {
237 26
        $className = $this->resolveClassName($info);
238 26
        $methodName = $this->resolveFieldMethodName($info);
239
240 26
        if ($resolver = $this->make($className)) {
241 25
            if (method_exists($resolver, $methodName)) {
242 25
                return $resolver->{$methodName}($source, $args, $context, $info);
243
            }
244
        }
245
    }
246
247 11
    public function fieldFromArray($source, $args, $context, ResolveInfo $info)
248
    {
249 11
        if (is_array($source) || $source instanceof \ArrayAccess) {
250 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

250
            return collect(/** @scrutinizer ignore-type */ $this->propertyNames($info))
Loading history...
251 9
                ->map(function ($propertyName) use ($source) {
252 9
                    return $source[$propertyName] ?? null;
253
                })
254 9
                ->reject(function ($value) {
255 9
                    return is_null($value);
256
                })
257 9
                ->first();
258
        }
259
    }
260
261 7
    public function fieldFromObject($source, $args, $context, ResolveInfo $info)
262
    {
263 7
        if (is_object($source)) {
264 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

264
            return collect(/** @scrutinizer ignore-type */ $this->propertyNames($info))
Loading history...
265 7
                ->map(function ($propertyName) use ($source) {
266 7
                    return $source->{$propertyName} ?? null;
267
                })
268 7
                ->reject(function ($value) {
269 7
                    return is_null($value);
270
                })
271 7
                ->first();
272
        }
273
    }
274
275 16
    public function fieldIsBackedEnum($field): bool
276
    {
277 16
        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...
278
    }
279
280 4
    public function typeFromArray($source, $context, ResolveInfo $info)
281
    {
282 4
        if (is_array($source) || $source instanceof \ArrayAccess) {
283 4
            return $source['__typename'] ?? null;
284
        }
285
    }
286
287 4
    public function typeFromObject($source, $context, ResolveInfo $info)
288
    {
289 4
        if (is_object($source)) {
290 2
            return $source->__typename ?? null;
291
        }
292
    }
293
294 4
    public function typeFromParentResolver($source, $context, ResolveInfo $info)
295
    {
296 4
        $className = $this->resolveClassName($info);
297 4
        $methodName = $this->resolveTypeMethodName($info);
298
299 4
        if ($resolver = $this->make($className)) {
300 4
            if (method_exists($resolver, $methodName)) {
301 4
                return $resolver->{$methodName}($source, $context, $info);
302
            }
303
        }
304
    }
305
306 1
    public function typeFromBaseClass($source, $context, ResolveInfo $info)
307
    {
308 1
        if (is_object($source)) {
309 1
            return class_basename($source);
310
        }
311
    }
312
313 11
    public function propertyNames(ResolveInfo $info): array
314
    {
315 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

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