1 <?php
2 namespace GuzzleHttp\Stream;
3 
4 /**
5  * PHP stream implementation
6  */
7 class 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