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