Passed
Pull Request — master (#45)
by Markus
07:24 queued 18s
created

HandlesGraphqlRequests::beforeExecutionHook()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 7
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 4
crap 1
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 27
    public function __invoke(Request $request)
42
    {
43 27
        $this->classCache = [];
44 27
        $this->namespaceCache = null;
45
46 27
        $loader = app(DataLoader::class);
47
48 27
        $query = $request->input('query');
49 27
        $variables = $request->input('variables');
50 27
        $operationName = $request->input('operationName');
51
52
        try {
53 27
            $schema = BuildSchema::build($this->schema(), [$this, 'decorateTypeConfig']);
54
55 26
            $source = Parser::parse($query);
56
57 25
            $this->beforeExecutionHook($schema, $source, $operationName, $variables);
58
59
            /** @var \GraphQL\Executor\ExecutionResult */
60 25
            $result = null;
61
62 25
            GraphQL::promiseToExecute(
63 25
                app(PromiseAdapter::class),
64
                $schema,
65
                $source,
66 25
                null, // root
67 25
                compact('loader'), // context
68
                $variables,
69
                $operationName,
70 25
                [$this, 'resolveField'],
71 25
                null // validationRules
72 25
            )->then(function ($value) use (&$result) {
73 25
                $result = $value;
74 25
            });
75
76 25
            $loader->run();
77 2
        } catch (GraphqlError $e) {
78 2
            $result = new ExecutionResult(null, [$e]);
79
        }
80
81 27
        $result->setErrorFormatter([$this, 'errorFormatter']);
82
83 27
        return $this->decorateResponse($result->toArray($this->debugFlags()));
84
    }
85
86 24
    public function beforeExecutionHook(
87
        Schema $schema,
88
        DocumentNode $query,
89
        string $operationName = null,
90
        $variables = null
91
    ): void {
92 24
        return;
93
    }
94
95 12
    public function errorFormatter(GraphqlError $graphqlError)
96
    {
97 12
        $formattedError = FormattedError::createFromException($graphqlError);
98 12
        $throwable = $graphqlError->getPrevious();
99
100 12
        $this->reportException(
101 12
            $throwable instanceof Exception ? $throwable : $graphqlError
0 ignored issues
show
introduced by
$throwable is always a sub-type of Exception.
Loading history...
102
        );
103
104 12
        if ($throwable instanceof HttpException) {
105 1
            return array_merge($formattedError, [
106 1
                'message' => $throwable->getMessage(),
107
                'extensions' => [
108 1
                    'category' => 'client',
109 1
                    'code' => $throwable->getStatusCode(),
110
                ],
111
            ]);
112
        }
113
114 11
        if ($throwable instanceof ModelNotFoundException) {
115 1
            return array_merge($formattedError, [
116 1
                'message' => class_basename($throwable->getModel()) . ' not found.',
117
                'extensions' => [
118
                    'category' => 'client',
119
                    'code' => 404,
120
                ],
121
            ]);
122
        }
123
124 10
        if ($throwable instanceof ValidationException) {
125 1
            return array_merge($formattedError, [
126 1
                'message' => $throwable->getMessage(),
127
                'extensions' => [
128 1
                    'category' => 'validation',
129 1
                    'validation' => $throwable->errors(),
130
                ],
131
            ]);
132
        }
133
134 9
        return $formattedError;
135
    }
136
137 12
    public function reportException(Exception $exception)
138
    {
139 12
        app(ExceptionHandler::class)->report($exception);
140 12
    }
141
142 25
    public function schema()
143
    {
144 25
        return file_get_contents($this->schemaPath());
145
    }
146
147 25
    public function schemaPath()
148
    {
149 25
        return config('butler.graphql.schema');
150
    }
151
152 26
    public function decorateTypeConfig(array $config, TypeDefinitionNode $typeDefinitionNode)
153
    {
154 26
        if ($this->shouldDecorateWithResolveType($typeDefinitionNode)) {
155 23
            $config['resolveType'] = [$this, 'resolveType'];
156
        }
157 26
        return $config;
158
    }
159
160 26
    protected function shouldDecorateWithResolveType(TypeDefinitionNode $typeDefinitionNode)
161
    {
162 26
        return $typeDefinitionNode instanceof InterfaceTypeDefinitionNode
163 26
            || $typeDefinitionNode instanceof UnionTypeDefinitionNode;
164
    }
165
166 27
    public function debugFlags()
167
    {
168 27
        $flags = 0;
169 27
        if (config('butler.graphql.include_debug_message')) {
170 8
            $flags |= DebugFlag::INCLUDE_DEBUG_MESSAGE;
171
        }
172 27
        if (config('butler.graphql.include_trace')) {
173 3
            $flags |= DebugFlag::INCLUDE_TRACE;
174
        }
175 27
        return $flags;
176
    }
177
178 24
    public function resolveField($source, $args, $context, ResolveInfo $info)
179
    {
180 24
        $field = $this->fieldFromResolver($source, $args, $context, $info)
181 10
            ?? $this->fieldFromArray($source, $args, $context, $info)
182 15
            ?? $this->fieldFromObject($source, $args, $context, $info);
183
184 15
        return call(static function () use ($field, $source, $args, $context, $info) {
185 15
            return $field instanceof \Closure
186 1
                ? $field($source, $args, $context, $info)
187 15
                : $field;
188 15
        });
189
    }
190
191 4
    public function resolveType($source, $context, ResolveInfo $info)
192
    {
193 4
        return $this->typeFromArray($source, $context, $info)
194 4
            ?? $this->typeFromObject($source, $context, $info)
195 4
            ?? $this->typeFromParentResolver($source, $context, $info)
196 4
            ?? $this->typeFromBaseClass($source, $context, $info);
197
    }
198
199 24
    public function fieldFromResolver($source, $args, $context, ResolveInfo $info)
200
    {
201 24
        $className = $this->resolveClassName($info);
202 24
        $methodName = $this->resolveFieldMethodName($info);
203
204 24
        if ($resolver = $this->make($className)) {
205 23
            if (method_exists($resolver, $methodName)) {
206 23
                return $resolver->{$methodName}($source, $args, $context, $info);
207
            }
208
        }
209 10
    }
210
211 10
    public function fieldFromArray($source, $args, $context, ResolveInfo $info)
212
    {
213 10
        if (is_array($source) || $source instanceof \ArrayAccess) {
214 8
            return collect($this->propertyNames($info))
215 8
                ->map(function ($propertyName) use ($source) {
216 8
                    return $source[$propertyName] ?? null;
217 8
                })
218 8
                ->reject(function ($value) {
219 8
                    return is_null($value);
220 8
                })
221 8
                ->first();
222
        }
223 7
    }
224
225 7
    public function fieldFromObject($source, $args, $context, ResolveInfo $info)
226
    {
227 7
        if (is_object($source)) {
228 7
            return collect($this->propertyNames($info))
229 7
                ->map(function ($propertyName) use ($source) {
230 7
                    return $source->{$propertyName} ?? null;
231 7
                })
232 7
                ->reject(function ($value) {
233 7
                    return is_null($value);
234 7
                })
235 7
                ->first();
236
        }
237 1
    }
238
239 4
    public function typeFromArray($source, $context, ResolveInfo $info)
240
    {
241 4
        if (is_array($source) || $source instanceof \ArrayAccess) {
242 4
            return $source['__typename'] ?? null;
243
        }
244 2
    }
245
246 4
    public function typeFromObject($source, $context, ResolveInfo $info)
247
    {
248 4
        if (is_object($source)) {
249 2
            return $source->__typename ?? null;
250
        }
251 4
    }
252
253 4
    public function typeFromParentResolver($source, $context, ResolveInfo $info)
254
    {
255 4
        $className = $this->resolveClassName($info);
256 4
        $methodName = $this->resolveTypeMethodName($info);
257
258 4
        if ($resolver = $this->make($className)) {
259 4
            if (method_exists($resolver, $methodName)) {
260 4
                return $resolver->{$methodName}($source, $context, $info);
261
            }
262
        }
263
    }
264
265 1
    public function typeFromBaseClass($source, $context, ResolveInfo $info)
266
    {
267 1
        if (is_object($source)) {
268 1
            return class_basename($source);
269
        }
270
    }
271
272 10
    public function propertyNames(ResolveInfo $info): array
273
    {
274 10
        return collect([
275 10
            Str::snake($info->fieldName),
276 10
            Str::camel($info->fieldName),
277 10
            Str::kebab(Str::camel($info->fieldName)),
278 10
        ])->unique()->toArray();
279
    }
280
281 24
    protected function resolveClassName(ResolveInfo $info): string
282
    {
283 24
        if ($info->parentType->name === 'Query') {
284 23
            return $this->queriesNamespace() . Str::studly($info->fieldName);
285
        }
286
287 11
        if ($info->parentType->name === 'Mutation') {
288 1
            return $this->mutationsNamespace() . Str::studly($info->fieldName);
289
        }
290
291 11
        return $this->typesNamespace() . Str::studly($info->parentType->name);
292
    }
293
294 24
    public function resolveFieldMethodName(ResolveInfo $info): string
295
    {
296 24
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
297 24
            return '__invoke';
298
        }
299
300 11
        return Str::camel($info->fieldName);
301
    }
302
303 4
    public function resolveTypeMethodName(ResolveInfo $info): string
304
    {
305 4
        if (in_array($info->parentType->name, ['Query', 'Mutation'])) {
306 2
            return 'resolveType';
307
        }
308
309 2
        return 'resolveTypeFor' . ucfirst(Str::camel($info->fieldName));
310
    }
311
312 24
    public function namespace(): string
313
    {
314 24
        return $this->namespaceCache ?? $this->namespaceCache = config('butler.graphql.namespace');
315
    }
316
317 23
    public function queriesNamespace(): string
318
    {
319 23
        return $this->namespace() . 'Queries\\';
320
    }
321
322 1
    public function mutationsNamespace(): string
323
    {
324 1
        return $this->namespace() . 'Mutations\\';
325
    }
326
327 11
    public function typesNamespace(): string
328
    {
329 11
        return $this->namespace() . 'Types\\';
330
    }
331
332 27
    public function decorateResponse(array $data): array
333
    {
334 27
        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

334
        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...
335 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

335
            $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...
336
        }
337 27
        return $data;
338
    }
339
340 24
    protected function make(string $className)
341
    {
342 24
        if (array_key_exists($className, $this->classCache)) {
343 10
            return $this->classCache[$className];
344
        }
345
346 24
        $class = app()->has($className) || class_exists($className)
347 24
            ? app($className)
348 23
            : null;
349
350 23
        return $this->classCache[$className] = $class;
351
    }
352
}
353