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