Passed
Push — master ( e63f1d...295fd2 )
by Andrea Marco
03:11 queued 14s
created

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