Passed
Pull Request — master (#17)
by Mihail
15:10
created

HTTPError::toXml()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 6
eloc 13
c 2
b 1
f 0
nc 5
nop 0
dl 0
loc 19
rs 9.2222
1
<?php
2
3
namespace Koded\Http;
4
5
use Koded\Http\Interfaces\HttpStatus;
6
use RuntimeException;
7
use Throwable;
8
use function array_filter;
9
use function Koded\Stdlib\json_serialize;
10
use function Koded\Stdlib\xml_serialize;
11
use function rawurldecode;
12
13
interface HTTPException extends Throwable
14
{
15
    public function getStatusCode(): int;
16
17
    public function getTitle(): string;
18
19
    public function getType(): string;
20
21
    public function getDetail(): string;
22
23
    public function getInstance(): string;
24
25
    public function getHeaders(): iterable;
26
27
    public function setInstance(string $value): static;
28
29
    public function setMember(string $name, mixed $value): static;
30
31
    public function toJson(): string;
32
33
    public function toXml(): string;
34
35
    public function toArray(): array;
36
}
37
38
/**
39
 * Represents a generic HTTP error, that
40
 * follows the RFC-9457 (and RFC-7807) standard.
41
 *
42
 * Raise an instance of subclass of `HTTPError` to have Koded return
43
 * a formatted error response and appropriate HTTP status code to
44
 * the client when something goes wrong. JSON and XML media types are
45
 * supported by default.
46
 *
47
 * NOTE:
48
 *  if you wish to return custom error messages, you can create
49
 *  your own HTTPError subclass and register it with the error
50
 *  handler method to convert it into the desired HTTP response.
51
 *
52
 * @link https://datatracker.ietf.org/doc/html/rfc9457
53
 */
54
class HTTPError extends RuntimeException implements HTTPException
55
{
56
    /**
57
     * Extension members for problem type definitions may extend the
58
     * problem details object with additional information. Clients
59
     * consuming problem MUST ignore any extensions that they don't
60
     * recognize, allowing problem types to evolve and include
61
     * additional information in the future.
62
     *
63
     * @var array
64
     */
65
    protected array $members = [];
66
67
    /**
68
     * HTTPError constructor.
69
     *
70
     * @param int             $status   HTTP status code
71
     * @param string          $title    Error title to send to the client. If not provided, defaults to status line
72
     * @param string          $detail   Human-friendly description of the error, along with a helpful suggestion or two
73
     * @param string          $instance A URI reference that identifies the specific occurrence of the problem.
74
     * @param string          $type     A URI reference that identifies the problem type and points to a human-readable documentation
75
     * @param array|null      $headers  Extra headers to add to the response
76
     * @param Throwable|null $previous  The previous Throwable, if any
77
     */
78
    public function __construct(
79
        int              $status,
80
        protected string $title = '',
81
        protected string $detail = '',
82
        protected string $instance = '',
83
        protected string $type = '',
84
        protected ?array $headers = [],
85
        ?Throwable       $previous = null)
86
    {
87
        $this->code = $status;
88
        $this->code = static::status($this);
89
        $this->message = $detail ?: StatusCode::description($this->code);
90
        [
91
            'title'    => $this->title,
92
            'detail'   => $this->detail,
93
            'instance' => $this->instance,
94
            'type'     => $this->type,
95
        ] = $this->toArray();
96
        parent::__construct($this->message, $this->code, $previous);
97
    }
98
99
    public static function status(
100
        Throwable $ex,
101
        int $prefer = HttpStatus::I_AM_TEAPOT): int
102
    {
103
        $code = $ex->getCode();
104
        return ($code < 100 || $code > 599) ? $prefer : $code;
105
    }
106
107
    public function getStatusCode(): int
108
    {
109
        return $this->code;
110
    }
111
112
    public function getTitle(): string
113
    {
114
        return $this->title;
115
    }
116
117
    public function getType(): string
118
    {
119
        return $this->type;
120
    }
121
122
    public function getDetail(): string
123
    {
124
        return $this->detail;
125
    }
126
127
    public function getInstance(): string
128
    {
129
        return $this->instance;
130
    }
131
132
    public function getHeaders(): iterable
133
    {
134
        return $this->headers ?? [];
135
    }
136
137
    public function setInstance(string $value): static
138
    {
139
        $this->instance = $value;
140
        return $this;
141
    }
142
143
    public function setMember(string $name, mixed $value): static
144
    {
145
        $this->members[$name] = $value;
146
        return $this;
147
    }
148
149
    public function toJson(): string
150
    {
151
        return rawurldecode(json_serialize(array_filter($this->toArray())));
152
    }
153
154
    public function toXml(): string
155
    {
156
        $data = array_filter($this->toArray());
157
        foreach ($data as $k => $v) {
158
            if (is_array($v)) {
159
                $data[$k] = $v;
160
            } else if ($k === 'status') {
161
                $data[$k] = [
162
                    '@type' => 'xsd:positiveInteger',
163
                    '#'     => $v
164
                ];
165
            } else if ($k === 'instance' || $k === 'type') {
166
                $data[$k] = [
167
                    '@type' => 'xsd:anyURI',
168
                    '#'     => $v
169
                ];
170
            }
171
        }
172
        return rawurldecode(xml_serialize('problem', array_filter($data)));
173
        //return rawurldecode(xml_serialize('problem', array_filter($this->toArray())));
174
    }
175
176
    /**
177
     * @return array{status: int, instance: string, detail: string, title: string, type: string}
178
     */
179
    public function toArray(): array
180
    {
181
        $status = static::status($this);
182
        return [
183
            'status'   => $status,
184
            'instance' => $this->instance,
185
            'detail'   => $this->detail ?: $this->message,
186
            'title'    => $this->title ?: HttpStatus::CODE[$this->code],
187
            'type'     => $this->type ?: "https://httpstatuses.com/$status",
188
        ] + $this->members;
189
    }
190
191
    /**
192
     * Implements the Stringable interface.
193
     * Useful when converting the instance as \Psr\Http\Message\StreamInterface,
194
     * or typecasting it as a string.
195
     *
196
     * @return string JSON representation of the HTTPError.
197
     * @implements \Stringable
198
     */
199
    public function __toString(): string
200
    {
201
        return $this->toJson();
202
    }
203
204
    /**
205
     * @internal
206
     */
207
    public function __serialize(): array
208
    {
209
        return $this->toArray() + [
210
            'members' => $this->members,
211
            'headers' => $this->headers,
212
        ];
213
    }
214
215
    /**
216
     * @internal
217
     */
218
    public function __unserialize(array $serialized): void
219
    {
220
        [
221
            'status'   => $this->code,
222
            'detail'   => $this->detail,
223
            'detail'   => $this->message, // copy message
224
            'title'    => $this->title,
225
            'instance' => $this->instance,
226
            'type'     => $this->type,
227
            'members'  => $this->members,
228
            'headers'  => $this->headers,
229
        ] = $serialized;
230
    }
231
}
232