Passed
Push — master ( 3ff273...94f211 )
by Andrea Marco
14:46 queued 20s
created

JsonApiError::shouldHandleRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cerbero\JsonApiError;
6
7
use Cerbero\JsonApiError\Contracts\JsonApiErrorsAware;
8
use Cerbero\JsonApiError\Contracts\JsonApiSafe;
9
use Cerbero\JsonApiError\Data\JsonApiErrorData;
10
use Cerbero\JsonApiError\Exceptions\InvalidHandlerException;
11
use Cerbero\JsonApiError\Exceptions\JsonApiException;
12
use Cerbero\JsonApiError\Exceptions\NoErrorsException;
13
use Closure;
14
use Illuminate\Http\JsonResponse;
15
use Illuminate\Http\Request;
16
use Illuminate\Support\Arr;
17
use Illuminate\Support\Reflector;
18
use Illuminate\Validation\ValidationException;
19
use ReflectionFunction;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
22
use Throwable;
23
24
/**
25
 * The entry-point to render JSON:API compliant errors.
26
 */
27
final class JsonApiError
28
{
29
    /**
30
     * The map between throwables and handlers.
31
     *
32
     * @var array<class-string, callable>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, callable> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, callable>.
Loading history...
33
     */
34
    private static array $handlersMap = [
35
        \Cerbero\JsonApiError\Contracts\JsonApiErrorsAware::class => [self::class, 'fromJsonApiErrorsAware'],
36
        \Cerbero\JsonApiError\Contracts\JsonApiSafe::class => [self::class, 'fromJsonApiSafe'],
37
        \Illuminate\Validation\ValidationException::class => [self::class, 'fromValidation'],
38
        \Symfony\Component\HttpKernel\Exception\HttpException::class => [self::class, 'fromHttpException'],
39
    ];
40
41
    /**
42
     * The map between throwables and HTTP statuses.
43
     *
44
     * @var array<class-string, int>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, int> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, int>.
Loading history...
45
     */
46
    private static array $statusesMap = [
47
        \Illuminate\Auth\Access\AuthorizationException::class => 403,
48
        \Illuminate\Auth\AuthenticationException::class => 401,
49
        \Illuminate\Database\Eloquent\ModelNotFoundException::class => 404,
50
        \Illuminate\Validation\UnauthorizedException::class => 403,
51
    ];
52
53
    /**
54
     * The user-defined logic to determine whether the current request should be handled.
55
     *
56
     * @var ?Closure(Request): bool
57
     */
58
    private static ?Closure $shouldHandleRequest = null;
59
60
    /**
61
     * The user-defined data to merge with all JSON:API errors.
62
     *
63
     * @var ?Closure(): JsonApiErrorData
64
     */
65
    private static ?Closure $merge = null;
66
67
    /**
68
     * The JSON:API errors.
69
     *
70
     * @var JsonApiErrorData[] $errors
71
     */
72
    public readonly array $errors;
73
74
    /**
75
     * Instantiate the class.
76
     */
77 12
    public function __construct(JsonApiErrorData ...$errors)
78
    {
79 12
        $this->errors = match (true) {
80 12
            empty($errors) => throw new NoErrorsException(),
81 12
            is_null($merge = self::$merge) => $errors,
82 12
            default => array_map(fn(JsonApiErrorData $error) => $error->merge($merge()), $errors),
83 12
        };
84
    }
85
86
    /**
87
     * Define a custom logic to determine whether the given request should be handled.
88
     *
89
     * @param Closure(Request): bool $callback
90
     */
91
    public static function shouldHandleRequest(Closure $callback): void
92
    {
93
        self::$shouldHandleRequest = $callback;
94
    }
95
96
    /**
97
     * Determine whether the given request should be handled.
98
     */
99 11
    public static function handlesRequest(Request $request): bool
100
    {
101 11
        return self::$shouldHandleRequest ? (self::$shouldHandleRequest)($request) : $request->expectsJson();
102
    }
103
104
    /**
105
     * Define a custom handler to turn the given throwable into a JSON:API error.
106
     *
107
     * @param Closure(Throwable): (JsonApiErrorData|JsonApiErrorData[]) $handler
108
     */
109 2
    public static function handle(Closure $handler): void
110
    {
111 2
        $parameters = (new ReflectionFunction($handler))->getParameters();
112
        /** @var class-string[] */
113 2
        $types = empty($parameters) ? [''] : (Reflector::getParameterClassNames($parameters[0]) ?: ['']);
114
115 2
        foreach ($types as $type) {
116 2
            throw_unless(is_subclass_of($type, Throwable::class), InvalidHandlerException::class);
117
118 1
            self::$handlersMap[$type] = fn(Throwable $e) => new self(...Arr::wrap($handler($e)));
119
        }
120
    }
121
122
    /**
123
     * Map the given throwable to the provided HTTP status.
124
     *
125
     * @param class-string $throwable
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
126
     */
127 1
    public static function mapToStatus(string $throwable, int $status): void
128
    {
129 1
        self::$statusesMap[$throwable] = $status;
130
    }
131
132
    /**
133
     * Define custom data to merge with all JSON:API errors.
134
     *
135
     * @param Closure(): JsonApiErrorData $callback
136
     */
137 1
    public static function merge(Closure $callback): void
138
    {
139 1
        self::$merge = $callback;
140
    }
141
142
    /**
143
     * Instantiate the class from the given raw error.
144
     */
145 4
    public static function raw(?string $detail = null, int $status = Response::HTTP_BAD_REQUEST): self
146
    {
147 4
        return new self(new JsonApiErrorData($detail, $status));
148
    }
149
150
    /**
151
     * Instantiate the class from the given throwable.
152
     */
153 10
    public static function from(Throwable $e): self
154
    {
155
        /** @var ?callable */
156 10
        $handler = Arr::first(self::$handlersMap, fn(callable $h, string $class) => is_a($e, $class));
157
158 10
        return match (true) {
159 10
            $handler !== null => $handler($e),
160 10
            isset(self::$statusesMap[$e::class]) => self::fromStatus(self::$statusesMap[$e::class]),
161 10
            default => self::unexpected($e),
162 10
        };
163
    }
164
165
    /**
166
     * Instantiate the class from the given HTTP status.
167
     */
168 6
    public static function fromStatus(int $status): self
169
    {
170 6
        return new self(new JsonApiErrorData(status: $status));
171
    }
172
173
    /**
174
     * Instantiate the class from the given unexpected throwable.
175
     */
176 1
    public static function unexpected(Throwable $e): self
177
    {
178 1
        return new self(JsonApiErrorData::unexpected($e));
179
    }
180
181
    /**
182
     * Instantiate the class from the given instance aware of JSON:API errors.
183
     */
184
    public static function fromJsonApiErrorsAware(JsonApiErrorsAware $instance): self
185
    {
186
        return new self(...$instance->jsonApiErrors());
187
    }
188
189
    /**
190
     * Instantiate the class from the given JSON:API renderable.
191
     */
192
    public static function fromJsonApiSafe(JsonApiSafe $e): self
193
    {
194
        return new self(new JsonApiErrorData($e->getMessage(), max($e->getCode(), Response::HTTP_BAD_REQUEST)));
195
    }
196
197
    /**
198
     * Instantiate the class from the given validation exception.
199
     */
200 2
    public static function fromValidation(ValidationException $e): self
201
    {
202 2
        $errors = [];
203
204 2
        foreach ($e->errors() as $dot => $details) {
205 2
            foreach ($details as $detail) {
206 2
                $errors[] = JsonApiErrorData::unprocessable($detail, $dot);
207
            }
208
        }
209
210 2
        return new self(...$errors);
211
    }
212
213
    /**
214
     * Instantiate the class from the given HTTP exception.
215
     */
216 3
    public static function fromHttpException(HttpExceptionInterface $e): self
217
    {
218 3
        return self::fromStatus($e->getStatusCode());
219
    }
220
221
    /**
222
     * Retrieve the HTTP response containing the JSON:API errors.
223
     */
224 11
    public function response(): JsonResponse
225
    {
226 11
        return new JsonResponse($this->toArray(), $this->errors[0]->status);
227
    }
228
229
    /**
230
     * Retrieve the formatted JSON:API errors.
231
     *
232
     * @return array<string, array<int, array<string, mixed>>>
233
     */
234 11
    public function toArray(): array
235
    {
236 11
        $data = [];
237
238 11
        foreach ($this->errors as $error) {
239 11
            $data['errors'][] = array_filter($error->toArray(), filled(...));
240
        }
241
242 11
        return $data;
243
    }
244
245
    /**
246
     * Stop the code flow to render the JSON:API errors.
247
     */
248 1
    public function throw(): never
249
    {
250 1
        throw new JsonApiException(...$this->errors);
251
    }
252
}
253