1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Http; |
4: | |
5: | use Psr\Http\Message\MessageInterface; |
6: | use Psr\Http\Message\StreamInterface; |
7: | use Salient\Contract\Core\Arrayable; |
8: | use Salient\Contract\Core\MimeType; |
9: | use Salient\Contract\Http\HttpHeader; |
10: | use Salient\Contract\Http\HttpHeadersInterface; |
11: | use Salient\Contract\Http\HttpMessageInterface; |
12: | use Salient\Contract\Http\HttpMultipartStreamInterface; |
13: | use Salient\Core\Concern\HasMutator; |
14: | use Salient\Utility\Exception\InvalidArgumentTypeException; |
15: | use Salient\Utility\Regex; |
16: | use InvalidArgumentException; |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | |
23: | abstract class AbstractHttpMessage implements HttpMessageInterface |
24: | { |
25: | use HasHttpHeaders; |
26: | use HasMutator; |
27: | |
28: | protected string $ProtocolVersion; |
29: | protected HttpHeadersInterface $Headers; |
30: | protected StreamInterface $Body; |
31: | |
32: | |
33: | |
34: | |
35: | abstract protected function getStartLine(): string; |
36: | |
37: | |
38: | |
39: | |
40: | |
41: | public function __construct( |
42: | $body = null, |
43: | $headers = null, |
44: | string $version = '1.1' |
45: | ) { |
46: | $this->ProtocolVersion = $this->filterProtocolVersion($version); |
47: | $this->Headers = $this->filterHeaders($headers); |
48: | $this->Body = $this->filterBody($body); |
49: | |
50: | $this->maybeSetContentType(); |
51: | } |
52: | |
53: | |
54: | |
55: | |
56: | public function getProtocolVersion(): string |
57: | { |
58: | return $this->ProtocolVersion; |
59: | } |
60: | |
61: | |
62: | |
63: | |
64: | public function getBody(): StreamInterface |
65: | { |
66: | return $this->Body; |
67: | } |
68: | |
69: | |
70: | |
71: | |
72: | public function getHttpPayload(bool $withoutBody = false): string |
73: | { |
74: | $message = implode("\r\n", [ |
75: | $this->getStartLine(), |
76: | (string) $this->Headers, |
77: | '', |
78: | '', |
79: | ]); |
80: | |
81: | return $withoutBody |
82: | ? $message |
83: | : $message . $this->Body; |
84: | } |
85: | |
86: | |
87: | |
88: | |
89: | public function withProtocolVersion(string $version): MessageInterface |
90: | { |
91: | return $this->with('ProtocolVersion', $this->filterProtocolVersion($version)); |
92: | } |
93: | |
94: | |
95: | |
96: | |
97: | public function withBody($body): MessageInterface |
98: | { |
99: | return $this |
100: | ->with('Body', $this->filterBody($body)) |
101: | ->maybeSetContentType(); |
102: | } |
103: | |
104: | private function filterProtocolVersion(string $version): string |
105: | { |
106: | if (!Regex::match('/^[0-9](?:\.[0-9])?$/D', $version)) { |
107: | throw new InvalidArgumentException( |
108: | sprintf('Invalid HTTP protocol version: %s', $version) |
109: | ); |
110: | } |
111: | return $version; |
112: | } |
113: | |
114: | |
115: | |
116: | |
117: | private function filterHeaders($headers): HttpHeadersInterface |
118: | { |
119: | if ($headers instanceof HttpHeadersInterface) { |
120: | return $headers; |
121: | } |
122: | return new HttpHeaders($headers ?? []); |
123: | } |
124: | |
125: | |
126: | |
127: | |
128: | private function filterBody($body): StreamInterface |
129: | { |
130: | if ($body instanceof StreamInterface) { |
131: | return $body; |
132: | } |
133: | if (is_string($body) || $body === null) { |
134: | return HttpStream::fromString((string) $body); |
135: | } |
136: | try { |
137: | return new HttpStream($body); |
138: | } catch (InvalidArgumentException $ex) { |
139: | throw new InvalidArgumentTypeException( |
140: | 1, |
141: | 'body', |
142: | 'StreamInterface|resource|string|null', |
143: | $body |
144: | ); |
145: | } |
146: | } |
147: | |
148: | |
149: | |
150: | |
151: | private function maybeSetContentType(): self |
152: | { |
153: | if ($this->Body instanceof HttpMultipartStreamInterface) { |
154: | $this->Headers = $this->Headers->set( |
155: | HttpHeader::CONTENT_TYPE, |
156: | sprintf( |
157: | '%s; boundary=%s', |
158: | MimeType::FORM_MULTIPART, |
159: | HttpUtil::maybeQuoteString($this->Body->getBoundary()), |
160: | ), |
161: | ); |
162: | } |
163: | return $this; |
164: | } |
165: | |
166: | |
167: | |
168: | |
169: | public function __toString(): string |
170: | { |
171: | return $this->getHttpPayload(); |
172: | } |
173: | |
174: | |
175: | |
176: | |
177: | public function jsonSerialize(): array |
178: | { |
179: | return [ |
180: | 'httpVersion' => sprintf('HTTP/%s', $this->ProtocolVersion), |
181: | 'cookies' => [], |
182: | 'headers' => $this->Headers->jsonSerialize(), |
183: | 'headersSize' => strlen($this->getHttpPayload(true)), |
184: | 'bodySize' => $this->Body->getSize() ?? -1, |
185: | ]; |
186: | } |
187: | } |
188: | |