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