1<?php
2
3namespace GuzzleHttp\Psr7;
4
5use Psr\Http\Message\StreamInterface;
6
7/**
8 * PHP stream implementation.
9 *
10 * @var $stream
11 */
12class Stream implements StreamInterface
13{
14    /**
15     * Resource modes.
16     *
17     * @var string
18     *
19     * @see http://php.net/manual/function.fopen.php
20     * @see http://php.net/manual/en/function.gzopen.php
21     */
22    const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/';
23    const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/';
24
25    private $stream;
26    private $size;
27    private $seekable;
28    private $readable;
29    private $writable;
30    private $uri;
31    private $customMetadata;
32
33    /**
34     * This constructor accepts an associative array of options.
35     *
36     * - size: (int) If a read stream would otherwise have an indeterminate
37     *   size, but the size is known due to foreknowledge, then you can
38     *   provide that size, in bytes.
39     * - metadata: (array) Any additional metadata to return when the metadata
40     *   of the stream is accessed.
41     *
42     * @param resource $stream  Stream resource to wrap.
43     * @param array    $options Associative array of options.
44     *
45     * @throws \InvalidArgumentException if the stream is not a stream resource
46     */
47    public function __construct($stream, $options = [])
48    {
49        if (!is_resource($stream)) {
50            throw new \InvalidArgumentException('Stream must be a resource');
51        }
52
53        if (isset($options['size'])) {
54            $this->size = $options['size'];
55        }
56
57        $this->customMetadata = isset($options['metadata'])
58            ? $options['metadata']
59            : [];
60
61        $this->stream = $stream;
62        $meta = stream_get_meta_data($this->stream);
63        $this->seekable = $meta['seekable'];
64        $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']);
65        $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']);
66        $this->uri = $this->getMetadata('uri');
67    }
68
69    /**
70     * Closes the stream when the destructed
71     */
72    public function __destruct()
73    {
74        $this->close();
75    }
76
77    public function __toString()
78    {
79        try {
80            if ($this->isSeekable()) {
81                $this->seek(0);
82            }
83            return $this->getContents();
84        } catch (\Exception $e) {
85            return '';
86        }
87    }
88
89    public function getContents()
90    {
91        if (!isset($this->stream)) {
92            throw new \RuntimeException('Stream is detached');
93        }
94
95        $contents = stream_get_contents($this->stream);
96
97        if ($contents === false) {
98            throw new \RuntimeException('Unable to read stream contents');
99        }
100
101        return $contents;
102    }
103
104    public function close()
105    {
106        if (isset($this->stream)) {
107            if (is_resource($this->stream)) {
108                fclose($this->stream);
109            }
110            $this->detach();
111        }
112    }
113
114    public function detach()
115    {
116        if (!isset($this->stream)) {
117            return null;
118        }
119
120        $result = $this->stream;
121        unset($this->stream);
122        $this->size = $this->uri = null;
123        $this->readable = $this->writable = $this->seekable = false;
124
125        return $result;
126    }
127
128    public function getSize()
129    {
130        if ($this->size !== null) {
131            return $this->size;
132        }
133
134        if (!isset($this->stream)) {
135            return null;
136        }
137
138        // Clear the stat cache if the stream has a URI
139        if ($this->uri) {
140            clearstatcache(true, $this->uri);
141        }
142
143        $stats = fstat($this->stream);
144        if (isset($stats['size'])) {
145            $this->size = $stats['size'];
146            return $this->size;
147        }
148
149        return null;
150    }
151
152    public function isReadable()
153    {
154        return $this->readable;
155    }
156
157    public function isWritable()
158    {
159        return $this->writable;
160    }
161
162    public function isSeekable()
163    {
164        return $this->seekable;
165    }
166
167    public function eof()
168    {
169        if (!isset($this->stream)) {
170            throw new \RuntimeException('Stream is detached');
171        }
172
173        return feof($this->stream);
174    }
175
176    public function tell()
177    {
178        if (!isset($this->stream)) {
179            throw new \RuntimeException('Stream is detached');
180        }
181
182        $result = ftell($this->stream);
183
184        if ($result === false) {
185            throw new \RuntimeException('Unable to determine stream position');
186        }
187
188        return $result;
189    }
190
191    public function rewind()
192    {
193        $this->seek(0);
194    }
195
196    public function seek($offset, $whence = SEEK_SET)
197    {
198        $whence = (int) $whence;
199
200        if (!isset($this->stream)) {
201            throw new \RuntimeException('Stream is detached');
202        }
203        if (!$this->seekable) {
204            throw new \RuntimeException('Stream is not seekable');
205        }
206        if (fseek($this->stream, $offset, $whence) === -1) {
207            throw new \RuntimeException('Unable to seek to stream position '
208                . $offset . ' with whence ' . var_export($whence, true));
209        }
210    }
211
212    public function read($length)
213    {
214        if (!isset($this->stream)) {
215            throw new \RuntimeException('Stream is detached');
216        }
217        if (!$this->readable) {
218            throw new \RuntimeException('Cannot read from non-readable stream');
219        }
220        if ($length < 0) {
221            throw new \RuntimeException('Length parameter cannot be negative');
222        }
223
224        if (0 === $length) {
225            return '';
226        }
227
228        $string = fread($this->stream, $length);
229        if (false === $string) {
230            throw new \RuntimeException('Unable to read from stream');
231        }
232
233        return $string;
234    }
235
236    public function write($string)
237    {
238        if (!isset($this->stream)) {
239            throw new \RuntimeException('Stream is detached');
240        }
241        if (!$this->writable) {
242            throw new \RuntimeException('Cannot write to a non-writable stream');
243        }
244
245        // We can't know the size after writing anything
246        $this->size = null;
247        $result = fwrite($this->stream, $string);
248
249        if ($result === false) {
250            throw new \RuntimeException('Unable to write to stream');
251        }
252
253        return $result;
254    }
255
256    public function getMetadata($key = null)
257    {
258        if (!isset($this->stream)) {
259            return $key ? null : [];
260        } elseif (!$key) {
261            return $this->customMetadata + stream_get_meta_data($this->stream);
262        } elseif (isset($this->customMetadata[$key])) {
263            return $this->customMetadata[$key];
264        }
265
266        $meta = stream_get_meta_data($this->stream);
267
268        return isset($meta[$key]) ? $meta[$key] : null;
269    }
270}
271