1<?php 2namespace GuzzleHttp\Stream; 3 4use GuzzleHttp\Stream\Exception\SeekException; 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 /** 40 * {@inheritdoc} 41 * @throws SeekException When seeking with SEEK_END or when seeking 42 * past the total size of the buffer stream 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 } else { 51 return false; 52 } 53 54 // You cannot skip ahead past where you've read from the remote stream 55 if ($byte > $this->stream->getSize()) { 56 throw new SeekException( 57 $this, 58 $byte, 59 sprintf('Cannot seek to byte %d when the buffered stream only' 60 . ' contains %d bytes', $byte, $this->stream->getSize()) 61 ); 62 } 63 64 return $this->stream->seek($byte); 65 } 66 67 public function read($length) 68 { 69 // Perform a regular read on any previously read data from the buffer 70 $data = $this->stream->read($length); 71 $remaining = $length - strlen($data); 72 73 // More data was requested so read from the remote stream 74 if ($remaining) { 75 // If data was written to the buffer in a position that would have 76 // been filled from the remote stream, then we must skip bytes on 77 // the remote stream to emulate overwriting bytes from that 78 // position. This mimics the behavior of other PHP stream wrappers. 79 $remoteData = $this->remoteStream->read( 80 $remaining + $this->skipReadBytes 81 ); 82 83 if ($this->skipReadBytes) { 84 $len = strlen($remoteData); 85 $remoteData = substr($remoteData, $this->skipReadBytes); 86 $this->skipReadBytes = max(0, $this->skipReadBytes - $len); 87 } 88 89 $data .= $remoteData; 90 $this->stream->write($remoteData); 91 } 92 93 return $data; 94 } 95 96 public function write($string) 97 { 98 // When appending to the end of the currently read stream, you'll want 99 // to skip bytes from being read from the remote stream to emulate 100 // other stream wrappers. Basically replacing bytes of data of a fixed 101 // length. 102 $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); 103 if ($overflow > 0) { 104 $this->skipReadBytes += $overflow; 105 } 106 107 return $this->stream->write($string); 108 } 109 110 public function eof() 111 { 112 return $this->stream->eof() && $this->remoteStream->eof(); 113 } 114 115 /** 116 * Close both the remote stream and buffer stream 117 */ 118 public function close() 119 { 120 $this->remoteStream->close() && $this->stream->close(); 121 } 122} 123