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\LeafType; |
||||
19 | use GraphQL\Type\Definition\ResolveInfo; |
||||
20 | use GraphQL\Type\Definition\WrappingType; |
||||
21 | use GraphQL\Type\Schema; |
||||
22 | use GraphQL\Utils\BuildSchema; |
||||
23 | use Illuminate\Contracts\Debug\ExceptionHandler; |
||||
24 | use Illuminate\Database\Eloquent\MissingAttributeException; |
||||
25 | use Illuminate\Database\Eloquent\ModelNotFoundException; |
||||
26 | use Illuminate\Http\Request; |
||||
27 | use Illuminate\Support\Str; |
||||
28 | use Illuminate\Validation\ValidationException; |
||||
29 | use Symfony\Component\HttpKernel\Exception\HttpException; |
||||
30 | |||||
31 | use function Amp\call; |
||||
32 | |||||
33 | trait HandlesGraphqlRequests |
||||
34 | { |
||||
35 | private $classCache; |
||||
36 | private $namespaceCache; |
||||
37 | |||||
38 | /** |
||||
39 | * Invoke the Graphql request handler. |
||||
40 | * |
||||
41 | * @param \Illuminate\Http\Request $request |
||||
42 | * @return array |
||||
43 | */ |
||||
44 | 30 | public function __invoke(Request $request) |
|||
45 | { |
||||
46 | 30 | $this->classCache = []; |
|||
47 | 30 | $this->namespaceCache = null; |
|||
48 | |||||
49 | 30 | $loader = app(DataLoader::class); |
|||
50 | |||||
51 | 30 | $query = $request->input('query'); |
|||
52 | 30 | $variables = $request->input('variables'); |
|||
53 | 30 | $operationName = $request->input('operationName'); |
|||
54 | |||||
55 | try { |
||||
56 | 30 | $schema = BuildSchema::build($this->schema(), [$this, 'decorateTypeConfig']); |
|||
57 | |||||
58 | 29 | $source = Parser::parse($query); |
|||
59 | |||||
60 | 28 | $this->beforeExecutionHook($schema, $source, $operationName, $variables); |
|||
61 | |||||
62 | /** @var \GraphQL\Executor\ExecutionResult */ |
||||
63 | 28 | $result = null; |
|||
64 | |||||
65 | 28 | GraphQL::promiseToExecute( |
|||
66 | 28 | app(PromiseAdapter::class), |
|||
67 | $schema, |
||||
68 | $source, |
||||
69 | null, // root |
||||
70 | 28 | compact('loader'), // context |
|||
71 | $variables, |
||||
72 | $operationName, |
||||
73 | 28 | [$this, 'resolveField'], |
|||
74 | null // validationRules |
||||
75 | 28 | )->then(function ($value) use (&$result) { |
|||
76 | 28 | $result = $value; |
|||
77 | }); |
||||
78 | |||||
79 | 28 | $loader->run(); |
|||
80 | 2 | } catch (GraphqlError $e) { |
|||
81 | 2 | $result = new ExecutionResult(null, [$e]); |
|||
82 | } |
||||
83 | |||||
84 | 30 | $result->setErrorFormatter([$this, 'errorFormatter']); |
|||
85 | |||||
86 | 30 | return $this->decorateResponse($result->toArray($this->debugFlags())); |
|||
87 | } |
||||
88 | |||||
89 | 27 | public function beforeExecutionHook( |
|||
90 | Schema $schema, |
||||
91 | DocumentNode $query, |
||||
92 | string $operationName = null, |
||||
93 | $variables = null |
||||
94 | ): void { |
||||
95 | 27 | return; |
|||
96 | } |
||||
97 | |||||
98 | 13 | public function errorFormatter(GraphqlError $graphqlError) |
|||
99 | { |
||||
100 | 13 | $formattedError = FormattedError::createFromException($graphqlError); |
|||
101 | 13 | $throwable = $graphqlError->getPrevious(); |
|||
102 | |||||
103 | 13 | $this->reportException( |
|||
104 | 13 | $throwable instanceof Exception ? $throwable : $graphqlError |
|||
0 ignored issues
–
show
introduced
by
![]() |
|||||
105 | ); |
||||
106 | |||||
107 | if ( |
||||
108 | 13 | $throwable instanceof HttpException && |
|||
109 | 13 | $throwable->getStatusCode() >= 400 && |
|||
110 | 13 | $throwable->getStatusCode() < 500 |
|||
111 | ) { |
||||
112 | 1 | return array_merge($formattedError, [ |
|||
113 | 1 | 'message' => $throwable->getMessage(), |
|||
114 | 'extensions' => [ |
||||
115 | 1 | 'category' => 'client', |
|||
116 | 1 | 'code' => $throwable->getStatusCode(), |
|||
117 | ], |
||||
118 | ]); |
||||
119 | } |
||||
120 | |||||
121 | 12 | if ($throwable instanceof ModelNotFoundException) { |
|||
122 | 1 | return array_merge($formattedError, [ |
|||
123 | 1 | 'message' => class_basename($throwable->getModel()) . ' not found.', |
|||
124 | 'extensions' => [ |
||||
125 | 'category' => 'client', |
||||
126 | 'code' => 404, |
||||
127 | ], |
||||
128 | ]); |
||||
129 | } |
||||
130 | |||||
131 | 11 | if ($throwable instanceof ValidationException) { |
|||
132 | 1 | return array_merge($formattedError, [ |
|||
133 | 1 | 'message' => $throwable->getMessage(), |
|||
134 | 'extensions' => [ |
||||
135 | 1 | 'category' => 'validation', |
|||
136 | 1 | 'validation' => $throwable->errors(), |
|||
137 | ], |
||||
138 | ]); |
||||
139 | } |
||||
140 | |||||
141 | 10 | return $formattedError; |
|||
142 | } |
||||
143 | |||||
144 | 13 | public function reportException(Exception $exception) |
|||
145 | { |
||||
146 | 13 | app(ExceptionHandler::class)->report($exception); |
|||
147 | } |
||||
148 | |||||
149 | 28 | public function schema() |
|||
150 | { |
||||
151 | 28 | return file_get_contents($this->schemaPath()); |
|||
152 | } |
||||
153 | |||||
154 | 28 | public function schemaPath() |
|||
155 | { |
||||
156 | 28 | return config('butler.graphql.schema'); |
|||
157 | } |
||||
158 | |||||
159 | 29 | public function decorateTypeConfig(array $config, TypeDefinitionNode $typeDefinitionNode) |
|||
160 | { |
||||
161 | 29 | if ($this->shouldDecorateWithResolveType($typeDefinitionNode)) { |
|||
162 | 26 | $config['resolveType'] = [$this, 'resolveType']; |
|||
163 | } |
||||
164 | 29 | return $config; |
|||
165 | } |
||||
166 | |||||
167 | 29 | protected function shouldDecorateWithResolveType(TypeDefinitionNode $typeDefinitionNode) |
|||
168 | { |
||||
169 | 29 | return $typeDefinitionNode instanceof InterfaceTypeDefinitionNode |
|||
170 | || $typeDefinitionNode instanceof UnionTypeDefinitionNode; |
||||
171 | } |
||||
172 | |||||
173 | 30 | public function debugFlags() |
|||
174 | { |
||||
175 | 30 | $flags = 0; |
|||
176 | 30 | if (config('butler.graphql.include_debug_message')) { |
|||
177 | 8 | $flags |= DebugFlag::INCLUDE_DEBUG_MESSAGE; |
|||
178 | } |
||||
179 | 30 | if (config('butler.graphql.include_trace')) { |
|||
180 | 3 | $flags |= DebugFlag::INCLUDE_TRACE; |
|||
181 | } |
||||
182 | 30 | return $flags; |
|||
183 | } |
||||
184 | |||||
185 | 27 | public function resolveField($source, $args, $context, ResolveInfo $info) |
|||
186 | { |
||||
187 | 27 | if ($resolver = $this->resolverForField($info)) { |
|||
188 | 26 | $field = $resolver($source, $args, $context, $info); |
|||
189 | } else { |
||||
190 | 11 | $field = $this->fieldFromArray($source, $args, $context, $info) |
|||
191 | 7 | ?? $this->fieldFromObject($source, $args, $context, $info); |
|||
192 | } |
||||
193 | |||||
194 | 17 | if ($this->fieldIsBackedEnum($field) && $this->returnTypeIsLeaf($info)) { |
|||
195 | $field = $field->value; |
||||
196 | } |
||||
197 | |||||
198 | 17 | return call(static function () use ($field, $source, $args, $context, $info) { |
|||
199 | 17 | return $field instanceof \Closure |
|||
200 | 1 | ? $field($source, $args, $context, $info) |
|||
201 | 17 | : $field; |
|||
202 | }); |
||||
203 | } |
||||
204 | |||||
205 | 4 | public function resolveType($source, $context, ResolveInfo $info) |
|||
206 | { |
||||
207 | 4 | return $this->typeFromArray($source, $context, $info) |
|||
208 | 4 | ?? $this->typeFromObject($source, $context, $info) |
|||
209 | 4 | ?? $this->typeFromParentResolver($source, $context, $info) |
|||
210 | 4 | ?? $this->typeFromBaseClass($source, $context, $info); |
|||
211 | } |
||||
212 | |||||
213 | 27 | public function resolverForField(ResolveInfo $info) |
|||
214 | { |
||||
215 | 27 | $className = $this->resolveClassName($info); |
|||
216 | 27 | $methodName = $this->resolveFieldMethodName($info); |
|||
217 | |||||
218 | 27 | if ($resolver = $this->make($className)) { |
|||
219 | 26 | if (method_exists($resolver, $methodName)) { |
|||
220 | 26 | return fn (...$args) => $resolver->{$methodName}(...$args); |
|||
221 | } |
||||
222 | } |
||||
223 | } |
||||
224 | |||||
225 | 11 | public function fieldFromArray($source, $args, $context, ResolveInfo $info) |
|||
226 | { |
||||
227 | 11 | if (is_array($source) || $source instanceof \ArrayAccess) { |
|||
228 | 9 | return collect($this->propertyNames($info)) |
|||
0 ignored issues
–
show
$this->propertyNames($info) of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
229 | 9 | ->map(function ($propertyName) use ($source) { |
|||
230 | try { |
||||
231 | 9 | return $source[$propertyName] ?? null; |
|||
232 | } catch (MissingAttributeException) { |
||||
233 | return null; |
||||
234 | } |
||||
235 | }) |
||||
236 | 9 | ->reject(function ($value) { |
|||
237 | 9 | return is_null($value); |
|||
238 | }) |
||||
239 | 9 | ->first(); |
|||
240 | } |
||||
241 | } |
||||
242 | |||||
243 | 7 | public function fieldFromObject($source, $args, $context, ResolveInfo $info) |
|||
244 | { |
||||
245 | 7 | if (is_object($source)) { |
|||
246 | 7 | return collect($this->propertyNames($info)) |
|||
0 ignored issues
–
show
$this->propertyNames($info) of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
247 | 7 | ->map(function ($propertyName) use ($source) { |
|||
248 | try { |
||||
249 | 7 | return $source->{$propertyName} ?? null; |
|||
250 | } catch (MissingAttributeException) { |
||||
0 ignored issues
–
show
catch (\Illuminate\Datab...singAttributeException) is not reachable.
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed. Unreachable code is most often the result of function fx() {
try {
doSomething();
return true;
}
catch (\Exception $e) {
return false;
}
return false;
}
In the above example, the last ![]() |
|||||
251 | return null; |
||||
252 | } |
||||
253 | }) |
||||
254 | 7 | ->reject(function ($value) { |
|||
255 | 7 | return is_null($value); |
|||
256 | }) |
||||
257 | 7 | ->first(); |
|||
258 | } |
||||
259 | } |
||||
260 | |||||
261 | 17 | public function fieldIsBackedEnum($field): bool |
|||
262 | { |
||||
263 | 17 | return function_exists('enum_exists') && $field instanceof \BackedEnum; |
|||
0 ignored issues
–
show
The type
BackedEnum was not found. Maybe you did not declare it correctly or list all dependencies?
The issue could also be caused by a filter entry in the build configuration.
If the path has been excluded in your configuration, e.g. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||
264 | } |
||||
265 | |||||
266 | 4 | public function typeFromArray($source, $context, ResolveInfo $info) |
|||
267 | { |
||||
268 | 4 | if (is_array($source) || $source instanceof \ArrayAccess) { |
|||
269 | 4 | return $source['__typename'] ?? null; |
|||
270 | } |
||||
271 | } |
||||
272 | |||||
273 | 4 | public function typeFromObject($source, $context, ResolveInfo $info) |
|||
274 | { |
||||
275 | 4 | if (is_object($source)) { |
|||
276 | 2 | return $source->__typename ?? null; |
|||
277 | } |
||||
278 | } |
||||
279 | |||||
280 | 4 | public function typeFromParentResolver($source, $context, ResolveInfo $info) |
|||
281 | { |
||||
282 | 4 | $className = $this->resolveClassName($info); |
|||
283 | 4 | $methodName = $this->resolveTypeMethodName($info); |
|||
284 | |||||
285 | 4 | if ($resolver = $this->make($className)) { |
|||
286 | 4 | if (method_exists($resolver, $methodName)) { |
|||
287 | 4 | return $resolver->{$methodName}($source, $context, $info); |
|||
288 | } |
||||
289 | } |
||||
290 | } |
||||
291 | |||||
292 | 1 | public function typeFromBaseClass($source, $context, ResolveInfo $info) |
|||
293 | { |
||||
294 | 1 | if (is_object($source)) { |
|||
295 | 1 | return class_basename($source); |
|||
296 | } |
||||
297 | } |
||||
298 | |||||
299 | 11 | public function propertyNames(ResolveInfo $info): array |
|||
300 | { |
||||
301 | 11 | return collect([ |
|||
0 ignored issues
–
show
array(Illuminate\Support...mel($info->fieldName))) of type array<integer,string> is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
302 | 11 | Str::snake($info->fieldName), |
|||
303 | 11 | Str::camel($info->fieldName), |
|||
304 | 11 | Str::kebab(Str::camel($info->fieldName)), |
|||
305 | 11 | ])->unique()->toArray(); |
|||
306 | } |
||||
307 | |||||
308 | 27 | protected function resolveClassName(ResolveInfo $info): string |
|||
309 | { |
||||
310 | 27 | if ($info->parentType->name === 'Query') { |
|||
311 | 26 | return $this->queriesNamespace() . Str::studly($info->fieldName); |
|||
312 | } |
||||
313 | |||||
314 | 13 | if ($info->parentType->name === 'Mutation') { |
|||
315 | 1 | return $this->mutationsNamespace() . Str::studly($info->fieldName); |
|||
316 | } |
||||
317 | |||||
318 | 13 | return $this->typesNamespace() . Str::studly($info->parentType->name); |
|||
319 | } |
||||
320 | |||||
321 | 27 | public function resolveFieldMethodName(ResolveInfo $info): string |
|||
322 | { |
||||
323 | 27 | if (in_array($info->parentType->name, ['Query', 'Mutation'])) { |
|||
324 | 27 | return '__invoke'; |
|||
325 | } |
||||
326 | |||||
327 | 13 | return Str::camel($info->fieldName); |
|||
328 | } |
||||
329 | |||||
330 | 4 | public function resolveTypeMethodName(ResolveInfo $info): string |
|||
331 | { |
||||
332 | 4 | if (in_array($info->parentType->name, ['Query', 'Mutation'])) { |
|||
333 | 2 | return 'resolveType'; |
|||
334 | } |
||||
335 | |||||
336 | 2 | return 'resolveTypeFor' . ucfirst(Str::camel($info->fieldName)); |
|||
337 | } |
||||
338 | |||||
339 | 27 | public function namespace(): string |
|||
340 | { |
||||
341 | 27 | return $this->namespaceCache ?? $this->namespaceCache = config('butler.graphql.namespace'); |
|||
342 | } |
||||
343 | |||||
344 | 26 | public function queriesNamespace(): string |
|||
345 | { |
||||
346 | 26 | return $this->namespace() . 'Queries\\'; |
|||
347 | } |
||||
348 | |||||
349 | 1 | public function mutationsNamespace(): string |
|||
350 | { |
||||
351 | 1 | return $this->namespace() . 'Mutations\\'; |
|||
352 | } |
||||
353 | |||||
354 | 13 | public function typesNamespace(): string |
|||
355 | { |
||||
356 | 13 | return $this->namespace() . 'Types\\'; |
|||
357 | } |
||||
358 | |||||
359 | public function returnTypeIsLeaf(ResolveInfo $info): bool |
||||
360 | { |
||||
361 | $returnType = $info->returnType instanceof WrappingType |
||||
362 | ? $info->returnType->getWrappedType(true) |
||||
363 | : $info->returnType; |
||||
364 | |||||
365 | return $returnType instanceof LeafType; |
||||
366 | } |
||||
367 | |||||
368 | 30 | public function decorateResponse(array $data): array |
|||
369 | { |
||||
370 | 30 | if (app()->bound('debugbar') && app('debugbar')->isEnabled()) { |
|||
371 | 1 | $data['debug'] = app('debugbar')->getData(); |
|||
372 | } |
||||
373 | 30 | return $data; |
|||
374 | } |
||||
375 | |||||
376 | 27 | protected function make(string $className) |
|||
377 | { |
||||
378 | 27 | if (array_key_exists($className, $this->classCache)) { |
|||
379 | 12 | return $this->classCache[$className]; |
|||
380 | } |
||||
381 | |||||
382 | 27 | $class = app()->has($className) || class_exists($className) |
|||
383 | 27 | ? app($className) |
|||
384 | 8 | : null; |
|||
385 | |||||
386 | 26 | return $this->classCache[$className] = $class; |
|||
387 | } |
||||
388 | } |
||||
389 |