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\HasMutator;
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 HasMutator;
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: * Creates a new HttpResponse object
95: *
96: * @param StreamInterface|resource|string|null $body
97: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string>|null $headers
98: */
99: public function __construct(
100: int $code = 200,
101: $body = null,
102: $headers = null,
103: ?string $reasonPhrase = null,
104: string $version = '1.1'
105: ) {
106: $this->StatusCode = $this->filterStatusCode($code);
107: $this->ReasonPhrase = $this->filterReasonPhrase($code, $reasonPhrase);
108:
109: parent::__construct($body, $headers, $version);
110: }
111:
112: /**
113: * @inheritDoc
114: */
115: public static function fromPsr7(MessageInterface $message): HttpResponse
116: {
117: if ($message instanceof HttpResponse) {
118: return $message;
119: }
120:
121: if (!$message instanceof ResponseInterface) {
122: throw new InvalidArgumentTypeException(1, 'message', ResponseInterface::class, $message);
123: }
124:
125: return new self(
126: $message->getStatusCode(),
127: $message->getBody(),
128: $message->getHeaders(),
129: $message->getReasonPhrase(),
130: $message->getProtocolVersion(),
131: );
132: }
133:
134: /**
135: * @inheritDoc
136: */
137: public function getStatusCode(): int
138: {
139: return $this->StatusCode;
140: }
141:
142: /**
143: * @inheritDoc
144: */
145: public function getReasonPhrase(): string
146: {
147: return (string) $this->ReasonPhrase;
148: }
149:
150: /**
151: * @inheritDoc
152: */
153: public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
154: {
155: return $this
156: ->with('StatusCode', $this->filterStatusCode($code))
157: ->with('ReasonPhrase', $this->filterReasonPhrase($code, $reasonPhrase));
158: }
159:
160: /**
161: * @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}
162: */
163: public function jsonSerialize(): array
164: {
165: $response = parent::jsonSerialize();
166:
167: $mediaType = $this->Headers->getHeaderValues(HttpHeader::CONTENT_TYPE);
168: $location = $this->Headers->getHeaderValues(HttpHeader::LOCATION);
169:
170: return [
171: 'status' => $this->StatusCode,
172: 'statusText' => (string) $this->ReasonPhrase,
173: 'httpVersion' => $response['httpVersion'],
174: 'cookies' => $response['cookies'],
175: 'headers' => $response['headers'],
176: 'content' => [
177: 'size' => $response['bodySize'],
178: 'mimeType' => count($mediaType) === 1 ? $mediaType[0] : '',
179: 'text' => (string) $this->Body,
180: ],
181: 'redirectURL' => count($location) === 1 ? $location[0] : '',
182: ] + $response;
183: }
184:
185: /**
186: * @inheritDoc
187: */
188: protected function getStartLine(): string
189: {
190: return Arr::implode(' ', [
191: sprintf('HTTP/%s %d', $this->ProtocolVersion, $this->StatusCode),
192: $this->ReasonPhrase,
193: ]);
194: }
195:
196: private function filterStatusCode(int $code): int
197: {
198: if ($code < 100 || $code > 599) {
199: throw new InvalidArgumentException(
200: sprintf('Invalid HTTP status code: %d', $code)
201: );
202: }
203: return $code;
204: }
205:
206: private function filterReasonPhrase(int $code, ?string $reasonPhrase): ?string
207: {
208: return Str::coalesce($reasonPhrase, null)
209: ?? static::STATUS_CODE[$code]
210: ?? null;
211: }
212: }
213: