Passed
Push — master ( 73b946...893eb4 )
by
unknown
09:22
created

HandlesGraphqlRequests::__invoke()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 1

Importance

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

297
        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...
298 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

298
            $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...
299
        }
300 24
        return $data;
301
    }
302
303 23
    protected function make(string $className)
304
    {
305 23
        if (array_key_exists($className, $this->classCache)) {
306 10
            return $this->classCache[$className];
307
        }
308
309 23
        $class = app()->has($className) || class_exists($className)
310 23
            ? app($className)
311 22
            : null;
312
313 22
        return $this->classCache[$className] = $class;
314
    }
315
}
316