Passed
Pull Request — master (#33)
by
unknown
04:15
created

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

290
        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...
291 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

291
            $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...
292
        }
293 24
        return $data;
294
    }
295
}
296