Passed
Push — master ( 295fd2...d0507a )
by Andrea Marco
02:54 queued 13s
created

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