1: <?php
2:
3: namespace Guzzle\Http;
4:
5: use Guzzle\Common\Exception\RuntimeException;
6:
7: /**
8: * EntityBody decorator that can cache previously read bytes from a sequentially read tstream
9: */
10: class CachingEntityBody extends AbstractEntityBodyDecorator
11: {
12: /**
13: * @var EntityBody Remote stream used to actually pull data onto the buffer
14: */
15: protected $remoteStream;
16:
17: /**
18: * @var int The number of bytes to skip reading due to a write on the temporary buffer
19: */
20: protected $skipReadBytes = 0;
21:
22: /**
23: * We will treat the buffer object as the body of the entity body
24: * {@inheritdoc}
25: */
26: public function __construct(EntityBodyInterface $body)
27: {
28: $this->remoteStream = $body;
29: $this->body = new EntityBody(fopen('php://temp', 'r+'));
30: }
31:
32: /**
33: * Will give the contents of the buffer followed by the exhausted remote stream.
34: *
35: * Warning: Loads the entire stream into memory
36: *
37: * @return string
38: */
39: public function __toString()
40: {
41: $pos = $this->ftell();
42: $this->rewind();
43:
44: $str = '';
45: while (!$this->isConsumed()) {
46: $str .= $this->read(16384);
47: }
48:
49: $this->seek($pos);
50:
51: return $str;
52: }
53:
54: /**
55: * {@inheritdoc}
56: */
57: public function getSize()
58: {
59: return max($this->body->getSize(), $this->remoteStream->getSize());
60: }
61:
62: /**
63: * {@inheritdoc}
64: * @throws RuntimeException When seeking with SEEK_END or when seeking past the total size of the buffer stream
65: */
66: public function seek($offset, $whence = SEEK_SET)
67: {
68: if ($whence == SEEK_SET) {
69: $byte = $offset;
70: } elseif ($whence == SEEK_CUR) {
71: $byte = $offset + $this->ftell();
72: } else {
73: throw new RuntimeException(__CLASS__ . ' supports only SEEK_SET and SEEK_CUR seek operations');
74: }
75:
76: // You cannot skip ahead past where you've read from the remote stream
77: if ($byte > $this->body->getSize()) {
78: throw new RuntimeException(
79: "Cannot seek to byte {$byte} when the buffered stream only contains {$this->body->getSize()} bytes"
80: );
81: }
82:
83: return $this->body->seek($byte);
84: }
85:
86: /**
87: * {@inheritdoc}
88: */
89: public function rewind()
90: {
91: return $this->seek(0);
92: }
93:
94: /**
95: * Does not support custom rewind functions
96: *
97: * @throws RuntimeException
98: */
99: public function setRewindFunction($callable)
100: {
101: throw new RuntimeException(__CLASS__ . ' does not support custom stream rewind functions');
102: }
103:
104: /**
105: * {@inheritdoc}
106: */
107: public function read($length)
108: {
109: // Perform a regular read on any previously read data from the buffer
110: $data = $this->body->read($length);
111: $remaining = $length - strlen($data);
112:
113: // More data was requested so read from the remote stream
114: if ($remaining) {
115: // If data was written to the buffer in a position that would have been filled from the remote stream,
116: // then we must skip bytes on the remote stream to emulate overwriting bytes from that position. This
117: // mimics the behavior of other PHP stream wrappers.
118: $remoteData = $this->remoteStream->read($remaining + $this->skipReadBytes);
119:
120: if ($this->skipReadBytes) {
121: $len = strlen($remoteData);
122: $remoteData = substr($remoteData, $this->skipReadBytes);
123: $this->skipReadBytes = max(0, $this->skipReadBytes - $len);
124: }
125:
126: $data .= $remoteData;
127: $this->body->write($remoteData);
128: }
129:
130: return $data;
131: }
132:
133: /**
134: * {@inheritdoc}
135: */
136: public function write($string)
137: {
138: // When appending to the end of the currently read stream, you'll want to skip bytes from being read from
139: // the remote stream to emulate other stream wrappers. Basically replacing bytes of data of a fixed length.
140: $overflow = (strlen($string) + $this->ftell()) - $this->remoteStream->ftell();
141: if ($overflow > 0) {
142: $this->skipReadBytes += $overflow;
143: }
144:
145: return $this->body->write($string);
146: }
147:
148: /**
149: * {@inheritdoc}
150: * @link http://php.net/manual/en/function.fgets.php
151: */
152: public function readLine($maxLength = null)
153: {
154: $buffer = '';
155: $size = 0;
156: while (!$this->isConsumed()) {
157: $byte = $this->read(1);
158: $buffer .= $byte;
159: // Break when a new line is found or the max length - 1 is reached
160: if ($byte == PHP_EOL || ++$size == $maxLength - 1) {
161: break;
162: }
163: }
164:
165: return $buffer;
166: }
167:
168: /**
169: * {@inheritdoc}
170: */
171: public function isConsumed()
172: {
173: return $this->body->isConsumed() && $this->remoteStream->isConsumed();
174: }
175:
176: /**
177: * Close both the remote stream and buffer stream
178: */
179: public function close()
180: {
181: return $this->remoteStream->close() && $this->body->close();
182: }
183:
184: /**
185: * {@inheritdoc}
186: */
187: public function setStream($stream, $size = 0)
188: {
189: $this->remoteStream->setStream($stream, $size);
190: }
191:
192: /**
193: * {@inheritdoc}
194: */
195: public function getContentType()
196: {
197: return $this->remoteStream->getContentType();
198: }
199:
200: /**
201: * {@inheritdoc}
202: */
203: public function getContentEncoding()
204: {
205: return $this->remoteStream->getContentEncoding();
206: }
207:
208: /**
209: * {@inheritdoc}
210: */
211: public function getMetaData($key = null)
212: {
213: return $this->remoteStream->getMetaData($key);
214: }
215:
216: /**
217: * {@inheritdoc}
218: */
219: public function getStream()
220: {
221: return $this->remoteStream->getStream();
222: }
223:
224: /**
225: * {@inheritdoc}
226: */
227: public function getWrapper()
228: {
229: return $this->remoteStream->getWrapper();
230: }
231:
232: /**
233: * {@inheritdoc}
234: */
235: public function getWrapperData()
236: {
237: return $this->remoteStream->getWrapperData();
238: }
239:
240: /**
241: * {@inheritdoc}
242: */
243: public function getStreamType()
244: {
245: return $this->remoteStream->getStreamType();
246: }
247:
248: /**
249: * {@inheritdoc}
250: */
251: public function getUri()
252: {
253: return $this->remoteStream->getUri();
254: }
255:
256: /**
257: * Always retrieve custom data from the remote stream
258: *
259: * {@inheritdoc}
260: */
261: public function getCustomData($key)
262: {
263: return $this->remoteStream->getCustomData($key);
264: }
265:
266: /**
267: * Always set custom data on the remote stream
268: *
269: * {@inheritdoc}
270: */
271: public function setCustomData($key, $value)
272: {
273: $this->remoteStream->setCustomData($key, $value);
274:
275: return $this;
276: }
277: }
278: