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: * Base class for PSR-7 HTTP message classes
20: *
21: * @api
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: * Get the start line of the message
34: */
35: abstract protected function getStartLine(): string;
36:
37: /**
38: * @param StreamInterface|resource|string|null $body
39: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string>|null $headers
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: * @inheritDoc
55: */
56: public function getProtocolVersion(): string
57: {
58: return $this->ProtocolVersion;
59: }
60:
61: /**
62: * @inheritDoc
63: */
64: public function getBody(): StreamInterface
65: {
66: return $this->Body;
67: }
68:
69: /**
70: * @inheritDoc
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: * @inheritDoc
88: */
89: public function withProtocolVersion(string $version): MessageInterface
90: {
91: return $this->with('ProtocolVersion', $this->filterProtocolVersion($version));
92: }
93:
94: /**
95: * @param StreamInterface|resource|string|null $body
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: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string>|null $headers
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: * @param StreamInterface|resource|string|null $body
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: * @return $this
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: * @inheritDoc
168: */
169: public function __toString(): string
170: {
171: return $this->getHttpPayload();
172: }
173:
174: /**
175: * @return array{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}>,headersSize:int,bodySize:int}
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: