Test Failed
Pull Request — master (#42)
by
unknown
12:11 queued 04:14
created

HandlesGraphqlRequests::schemaPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

302
        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...
303
            $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

303
            $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...
304
        }
305
        return $data;
306
    }
307
308
    protected function make(string $className)
309
    {
310
        if (array_key_exists($className, $this->classCache)) {
311
            return $this->classCache[$className];
312
        }
313
314
        $class = app()->has($className) || class_exists($className)
315
            ? app($className)
316
            : null;
317
318
        return $this->classCache[$className] = $class;
319
    }
320
}
321