Issues (21)

src/Http/Message/Stream.php (1 issue)

1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 *
11
 * Implementation of this file has been influenced by Zend Diactoros
12
 *
13
 * @link    https://github.com/zendframework/zend-diactoros
14
 * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md
15
 */
16
17
declare(strict_types=1);
18
19
namespace Phalcon\Http\Message;
20
21
use Exception;
22
use Phalcon\Helper\Arr;
23
use Phalcon\Http\Message\Traits\StreamTrait;
24
use Psr\Http\Message\StreamInterface;
25
use RuntimeException;
26
27
use function fclose;
28
use function feof;
29
use function fopen;
30
use function fread;
31
use function fseek;
32
use function fstat;
33
use function ftell;
34
use function fwrite;
35
use function get_resource_type;
36
use function is_resource;
37
use function is_string;
38
use function restore_error_handler;
39
use function set_error_handler;
40
use function stream_get_contents;
41
use function stream_get_meta_data;
42
use function strpbrk;
43
44
use const E_WARNING;
45
46
/**
47
 * PSR-7 Stream
48
 *
49
 * @property resource|null   $handle
50
 * @property resource|string $stream
51
 */
52
class Stream implements StreamInterface
53
{
54
    use StreamTrait;
55
56
    /**
57
     * @var resource|null
58
     */
59
    protected $handle = null;
60
61
    /**
62
     * @var resource|string
63
     */
64
    private $stream;
65
66
    /**
67
     * Stream constructor.
68
     *
69
     * @param mixed  $stream
70
     * @param string $mode
71
     */
72 188
    public function __construct($stream, string $mode = "rb")
73
    {
74 188
        $this->setStream($stream, $mode);
75 187
    }
76
77
    /**
78
     * Closes the stream when the destructed.
79
     */
80 126
    public function __destruct()
81
    {
82 126
        $this->close();
83 126
    }
84
85
    /**
86
     * Reads all data from the stream into a string, from the beginning to end.
87
     *
88
     * This method MUST attempt to seek to the beginning of the stream before
89
     * reading data and read the stream until the end is reached.
90
     *
91
     * Warning: This could attempt to load a large amount of data into memory.
92
     *
93
     * This method MUST NOT raise an exception in order to conform with PHP"s
94
     * string casting operations.
95
     *
96
     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
97
     */
98 8
    public function __toString(): string
99
    {
100
        try {
101 8
            if ($this->isReadable()) {
102 8
                if ($this->isSeekable()) {
103 8
                    $this->rewind();
104
                }
105
106 8
                return $this->getContents();
107
            }
108
        } catch (Exception $e) {
109
            unset($e);
110
        }
111
112
        return "";
113
    }
114
115
    /**
116
     * Closes the stream and any underlying resources.
117
     */
118 126
    public function close(): void
119
    {
120 126
        if (null !== $this->handle) {
121 118
            $handle = $this->detach();
122 118
            if (null !== $handle) {
0 ignored issues
show
The condition null !== $handle is always true.
Loading history...
123 118
                fclose($handle);
124
            }
125
        }
126 126
    }
127
128
    /**
129
     * Separates any underlying resources from the stream.
130
     *
131
     * After the stream has been detached, the stream is in an unusable state.
132
     *
133
     * @return resource | null
134
     */
135 124
    public function detach()
136
    {
137 124
        $handle       = $this->handle;
138 124
        $this->handle = null;
139
140 124
        return $handle;
141
    }
142
143
    /**
144
     * Returns true if the stream is at the end of the stream.
145
     */
146 6
    public function eof(): bool
147
    {
148 6
        if ($this->handle) {
149 5
            return feof($this->handle);
150
        }
151
152 1
        return true;
153
    }
154
155
    /**
156
     * Returns the remaining contents in a string
157
     */
158 15
    public function getContents(): string
159
    {
160 15
        $this->checkHandle();
161 15
        $this->checkReadable();
162
163 14
        $data = stream_get_contents($this->handle);
164
165 14
        if (false === $data) {
166
            throw new RuntimeException(
167
                "Could not read from the file/stream"
168
            );
169
        }
170
171 14
        return $data;
172
    }
173
174
    /**
175
     * Get stream metadata as an associative array or retrieve a specific key.
176
     *
177
     * The keys returned are identical to the keys returned from PHP's
178
     * stream_get_meta_data() function.
179
     *
180
     * @param mixed|null $key
181
     *
182
     * @return array|mixed|null
183
     */
184 42
    public function getMetadata($key = null)
185
    {
186 42
        if (null === $this->handle) {
187 1
            return null;
188
        }
189
190 41
        $metadata = stream_get_meta_data($this->handle);
191
192 41
        if (null === $key) {
193 1
            return $metadata;
194
        }
195
196 40
        return Arr::get($metadata, $key, []);
197
    }
198
199
    /**
200
     * Get the size of the stream if known.
201
     */
202 3
    public function getSize(): ?int
203
    {
204 3
        if (null !== $this->handle) {
205 2
            $stats = fstat($this->handle);
206
207 2
            if (false !== $stats) {
208 2
                return Arr::get($stats, "size", null);
209
            }
210
        }
211
212 1
        return null;
213
    }
214
215
    /**
216
     * Returns whether or not the stream is readable.
217
     */
218 26
    public function isReadable(): bool
219
    {
220 26
        $mode = (string) $this->getMetadata("mode");
221
222 26
        return false !== strpbrk($mode, "r+");
223
    }
224
225
    /**
226
     * Returns whether or not the stream is seekable.
227
     */
228 21
    public function isSeekable(): bool
229
    {
230 21
        return (bool) $this->getMetadata("seekable");
231
    }
232
233
    /**
234
     * Returns whether or not the stream is writable.
235
     */
236 10
    public function isWritable(): bool
237
    {
238 10
        $mode = (string) $this->getMetadata("mode");
239
240 10
        return false !== strpbrk($mode, "xwca+");
241
    }
242
243
    /**
244
     * Read data from the stream.
245
     *
246
     * @param int $length
247
     *
248
     * @return string
249
     */
250 8
    public function read($length): string
251
    {
252 8
        $this->checkHandle();
253 7
        $this->checkReadable();
254
255 7
        $length = (int) $length;
256 7
        $data   = fread($this->handle, $length);
257
258 7
        if (false === $data) {
259
            throw new RuntimeException(
260
                "Could not read from the file/stream"
261
            );
262
        }
263
264 7
        return $data;
265
    }
266
267
    /**
268
     * Seek to the beginning of the stream.
269
     *
270
     * If the stream is not seekable, this method will raise an exception;
271
     * otherwise, it will perform a seek(0).
272
     */
273 12
    public function rewind(): void
274
    {
275 12
        $this->seek(0);
276 11
    }
277
278
    /**
279
     * Seek to a position in the stream.
280
     *
281
     * @param int $offset
282
     * @param int $whence
283
     */
284 18
    public function seek($offset, $whence = 0): void
285
    {
286 18
        $this->checkHandle();
287 17
        $this->checkSeekable();
288
289 16
        $offset = (int) $offset;
290 16
        $whence = (int) $whence;
291 16
        $seeker = fseek($this->handle, $offset, $whence);
292
293 16
        if (0 !== $seeker) {
294
            throw new RuntimeException(
295
                "Could not seek on the file pointer"
296
            );
297
        }
298 16
    }
299
300
    /**
301
     * Sets the stream - existing instance
302
     *
303
     * @param mixed  $stream
304
     * @param string $mode
305
     */
306 188
    public function setStream($stream, string $mode = "rb"): void
307
    {
308 188
        $warning = false;
309 188
        $handle  = $stream;
310 188
        if (is_string($stream)) {
311 177
            set_error_handler(
312
                function ($error) use (&$warning) {
313
                    if ($error === E_WARNING) {
314
                        $warning = true;
315
                    }
316 177
                }
317
            );
318
319 177
            $handle = fopen($stream, $mode);
320
321 177
            restore_error_handler();
322
        }
323
        if (
324 188
            $warning ||
325 188
            !is_resource($handle) ||
326 188
            "stream" !== get_resource_type($handle)
327
        ) {
328 1
            throw new RuntimeException(
329
                "The stream provided is not valid " .
330 1
                "(string/resource) or could not be opened."
331
            );
332
        }
333
334 187
        $this->handle = $handle;
335 187
        $this->stream = $stream;
336 187
    }
337
338
    /**
339
     * Returns the current position of the file read/write pointer
340
     *
341
     * @return int
342
     * @throws RuntimeException
343
     */
344 3
    public function tell(): int
345
    {
346 3
        $this->checkHandle();
347
348 2
        $position = ftell($this->handle);
349
350 2
        if (false === $position) {
351
            throw new RuntimeException(
352
                "Could not retrieve the pointer position"
353
            );
354
        }
355
356 2
        return $position;
357
    }
358
359
    /**
360
     * Write data to the stream.
361
     *
362
     * @param string $data
363
     *
364
     * @return int
365
     */
366 8
    public function write($data): int
367
    {
368 8
        $this->checkHandle();
369 7
        $this->checkWritable();
370
371 6
        $bytes = fwrite($this->handle, $data);
372
373 6
        if (false === $bytes) {
374
            throw new RuntimeException(
375
                "Could not write to the file/stream"
376
            );
377
        }
378
379 6
        return $bytes;
380
    }
381
}
382