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