Passed
Push — master ( b7f261...78e61b )
by Mihail
02:02
created

HTTPError::status()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

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