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