Passed
Pull Request — master (#43)
by
unknown
06:57
created

HandlesGraphqlRequests::beforeExecutionHook()   A

Complexity

Conditions 1
Paths 1

Size

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

319
        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...
320 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

320
            $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...
321
        }
322 26
        return $data;
323
    }
324
325 23
    protected function make(string $className)
326
    {
327 23
        if (array_key_exists($className, $this->classCache)) {
328 10
            return $this->classCache[$className];
329
        }
330
331 23
        $class = app()->has($className) || class_exists($className)
332 23
            ? app($className)
333 22
            : null;
334
335 22
        return $this->classCache[$className] = $class;
336
    }
337
}
338