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

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

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