1: <?php declare(strict_types=1);
2:
3: namespace Salient\Http\Message;
4:
5: use Psr\Http\Message\MessageInterface as PsrMessageInterface;
6: use Psr\Http\Message\ResponseInterface as PsrResponseInterface;
7: use Salient\Contract\Http\Message\ResponseInterface;
8: use Salient\Core\Concern\ImmutableTrait;
9: use Salient\Utility\Arr;
10: use Salient\Utility\Str;
11: use InvalidArgumentException;
12:
13: /**
14: * @api
15: *
16: * @extends AbstractMessage<PsrResponseInterface>
17: */
18: class Response extends AbstractMessage implements ResponseInterface
19: {
20: use ImmutableTrait;
21:
22: /**
23: * @var array<int,string>
24: */
25: protected const STATUS_CODE = [
26: 100 => 'Continue',
27: 101 => 'Switching Protocols',
28: 102 => 'Processing',
29: 103 => 'Early Hints',
30: 200 => 'OK',
31: 201 => 'Created',
32: 202 => 'Accepted',
33: 203 => 'Non-Authoritative Information',
34: 204 => 'No Content',
35: 205 => 'Reset Content',
36: 206 => 'Partial Content',
37: 207 => 'Multi-Status',
38: 208 => 'Already Reported',
39: 226 => 'IM Used',
40: 300 => 'Multiple Choices',
41: 301 => 'Moved Permanently',
42: 302 => 'Found',
43: 303 => 'See Other',
44: 304 => 'Not Modified',
45: 305 => 'Use Proxy',
46: 307 => 'Temporary Redirect',
47: 308 => 'Permanent Redirect',
48: 400 => 'Bad Request',
49: 401 => 'Unauthorized',
50: 402 => 'Payment Required',
51: 403 => 'Forbidden',
52: 404 => 'Not Found',
53: 405 => 'Method Not Allowed',
54: 406 => 'Not Acceptable',
55: 407 => 'Proxy Authentication Required',
56: 408 => 'Request Timeout',
57: 409 => 'Conflict',
58: 410 => 'Gone',
59: 411 => 'Length Required',
60: 412 => 'Precondition Failed',
61: 413 => 'Content Too Large',
62: 414 => 'URI Too Long',
63: 415 => 'Unsupported Media Type',
64: 416 => 'Range Not Satisfiable',
65: 417 => 'Expectation Failed',
66: 421 => 'Misdirected Request',
67: 422 => 'Unprocessable Content',
68: 423 => 'Locked',
69: 424 => 'Failed Dependency',
70: 425 => 'Too Early',
71: 426 => 'Upgrade Required',
72: 428 => 'Precondition Required',
73: 429 => 'Too Many Requests',
74: 431 => 'Request Header Fields Too Large',
75: 451 => 'Unavailable For Legal Reasons',
76: 500 => 'Internal Server Error',
77: 501 => 'Not Implemented',
78: 502 => 'Bad Gateway',
79: 503 => 'Service Unavailable',
80: 504 => 'Gateway Timeout',
81: 505 => 'HTTP Version Not Supported',
82: 506 => 'Variant Also Negotiates',
83: 507 => 'Insufficient Storage',
84: 508 => 'Loop Detected',
85: 510 => 'Not Extended',
86: 511 => 'Network Authentication Required',
87: ];
88:
89: protected int $StatusCode;
90: protected ?string $ReasonPhrase;
91:
92: /**
93: * @api
94: */
95: final public function __construct(
96: int $code = 200,
97: $body = null,
98: $headers = null,
99: ?string $reasonPhrase = null,
100: string $version = '1.1'
101: ) {
102: $this->StatusCode = $this->filterStatusCode($code);
103: $this->ReasonPhrase = $this->filterReasonPhrase($code, $reasonPhrase);
104:
105: parent::__construct($body, $headers, $version);
106: }
107:
108: /**
109: * @inheritDoc
110: */
111: public static function fromPsr7(PsrMessageInterface $message): Response
112: {
113: return $message instanceof static
114: ? $message
115: : new static(
116: $message->getStatusCode(),
117: $message->getBody(),
118: $message->getHeaders(),
119: $message->getReasonPhrase(),
120: $message->getProtocolVersion(),
121: );
122: }
123:
124: /**
125: * @inheritDoc
126: */
127: public function getStatusCode(): int
128: {
129: return $this->StatusCode;
130: }
131:
132: /**
133: * @inheritDoc
134: */
135: public function getReasonPhrase(): string
136: {
137: return (string) $this->ReasonPhrase;
138: }
139:
140: /**
141: * @inheritDoc
142: */
143: public function withStatus(int $code, string $reasonPhrase = ''): PsrResponseInterface
144: {
145: return $this
146: ->with('StatusCode', $this->filterStatusCode($code))
147: ->with('ReasonPhrase', $this->filterReasonPhrase($code, $reasonPhrase));
148: }
149:
150: private function filterStatusCode(int $code): int
151: {
152: if ($code < 100 || $code > 599) {
153: throw new InvalidArgumentException(
154: sprintf('Invalid HTTP status code: %d', $code),
155: );
156: }
157: return $code;
158: }
159:
160: private function filterReasonPhrase(int $code, ?string $reasonPhrase): ?string
161: {
162: return Str::coalesce($reasonPhrase, null)
163: ?? static::STATUS_CODE[$code]
164: ?? null;
165: }
166:
167: /**
168: * @inheritDoc
169: */
170: protected function getStartLine(): string
171: {
172: return Arr::implode(' ', [
173: sprintf('HTTP/%s %d', $this->ProtocolVersion, $this->StatusCode),
174: $this->ReasonPhrase,
175: ]);
176: }
177:
178: /**
179: * @return array{status:int,statusText:string,httpVersion:string,cookies:array<array{name:string,value:string,path?:string,domain?:string,expires?:string,httpOnly?:bool,secure?:bool}>,headers:array<array{name:string,value:string}>,content:array{size:int,mimeType:string,text:string},redirectURL:string,headersSize:int,bodySize:int}
180: */
181: public function jsonSerialize(): array
182: {
183: $response = parent::jsonSerialize();
184:
185: $contentType = $this->Headers->getLastHeaderValue(self::HEADER_CONTENT_TYPE);
186: $location = $this->Headers->getHeader(self::HEADER_LOCATION);
187:
188: return [
189: 'status' => $this->StatusCode,
190: 'statusText' => (string) $this->ReasonPhrase,
191: 'httpVersion' => $response['httpVersion'],
192: 'cookies' => $response['cookies'],
193: 'headers' => $response['headers'],
194: 'content' => [
195: 'size' => $response['bodySize'],
196: 'mimeType' => $contentType,
197: 'text' => (string) $this->Body,
198: ],
199: 'redirectURL' => count($location) === 1 ? $location[0] : '',
200: ] + $response;
201: }
202: }
203: