Test Failed
Push — master ( 761758...65c987 )
by
unknown
07:29
created

HandlesGraphqlRequests::resolveFieldMethodName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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

322
        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...
323
            $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

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