Completed
Push — master ( cfad91...f5112d )
by Filipe
02:20
created

Stream::tell()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.1825

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
ccs 8
cts 11
cp 0.7272
rs 9.4285
cc 3
eloc 9
nc 3
nop 0
crap 3.1825
1
<?php
2
3
/**
4
 * This file is part of slick/http package
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace Slick\Http;
11
12
use Psr\Http\Message\StreamInterface;
13
use Slick\Http\Exception\InvalidArgumentException;
14
use RuntimeException;
15
16
/**
17
 * Implementation of PSR HTTP streams
18
 *
19
 * @package Slick\Http
20
 * @author  Filipe Silva <[email protected]>
21
 */
22
class Stream implements StreamInterface
23
{
24
25
    /**
26
     * @var resource
27
     */
28
    protected $resource;
29
30
    /**
31
     * @var string|resource
32
     */
33
    protected $stream;
34
35
    /**
36
     * @param string|resource $stream
37
     * @param string $mode Mode with which to open stream
38
     * @throws InvalidArgumentException
39
     */
40 274
    public function __construct($stream, $mode = 'r')
41
    {
42 274
        $this->setStream($stream, $mode);
43 274
    }
44
45
    /**
46
     * Reads all data from the stream into a string, from the beginning to end.
47
     *
48
     * This method MUST attempt to seek to the beginning of the stream before
49
     * reading data and read the stream until the end is reached.
50
     *
51
     * Warning: This could attempt to load a large amount of data into memory.
52
     *
53
     * This method MUST NOT raise an exception in order to conform with PHP's
54
     * string casting operations.
55
     *
56
     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
57
     * @return string
58
     */
59 8
    public function __toString()
60
    {
61 8
        if (!$this->isReadable()) {
62 2
            return '';
63
        }
64
65 6
        $value = '';
66
        try {
67 6
            $this->rewind();
68 6
            $value = $this->getContents();
69 3
        } catch (RuntimeException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
70
        }
71 6
        return $value;
72
73
    }
74
75
    /**
76
     * Closes the stream and any underlying resources.
77
     *
78
     * @return void
79
     */
80 6
    public function close()
81
    {
82 6
        if (! $this->resource) {
83 2
            return;
84
        }
85 4
        $resource = $this->detach();
86 4
        fclose($resource);
87 4
    }
88
89
    /**
90
     * Separates any underlying resources from the stream.
91
     *
92
     * After the stream has been detached, the stream is in an unusable state.
93
     *
94
     * @return resource|null Underlying PHP stream, if any
95
     */
96 26
    public function detach()
97
    {
98 26
        $resource = $this->resource;
99 26
        $this->resource = null;
100 26
        return $resource;
101
    }
102
103
    /**
104
     * Attach a new stream/resource to the instance.
105
     *
106
     * @param string|resource $resource
107
     * @param string $mode
108
     * @throws InvalidArgumentException for stream identifier that cannot be
109
     *     cast to a resource
110
     * @throws InvalidArgumentException for non-resource stream
111
     */
112 32
    public function attach($resource, $mode = 'r')
113
    {
114 32
        $this->setStream($resource, $mode);
115 14
    }
116
117
    /**
118
     * Get the size of the stream if known.
119
     *
120
     * @return int|null Returns the size in bytes if known, or null if unknown.
121
     */
122 6
    public function getSize()
123
    {
124 6
        if (null === $this->resource) {
125 2
            return null;
126
        }
127 4
        $stats = fstat($this->resource);
128 4
        return $stats['size'];
129
    }
130
131
    /**
132
     * Returns the current position of the file read/write pointer
133
     *
134
     * @return int Position of the file pointer
135
     * @throws \RuntimeException on error.
136
     */
137 8
    public function tell()
138
    {
139 8
        if (!$this->resource) {
140 2
            throw new RuntimeException(
141 1
                'No resource available; cannot tell position'
142 1
            );
143
        }
144 6
        $result = ftell($this->resource);
145 6
        if (!is_int($result)) {
146
            throw new RuntimeException(
147
                'Error occurred during tell operation'
148
            );
149
        }
150 6
        return $result;
151
    }
152
153
    /**
154
     * Returns true if the stream is at the end of the stream.
155
     *
156
     * @return bool
157
     */
158 8
    public function eof()
159
    {
160 8
        $return = true;
161 8
        if ($this->resource) {
162 6
            $return = feof($this->resource);
163 3
        }
164 8
        return $return;
165
    }
166
167
    /**
168
     * Returns whether or not the stream is seekable.
169
     *
170
     * @return bool
171
     */
172 20
    public function isSeekable()
173
    {
174 20
        $seekable = false;
175 20
        if ($this->resource) {
176 18
            $meta = stream_get_meta_data($this->resource);
177 18
            $seekable = $meta['seekable'];
178 9
        }
179 20
        return $seekable;
180
    }
181
182
    /**
183
     * Seek to a position in the stream.
184
     *
185
     * @link http://www.php.net/manual/en/function.fseek.php
186
     * @param int $offset Stream offset
187
     * @param int $whence Specifies how the cursor position will be calculated
188
     *     based on the seek offset. Valid values are identical to the built-in
189
     *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
190
     *     offset bytes SEEK_CUR: Set position to current location plus offset
191
     *     SEEK_END: Set position to end-of-stream plus offset.
192
     * @throws \RuntimeException on failure.
193
     *
194
     * @return bool
195
     */
196 18
    public function seek($offset, $whence = SEEK_SET)
197
    {
198 18
        if (! $this->resource) {
199 2
            throw new RuntimeException('No resource available; cannot seek position');
200
        }
201 16
        if (! $this->isSeekable()) {
202
            throw new RuntimeException('Stream is not seekable');
203
        }
204 16
        $result = fseek($this->resource, $offset, $whence);
205 16
        if (0 !== $result) {
206
            throw new RuntimeException('Error seeking within stream');
207
        }
208 16
        return true;
209
    }
210
211
    /**
212
     * Seek to the beginning of the stream.
213
     *
214
     * If the stream is not seekable, this method will raise an exception;
215
     * otherwise, it will perform a seek(0).
216
     *
217
     * @see seek()
218
     * @link http://www.php.net/manual/en/function.fseek.php
219
     * @throws \RuntimeException on failure.
220
     *
221
     * @return bool
222
     */
223 12
    public function rewind()
224
    {
225 12
        return $this->seek(0);
226
    }
227
228
    /**
229
     * Returns whether or not the stream is writable.
230
     *
231
     * @return bool
232
     */
233 80
    public function isWritable()
234
    {
235 80
        if (! $this->resource) {
236 2
            return false;
237
        }
238 78
        $meta = stream_get_meta_data($this->resource);
239 78
        $mode = $meta['mode'];
240
        return (
241 78
            strstr($mode, 'x')
242 70
            || strstr($mode, 'w')
243 49
            || strstr($mode, 'c')
244 24
            || strstr($mode, 'a')
245 49
            || strstr($mode, '+')
246 39
        );
247
    }
248
249
    /**
250
     * Write data to the stream.
251
     *
252
     * @param string $string The string that is to be written.
253
     * @return int Returns the number of bytes written to the stream.
254
     * @throws \RuntimeException on failure.
255
     */
256 34 View Code Duplication
    public function write($string)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
257
    {
258 34
        if (! $this->resource) {
259 2
            throw new RuntimeException('No resource available; cannot write');
260
        }
261 32
        if (! $this->isWritable()) {
262 2
            throw new RuntimeException('Stream is not writable');
263
        }
264 30
        $result = fwrite($this->resource, $string);
265 30
        if (false === $result) {
266
            throw new RuntimeException('Error writing to stream');
267
        }
268 30
        return $result;
269
    }
270
271
    /**
272
     * Returns whether or not the stream is readable.
273
     *
274
     * @return bool
275
     */
276 64
    public function isReadable()
277
    {
278 64
        if (! $this->resource) {
279 2
            return false;
280
        }
281 62
        $meta = stream_get_meta_data($this->resource);
282 62
        $mode = $meta['mode'];
283 62
        return (strstr($mode, 'r') || strstr($mode, '+'));
284
    }
285
286
    /**
287
     * Read data from the stream.
288
     *
289
     * @param int $length Read up to $length bytes from the object and return
290
     *     them. Fewer than $length bytes may be returned if underlying stream
291
     *     call returns fewer bytes.
292
     * @return string Returns the data read from the stream, or an empty string
293
     *     if no bytes are available.
294
     * @throws \RuntimeException if an error occurs.
295
     */
296 6 View Code Duplication
    public function read($length)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
297
    {
298 6
        if (! $this->resource) {
299 2
            throw new RuntimeException('No resource available; cannot read');
300
        }
301 4
        if (! $this->isReadable()) {
302
            throw new RuntimeException('Stream is not readable');
303
        }
304 4
        $result = fread($this->resource, $length);
305 4
        if (false === $result) {
306
            throw new RuntimeException('Error reading stream');
307
        }
308 4
        return $result;
309
    }
310
311
    /**
312
     * Returns the remaining contents in a string
313
     *
314
     * @return string
315
     * @throws \RuntimeException if unable to read or an error occurs while
316
     *     reading.
317
     */
318 12
    public function getContents()
319
    {
320 12
        if (! $this->isReadable()) {
321 2
            throw new RuntimeException('Stream is not readable');
322
        }
323 10
        $result = stream_get_contents($this->resource);
324 10
        if (false === $result) {
325
            throw new RuntimeException('Error reading from stream');
326
        }
327 10
        return $result;
328
    }
329
330
    /**
331
     * Get stream metadata as an associative array or retrieve a specific key.
332
     *
333
     * The keys returned are identical to the keys returned from PHP's
334
     * stream_get_meta_data() function.
335
     *
336
     * @link http://php.net/manual/en/function.stream-get-meta-data.php
337
     * @param string $key Specific metadata to retrieve.
338
     * @return array|mixed|null Returns an associative array if no key is
339
     *     provided. Returns a specific key value if a key is provided and the
340
     *     value is found, or null if the key is not found.
341
     */
342 6
    public function getMetadata($key = null)
343
    {
344 6
        if (null === $key) {
345 2
            return stream_get_meta_data($this->resource);
346
        }
347 4
        $metadata = stream_get_meta_data($this->resource);
348 4
        if (! array_key_exists($key, $metadata)) {
349 2
            return null;
350
        }
351 2
        return $metadata[$key];
352
    }
353
354
    /**
355
     * Set the internal stream resource.
356
     *
357
     * @param string|resource $stream String stream target or stream resource.
358
     * @param string $mode Resource mode for stream target.
359
     * @throws InvalidArgumentException for invalid streams or resources.
360
     */
361 274
    private function setStream($stream, $mode = 'r')
362
    {
363 274
        $error    = null;
364 274
        $resource = $stream;
365 274
        if (is_string($stream)) {
366 254
            set_error_handler(function ($e) use (&$error) {
367 2
                $error = $e;
368 254
            }, E_WARNING);
369 254
            $resource = fopen($stream, $mode);
370 254
            restore_error_handler();
371 127
        }
372 274
        if ($error) {
373 2
            throw new InvalidArgumentException(
374 1
                'Invalid stream reference provided'
375 1
            );
376
        }
377
        if (
378 274
            ! is_resource($resource) ||
379 274
            'stream' !== get_resource_type($resource)
380 137
        ) {
381 20
            throw new InvalidArgumentException(
382
                'Invalid stream provided; must be a string stream identifier '.
383 10
                'or stream resource'
384 10
            );
385
        }
386 274
        if ($stream !== $resource) {
387 254
            $this->stream = $stream;
388 127
        }
389 274
        $this->resource = $resource;
390
    }
391
}