1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Http\Message; |
4: | |
5: | use Salient\Contract\Core\DateFormatterInterface; |
6: | use Salient\Contract\Http\Message\StreamInterface; |
7: | use Salient\Contract\Http\Message\StreamPartInterface; |
8: | use Salient\Contract\Http\HasFormDataFlag; |
9: | use Salient\Http\Exception\InvalidStreamRequestException; |
10: | use Salient\Http\Exception\StreamClosedException; |
11: | use Salient\Http\Exception\StreamEncapsulationException; |
12: | use Salient\Http\Internal\FormDataEncoder; |
13: | use Salient\Utility\Exception\InvalidArgumentTypeException; |
14: | use Salient\Utility\File; |
15: | use Salient\Utility\Json; |
16: | use Salient\Utility\Str; |
17: | use InvalidArgumentException; |
18: | |
19: | |
20: | |
21: | |
22: | class Stream implements StreamInterface, HasFormDataFlag |
23: | { |
24: | private ?string $Uri; |
25: | private bool $IsReadable; |
26: | private bool $IsWritable; |
27: | private bool $IsSeekable; |
28: | |
29: | private $Stream; |
30: | |
31: | |
32: | |
33: | |
34: | |
35: | |
36: | final public function __construct($stream) |
37: | { |
38: | if (!File::isStream($stream)) { |
39: | throw new InvalidArgumentTypeException(1, 'stream', 'resource (stream)', $stream); |
40: | } |
41: | |
42: | $meta = stream_get_meta_data($stream); |
43: | |
44: | $this->Uri = $meta['uri'] ?? null; |
45: | $this->IsReadable = strpbrk($meta['mode'], 'r+') !== false; |
46: | $this->IsWritable = strpbrk($meta['mode'], 'waxc+') !== false; |
47: | $this->IsSeekable = $meta['seekable']; |
48: | $this->Stream = $stream; |
49: | } |
50: | |
51: | |
52: | |
53: | |
54: | public function __destruct() |
55: | { |
56: | $this->close(); |
57: | } |
58: | |
59: | |
60: | |
61: | |
62: | |
63: | |
64: | public static function fromString(string $content): self |
65: | { |
66: | return new static(Str::toStream($content)); |
67: | } |
68: | |
69: | |
70: | |
71: | |
72: | |
73: | |
74: | |
75: | |
76: | public static function fromData( |
77: | $data, |
78: | int $flags = Stream::DATA_PRESERVE_NUMERIC_KEYS | Stream::DATA_PRESERVE_STRING_KEYS, |
79: | ?DateFormatterInterface $dateFormatter = null, |
80: | bool $asJson = false, |
81: | ?string $boundary = null |
82: | ) { |
83: | if ($asJson) { |
84: | $callback = static function ($value) { |
85: | if ($value instanceof StreamPartInterface) { |
86: | throw new StreamEncapsulationException( |
87: | 'Multipart streams cannot be JSON-encoded', |
88: | ); |
89: | } |
90: | return false; |
91: | }; |
92: | $data = (new FormDataEncoder($flags, $dateFormatter, $callback))->getData($data); |
93: | return static::fromString(Json::encode($data)); |
94: | } |
95: | |
96: | $multipart = false; |
97: | $callback = static function ($value) use (&$multipart) { |
98: | if ($value instanceof StreamPartInterface) { |
99: | $multipart = true; |
100: | return $value; |
101: | } |
102: | return false; |
103: | }; |
104: | $data = (new FormDataEncoder($flags, $dateFormatter, $callback))->getValues($data); |
105: | |
106: | if (!$multipart) { |
107: | |
108: | foreach ($data as [$name, $content]) { |
109: | $query[] = rawurlencode($name) . '=' . rawurlencode($content); |
110: | } |
111: | return static::fromString(implode('&', $query ?? [])); |
112: | } |
113: | |
114: | |
115: | foreach ($data as [$name, $content]) { |
116: | if ($content instanceof StreamPartInterface) { |
117: | $parts[] = $content->withName($name); |
118: | } else { |
119: | $parts[] = new StreamPart($content, $name); |
120: | } |
121: | } |
122: | return new MultipartStream($parts ?? [], $boundary); |
123: | } |
124: | |
125: | |
126: | |
127: | |
128: | public function isReadable(): bool |
129: | { |
130: | return $this->IsReadable; |
131: | } |
132: | |
133: | |
134: | |
135: | |
136: | public function isWritable(): bool |
137: | { |
138: | return $this->IsWritable; |
139: | } |
140: | |
141: | |
142: | |
143: | |
144: | public function isSeekable(): bool |
145: | { |
146: | return $this->IsSeekable; |
147: | } |
148: | |
149: | |
150: | |
151: | |
152: | public function getSize(): ?int |
153: | { |
154: | $this->assertHasStream(); |
155: | |
156: | return File::stat($this->Stream, $this->Uri)['size'] ?? null; |
157: | } |
158: | |
159: | |
160: | |
161: | |
162: | public function getMetadata(?string $key = null) |
163: | { |
164: | $this->assertHasStream(); |
165: | |
166: | $meta = stream_get_meta_data($this->Stream); |
167: | return $key === null ? $meta : ($meta[$key] ?? null); |
168: | } |
169: | |
170: | |
171: | |
172: | |
173: | public function __toString(): string |
174: | { |
175: | if ($this->IsSeekable) { |
176: | $this->rewind(); |
177: | } |
178: | |
179: | return $this->getContents(); |
180: | } |
181: | |
182: | |
183: | |
184: | |
185: | public function getContents(): string |
186: | { |
187: | $this->assertIsReadable(); |
188: | |
189: | return File::getContents($this->Stream, null, $this->Uri); |
190: | } |
191: | |
192: | |
193: | |
194: | |
195: | public function tell(): int |
196: | { |
197: | $this->assertHasStream(); |
198: | |
199: | return File::tell($this->Stream, $this->Uri); |
200: | } |
201: | |
202: | |
203: | |
204: | |
205: | public function eof(): bool |
206: | { |
207: | $this->assertHasStream(); |
208: | |
209: | return File::eof($this->Stream); |
210: | } |
211: | |
212: | |
213: | |
214: | |
215: | public function rewind(): void |
216: | { |
217: | $this->assertIsSeekable(); |
218: | |
219: | File::rewind($this->Stream, $this->Uri); |
220: | } |
221: | |
222: | |
223: | |
224: | |
225: | public function read(int $length): string |
226: | { |
227: | $this->assertIsReadable(); |
228: | |
229: | if ($length < 0) { |
230: | throw new InvalidArgumentException( |
231: | 'Argument #1 ($length) must be greater than or equal to 0', |
232: | ); |
233: | } |
234: | |
235: | return $length |
236: | ? File::read($this->Stream, $length, $this->Uri) |
237: | : ''; |
238: | } |
239: | |
240: | |
241: | |
242: | |
243: | public function write(string $string): int |
244: | { |
245: | $this->assertHasStream(); |
246: | |
247: | if (!$this->IsWritable) { |
248: | throw new InvalidStreamRequestException('Stream is not writable'); |
249: | } |
250: | |
251: | return File::write($this->Stream, $string, null, $this->Uri); |
252: | } |
253: | |
254: | |
255: | |
256: | |
257: | |
258: | |
259: | public function seek(int $offset, int $whence = \SEEK_SET): void |
260: | { |
261: | $this->assertIsSeekable(); |
262: | |
263: | |
264: | File::seek($this->Stream, $offset, $whence, $this->Uri); |
265: | } |
266: | |
267: | |
268: | |
269: | |
270: | public function close(): void |
271: | { |
272: | if (!$this->Stream) { |
273: | return; |
274: | } |
275: | |
276: | File::close($this->Stream, $this->Uri); |
277: | $this->detach(); |
278: | } |
279: | |
280: | |
281: | |
282: | |
283: | public function detach() |
284: | { |
285: | if (!$this->Stream) { |
286: | return null; |
287: | } |
288: | |
289: | $result = $this->Stream; |
290: | |
291: | $this->Stream = null; |
292: | $this->IsReadable = false; |
293: | $this->IsWritable = false; |
294: | $this->IsSeekable = false; |
295: | |
296: | return $result; |
297: | } |
298: | |
299: | |
300: | |
301: | |
302: | private function assertIsSeekable(): void |
303: | { |
304: | $this->assertHasStream(); |
305: | |
306: | if (!$this->IsSeekable) { |
307: | throw new InvalidStreamRequestException('Stream is not seekable'); |
308: | } |
309: | } |
310: | |
311: | |
312: | |
313: | |
314: | private function assertIsReadable(): void |
315: | { |
316: | $this->assertHasStream(); |
317: | |
318: | if (!$this->IsReadable) { |
319: | throw new InvalidStreamRequestException('Stream is not readable'); |
320: | } |
321: | } |
322: | |
323: | |
324: | |
325: | |
326: | private function assertHasStream(): void |
327: | { |
328: | if (!$this->Stream) { |
329: | throw new StreamClosedException('Stream is closed or detached'); |
330: | } |
331: | } |
332: | } |
333: | |