Stream   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 292
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 38
eloc 64
c 1
b 0
f 0
dl 0
loc 292
ccs 73
cts 73
cp 1
rs 9.36

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getContents() 0 12 3
A read() 0 14 3
A isSeekable() 0 3 2
A detach() 0 5 1
A close() 0 7 2
A __toString() 0 7 2
A isReadable() 0 8 3
A eof() 0 3 2
A getMetadata() 0 13 3
A getSize() 0 7 2
A tell() 0 7 2
A rewind() 0 3 1
A isWritable() 0 8 3
A __construct() 0 5 2
A write() 0 14 3
A attach() 0 9 2
A seek() 0 9 2
1
<?php
2
namespace Fyuze\Http\Message;
3
4
use InvalidArgumentException;
5
use Psr\Http\Message\StreamInterface;
6
use RuntimeException;
7
8
class Stream implements StreamInterface
9
{
10
    /**
11
     * @var resource
12
     */
13
    protected $stream;
14
15
    /**
16
     * {@inheritdoc}
17
     *
18
     * @var  array
19
     * @link http://php.net/manual/function.fopen.php
20
     */
21
    protected static $modes = [
22
        'readable' => ['r', 'r+', 'w+', 'a+', 'x+', 'c+'],
23
        'writable' => ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'],
24
    ];
25
26
    /**
27
     * @param $stream
28
     * @param $mode
29
     */
30 50
    public function __construct($stream, $mode = 'r')
31
    {
32 50
        $resource = is_string($stream) ? fopen($stream, $mode) : $stream;
33
34 50
        $this->stream = $this->attach($resource);
35
    }
36
37
    /**
38
     * {@inheritdoc}
39
     *
40
     * @return void
41
     */
42 2
    public function close()
43
    {
44 2
        if (false === is_resource($this->stream)) {
45 1
            return;
46
        }
47
48 1
        fclose($this->detach());
49
    }
50
51
    /**
52
     * @param $resource
53
     * @return mixed
54
     */
55 50
    public function attach($resource)
56
    {
57 50
        if (false === is_resource($resource)) {
58 1
            throw new InvalidArgumentException(
59 1
                sprintf('You can only attach a resource or stream string, %s provided', gettype($resource))
60 1
            );
61
        }
62
63 50
        return $this->stream = $resource;
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     *
69
     * @return resource|null Underlying PHP stream, if any
70
     */
71 8
    public function detach()
72
    {
73 8
        $stream = $this->stream;
74 8
        $this->stream = null;
75 8
        return $stream;
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     *
81
     * @return int|null Returns the size in bytes if known, or null if unknown.
82
     */
83 3
    public function getSize()
84
    {
85 3
        if (null === $this->stream) {
86 1
            return null;
87
        }
88
89 2
        return fstat($this->stream)['size'];
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     *
95
     * @return int Position of the file pointer
96
     * @throws \RuntimeException on error.
97
     */
98 4
    public function tell()
99
    {
100 4
        if (!$this->stream) {
101 1
            throw new RuntimeException('No stream available, cannot get current position.');
102
        }
103
104 3
        return ftell($this->stream);
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     *
110
     * @return bool
111
     */
112 3
    public function eof()
113
    {
114 3
        return is_resource($this->stream) ? feof($this->stream) : true;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     *
120
     * @return bool
121
     */
122 24
    public function isSeekable()
123
    {
124 24
        return is_resource($this->stream) ? $this->getMetadata('seekable') : false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_resource($this...ata('seekable') : false also could return the type array which is incompatible with the documented return type boolean.
Loading history...
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     *
130
     * @link http://www.php.net/manual/en/function.fseek.php
131
     * @param int $offset Stream offset
132
     * @param int $whence Specifies how the cursor position will be calculated
133
     *     based on the seek offset. Valid values are identical to the built-in
134
     *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
135
     *     offset bytes SEEK_CUR: Set position to current location plus offset
136
     *     SEEK_END: Set position to end-of-stream plus offset.
137
     * @return bool
138
     * @throws \RuntimeException on failure.
139
     */
140 22
    public function seek($offset, $whence = SEEK_SET)
141
    {
142 22
        if (!$this->isSeekable()) {
143 1
            throw new RuntimeException('The current stream is not seekable.');
144
        }
145
146 21
        fseek($this->stream, $offset, $whence);
147
148 21
        return true;
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     *
154
     * @see seek()
155
     * @link http://www.php.net/manual/en/function.fseek.php
156
     * @throws \RuntimeException on failure.
157
     */
158 19
    public function rewind()
159
    {
160 19
        return $this->seek(0);
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     *
166
     * @return bool
167
     */
168 30
    public function isWritable()
169
    {
170 30
        foreach (static::$modes['writable'] as $mode) {
171 30
            if (strpos($this->getMetadata('mode'), $mode) !== false) {
0 ignored issues
show
Bug introduced by
It seems like $this->getMetadata('mode') can also be of type array and null; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

171
            if (strpos(/** @scrutinizer ignore-type */ $this->getMetadata('mode'), $mode) !== false) {
Loading history...
172 28
                return true;
173
            }
174
        }
175 2
        return false;
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     *
181
     * @param string $string The string that is to be written.
182
     * @return int Returns the number of bytes written to the stream.
183
     * @throws \RuntimeException on failure.
184
     */
185 28
    public function write($string)
186
    {
187 28
        if (false === $this->isWritable()) {
188 1
            throw new RuntimeException('The current stream is not writable.');
189
        }
190
191
        // @todo find out how to make fwrite fail
192 27
        if (false === $bytes = fwrite($this->stream, $string)) {
193
            // @codeCoverageIgnoreStart
194
            throw new RuntimeException('Error triggered when attempting to write to stream');
195
            // @codeCoverageIgnoreEnd
196
        }
197
198 27
        return $bytes;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     *
204
     * @return bool
205
     */
206 21
    public function isReadable()
207
    {
208 21
        foreach (static::$modes['readable'] as $mode) {
209 21
            if (strpos($this->getMetadata('mode'), $mode) !== false) {
0 ignored issues
show
Bug introduced by
It seems like $this->getMetadata('mode') can also be of type array and null; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

209
            if (strpos(/** @scrutinizer ignore-type */ $this->getMetadata('mode'), $mode) !== false) {
Loading history...
210 17
                return true;
211
            }
212
        }
213 4
        return false;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     *
219
     * @param int $length Read up to $length bytes from the object and return
220
     *     them. Fewer than $length bytes may be returned if underlying stream
221
     *     call returns fewer bytes.
222
     * @return string Returns the data read from the stream, or an empty string
223
     *     if no bytes are available.
224
     * @throws \RuntimeException if an error occurs.
225
     */
226 3
    public function read($length)
227
    {
228 3
        if (false === $this->isReadable()) {
229 1
            throw new RuntimeException('The current stream is not readable.');
230
        }
231
232
        // @todo find out how to make fread fail
233 2
        if (false === $content = fread($this->stream, $length)) {
234
            // @codeCoverageIgnoreStart
235
            throw new RuntimeException('An error occurred while reading stream.');
236
            // @codeCoverageIgnoreEnd
237
        }
238
239 2
        return $content;
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     *
245
     * @return string
246
     * @throws \RuntimeException if unable to read or an error occurs while
247
     *     reading.
248
     */
249 17
    public function getContents()
250
    {
251 17
        if (false === $this->isReadable()) {
252 3
            throw new RuntimeException('The current stream is not readable.');
253
        }
254
255 14
        if (false === $contents = stream_get_contents($this->stream)) {
256
            // @codeCoverageIgnoreStart
257
            throw new RuntimeException('An error occurred while getting stream contents.');
258
            // @codeCoverageIgnoreEnd
259
        }
260 14
        return $contents;
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     *
266
     * @link http://php.net/manual/en/function.stream-get-meta-data.php
267
     * @param string $key Specific metadata to retrieve.
268
     * @return array|mixed|null Returns an associative array if no key is
269
     *     provided. Returns a specific key value if a key is provided and the
270
     *     value is found, or null if the key is not found.
271
     */
272 39
    public function getMetadata($key = null)
273
    {
274 39
        $meta = stream_get_meta_data($this->stream);
275
276 39
        if ($key === null) {
277 1
            return $meta;
278
        }
279
280 38
        if (array_key_exists($key, $meta)) {
281 37
            return $meta[$key];
282
        }
283
284 1
        return null;
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     *
290
     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
291
     * @return string
292
     */
293 13
    public function __toString()
294
    {
295
        try {
296 13
            $this->rewind();
297 13
            return $this->getContents();
298 2
        } catch (RuntimeException $e) {
299 2
            return '';
300
        }
301
    }
302
}
303