1<?php 2 3declare(strict_types=1); 4 5namespace GuzzleHttp\Psr7; 6 7use Psr\Http\Message\StreamInterface; 8 9/** 10 * Provides a read only stream that pumps data from a PHP callable. 11 * 12 * When invoking the provided callable, the PumpStream will pass the amount of 13 * data requested to read to the callable. The callable can choose to ignore 14 * this value and return fewer or more bytes than requested. Any extra data 15 * returned by the provided callable is buffered internally until drained using 16 * the read() function of the PumpStream. The provided callable MUST return 17 * false when there is no more data to read. 18 */ 19final class PumpStream implements StreamInterface 20{ 21 /** @var callable(int): (string|false|null)|null */ 22 private $source; 23 24 /** @var int|null */ 25 private $size; 26 27 /** @var int */ 28 private $tellPos = 0; 29 30 /** @var array */ 31 private $metadata; 32 33 /** @var BufferStream */ 34 private $buffer; 35 36 /** 37 * @param callable(int): (string|false|null) $source Source of the stream data. The callable MAY 38 * accept an integer argument used to control the 39 * amount of data to return. The callable MUST 40 * return a string when called, or false|null on error 41 * or EOF. 42 * @param array{size?: int, metadata?: array} $options Stream options: 43 * - metadata: Hash of metadata to use with stream. 44 * - size: Size of the stream, if known. 45 */ 46 public function __construct(callable $source, array $options = []) 47 { 48 $this->source = $source; 49 $this->size = $options['size'] ?? null; 50 $this->metadata = $options['metadata'] ?? []; 51 $this->buffer = new BufferStream(); 52 } 53 54 public function __toString(): string 55 { 56 try { 57 return Utils::copyToString($this); 58 } catch (\Throwable $e) { 59 if (\PHP_VERSION_ID >= 70400) { 60 throw $e; 61 } 62 trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); 63 64 return ''; 65 } 66 } 67 68 public function close(): void 69 { 70 $this->detach(); 71 } 72 73 public function detach() 74 { 75 $this->tellPos = 0; 76 $this->source = null; 77 78 return null; 79 } 80 81 public function getSize(): ?int 82 { 83 return $this->size; 84 } 85 86 public function tell(): int 87 { 88 return $this->tellPos; 89 } 90 91 public function eof(): bool 92 { 93 return $this->source === null; 94 } 95 96 public function isSeekable(): bool 97 { 98 return false; 99 } 100 101 public function rewind(): void 102 { 103 $this->seek(0); 104 } 105 106 public function seek($offset, $whence = SEEK_SET): void 107 { 108 throw new \RuntimeException('Cannot seek a PumpStream'); 109 } 110 111 public function isWritable(): bool 112 { 113 return false; 114 } 115 116 public function write($string): int 117 { 118 throw new \RuntimeException('Cannot write to a PumpStream'); 119 } 120 121 public function isReadable(): bool 122 { 123 return true; 124 } 125 126 public function read($length): string 127 { 128 $data = $this->buffer->read($length); 129 $readLen = strlen($data); 130 $this->tellPos += $readLen; 131 $remaining = $length - $readLen; 132 133 if ($remaining) { 134 $this->pump($remaining); 135 $data .= $this->buffer->read($remaining); 136 $this->tellPos += strlen($data) - $readLen; 137 } 138 139 return $data; 140 } 141 142 public function getContents(): string 143 { 144 $result = ''; 145 while (!$this->eof()) { 146 $result .= $this->read(1000000); 147 } 148 149 return $result; 150 } 151 152 /** 153 * @return mixed 154 */ 155 public function getMetadata($key = null) 156 { 157 if (!$key) { 158 return $this->metadata; 159 } 160 161 return $this->metadata[$key] ?? null; 162 } 163 164 private function pump(int $length): void 165 { 166 if ($this->source !== null) { 167 do { 168 $data = ($this->source)($length); 169 if ($data === false || $data === null) { 170 $this->source = null; 171 172 return; 173 } 174 $this->buffer->write($data); 175 $length -= strlen($data); 176 } while ($length > 0); 177 } 178 } 179} 180