1<?php 2 3declare(strict_types=1); 4 5namespace GuzzleHttp\Psr7; 6 7use Psr\Http\Message\StreamInterface; 8 9/** 10 * Stream decorator that can cache previously read bytes from a sequentially 11 * read stream. 12 */ 13final class CachingStream implements StreamInterface 14{ 15 use StreamDecoratorTrait; 16 17 /** @var StreamInterface Stream being wrapped */ 18 private $remoteStream; 19 20 /** @var int Number of bytes to skip reading due to a write on the buffer */ 21 private $skipReadBytes = 0; 22 23 /** 24 * @var StreamInterface 25 */ 26 private $stream; 27 28 /** 29 * We will treat the buffer object as the body of the stream 30 * 31 * @param StreamInterface $stream Stream to cache. The cursor is assumed to be at the beginning of the stream. 32 * @param StreamInterface $target Optionally specify where data is cached 33 */ 34 public function __construct( 35 StreamInterface $stream, 36 StreamInterface $target = null 37 ) { 38 $this->remoteStream = $stream; 39 $this->stream = $target ?: new Stream(Utils::tryFopen('php://temp', 'r+')); 40 } 41 42 public function getSize(): ?int 43 { 44 $remoteSize = $this->remoteStream->getSize(); 45 46 if (null === $remoteSize) { 47 return null; 48 } 49 50 return max($this->stream->getSize(), $remoteSize); 51 } 52 53 public function rewind(): void 54 { 55 $this->seek(0); 56 } 57 58 public function seek($offset, $whence = SEEK_SET): void 59 { 60 if ($whence === SEEK_SET) { 61 $byte = $offset; 62 } elseif ($whence === SEEK_CUR) { 63 $byte = $offset + $this->tell(); 64 } elseif ($whence === SEEK_END) { 65 $size = $this->remoteStream->getSize(); 66 if ($size === null) { 67 $size = $this->cacheEntireStream(); 68 } 69 $byte = $size + $offset; 70 } else { 71 throw new \InvalidArgumentException('Invalid whence'); 72 } 73 74 $diff = $byte - $this->stream->getSize(); 75 76 if ($diff > 0) { 77 // Read the remoteStream until we have read in at least the amount 78 // of bytes requested, or we reach the end of the file. 79 while ($diff > 0 && !$this->remoteStream->eof()) { 80 $this->read($diff); 81 $diff = $byte - $this->stream->getSize(); 82 } 83 } else { 84 // We can just do a normal seek since we've already seen this byte. 85 $this->stream->seek($byte); 86 } 87 } 88 89 public function read($length): string 90 { 91 // Perform a regular read on any previously read data from the buffer 92 $data = $this->stream->read($length); 93 $remaining = $length - strlen($data); 94 95 // More data was requested so read from the remote stream 96 if ($remaining) { 97 // If data was written to the buffer in a position that would have 98 // been filled from the remote stream, then we must skip bytes on 99 // the remote stream to emulate overwriting bytes from that 100 // position. This mimics the behavior of other PHP stream wrappers. 101 $remoteData = $this->remoteStream->read( 102 $remaining + $this->skipReadBytes 103 ); 104 105 if ($this->skipReadBytes) { 106 $len = strlen($remoteData); 107 $remoteData = substr($remoteData, $this->skipReadBytes); 108 $this->skipReadBytes = max(0, $this->skipReadBytes - $len); 109 } 110 111 $data .= $remoteData; 112 $this->stream->write($remoteData); 113 } 114 115 return $data; 116 } 117 118 public function write($string): int 119 { 120 // When appending to the end of the currently read stream, you'll want 121 // to skip bytes from being read from the remote stream to emulate 122 // other stream wrappers. Basically replacing bytes of data of a fixed 123 // length. 124 $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); 125 if ($overflow > 0) { 126 $this->skipReadBytes += $overflow; 127 } 128 129 return $this->stream->write($string); 130 } 131 132 public function eof(): bool 133 { 134 return $this->stream->eof() && $this->remoteStream->eof(); 135 } 136 137 /** 138 * Close both the remote stream and buffer stream 139 */ 140 public function close(): void 141 { 142 $this->remoteStream->close(); 143 $this->stream->close(); 144 } 145 146 private function cacheEntireStream(): int 147 { 148 $target = new FnStream(['write' => 'strlen']); 149 Utils::copyToStream($this, $target); 150 151 return $this->tell(); 152 } 153} 154