Completed
Push — master ( 3dc44b...9f6852 )
by Tobias
02:56
created

Stream::__toString()   B

Complexity

Conditions 8
Paths 22

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.0291

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 12
cts 13
cp 0.9231
rs 8.4444
c 0
b 0
f 0
cc 8
nc 22
nop 0
crap 8.0291
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Nyholm\Psr7;
6
7
use Psr\Http\Message\StreamInterface;
8
use Symfony\Component\Debug\ErrorHandler as SymfonyLegacyErrorHandler;
9
use Symfony\Component\ErrorHandler\ErrorHandler as SymfonyErrorHandler;
10
11
/**
12
 * @author Michael Dowling and contributors to guzzlehttp/psr7
13
 * @author Tobias Nyholm <[email protected]>
14
 * @author Martijn van der Ven <[email protected]>
15
 */
16
final class Stream implements StreamInterface
17
{
18
    /** @var resource|null A resource reference */
19
    private $stream;
20
21
    /** @var bool */
22
    private $seekable;
23
24
    /** @var bool */
25
    private $readable;
26
27
    /** @var bool */
28
    private $writable;
29
30
    /** @var array|mixed|void|null */
31
    private $uri;
32
33
    /** @var int|null */
34
    private $size;
35
36
    /** @var array Hash of readable and writable stream types */
37
    private const READ_WRITE_HASH = [
38
        'read' => [
39
            'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
40
            'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
41
            'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
42
            'x+t' => true, 'c+t' => true, 'a+' => true,
43
        ],
44
        'write' => [
45
            'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
46
            'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
47
            'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
48
            'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
49
        ],
50
    ];
51
52 55
    private function __construct()
53
    {
54 55
    }
55
56
    /**
57
     * Creates a new PSR-7 stream.
58
     *
59
     * @param string|resource|StreamInterface $body
60
     *
61
     * @throws \InvalidArgumentException
62
     */
63 55
    public static function create($body = ''): StreamInterface
64
    {
65 55
        if ($body instanceof StreamInterface) {
66
            return $body;
67
        }
68
69 55
        if (\is_string($body)) {
70 21
            $resource = \fopen('php://temp', 'rw+');
71 21
            \fwrite($resource, $body);
72 21
            $body = $resource;
73
        }
74
75 55
        if (\is_resource($body)) {
76 55
            $new = new self();
77 55
            $new->stream = $body;
78 55
            $meta = \stream_get_meta_data($new->stream);
79 55
            $new->seekable = $meta['seekable'] && 0 === \fseek($new->stream, 0, \SEEK_CUR);
80 55
            $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
81 55
            $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
82 55
            $new->uri = $new->getMetadata('uri');
83
84 55
            return $new;
85
        }
86
87
        throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.');
88
    }
89
90
    /**
91
     * Closes the stream when the destructed.
92
     */
93 55
    public function __destruct()
94
    {
95 55
        $this->close();
96 55
    }
97
98
    /**
99
     * @return string
100
     */
101 15
    public function __toString()
102
    {
103
        try {
104 15
            if ($this->isSeekable()) {
105 14
                $this->seek(0);
106
            }
107
108 15
            return $this->getContents();
109 1
        } catch (\Throwable $e) {
110 1
            if (\PHP_VERSION_ID >= 70400) {
111
                throw $e;
112
            }
113
114 1
            if (\is_array($errorHandler = \set_error_handler('var_dump'))) {
115 1
                $errorHandler = $errorHandler[0] ?? null;
116
            }
117 1
            \restore_error_handler();
118
119 1
            if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) {
120 1
                return \trigger_error((string) $e, \E_USER_ERROR);
1 ignored issue
show
Bug Best Practice introduced by
The return type of return \trigger_error((string) $e, \E_USER_ERROR); (boolean) is incompatible with the return type declared by the interface Psr\Http\Message\StreamInterface::__toString of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
121
            }
122
123 1
            return '';
124
        }
125
    }
126
127 55
    public function close(): void
128
    {
129 55
        if (isset($this->stream)) {
130 53
            if (\is_resource($this->stream)) {
131 53
                \fclose($this->stream);
132
            }
133 53
            $this->detach();
134
        }
135 55
    }
136
137 55
    public function detach()
138
    {
139 55
        if (!isset($this->stream)) {
140 1
            return null;
141
        }
142
143 55
        $result = $this->stream;
144 55
        unset($this->stream);
145 55
        $this->size = $this->uri = null;
146 55
        $this->readable = $this->writable = $this->seekable = false;
147
148 55
        return $result;
149
    }
150
151 18
    public function getSize(): ?int
152
    {
153 18
        if (null !== $this->size) {
154 2
            return $this->size;
155
        }
156
157 18
        if (!isset($this->stream)) {
158 2
            return null;
159
        }
160
161
        // Clear the stat cache if the stream has a URI
162 16
        if ($this->uri) {
163 16
            \clearstatcache(true, $this->uri);
164
        }
165
166 16
        $stats = \fstat($this->stream);
167 16
        if (isset($stats['size'])) {
168 16
            $this->size = $stats['size'];
169
170 16
            return $this->size;
171
        }
172
173
        return null;
174
    }
175
176 3
    public function tell(): int
177
    {
178 3
        if (false === $result = \ftell($this->stream)) {
179
            throw new \RuntimeException('Unable to determine stream position');
180
        }
181
182 2
        return $result;
183
    }
184
185 8
    public function eof(): bool
186
    {
187 8
        return !$this->stream || \feof($this->stream);
188
    }
189
190 24
    public function isSeekable(): bool
191
    {
192 24
        return $this->seekable;
193
    }
194
195 28
    public function seek($offset, $whence = \SEEK_SET): void
196
    {
197 28
        if (!$this->seekable) {
198 2
            throw new \RuntimeException('Stream is not seekable');
199
        }
200
201 26
        if (-1 === \fseek($this->stream, $offset, $whence)) {
202
            throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true));
203
        }
204 26
    }
205
206 8
    public function rewind(): void
207
    {
208 8
        $this->seek(0);
209 7
    }
210
211 5
    public function isWritable(): bool
212
    {
213 5
        return $this->writable;
214
    }
215
216 9
    public function write($string): int
217
    {
218 9
        if (!$this->writable) {
219 2
            throw new \RuntimeException('Cannot write to a non-writable stream');
220
        }
221
222
        // We can't know the size after writing anything
223 8
        $this->size = null;
224
225 8
        if (false === $result = \fwrite($this->stream, $string)) {
226
            throw new \RuntimeException('Unable to write to stream');
227
        }
228
229 8
        return $result;
230
    }
231
232 5
    public function isReadable(): bool
233
    {
234 5
        return $this->readable;
235
    }
236
237 9
    public function read($length): string
238
    {
239 9
        if (!$this->readable) {
240 2
            throw new \RuntimeException('Cannot read from non-readable stream');
241
        }
242
243 7
        return \fread($this->stream, $length);
244
    }
245
246 17
    public function getContents(): string
247
    {
248 17
        if (!isset($this->stream)) {
249 1
            throw new \RuntimeException('Unable to read stream contents');
250
        }
251
252 16
        if (false === $contents = \stream_get_contents($this->stream)) {
253
            throw new \RuntimeException('Unable to read stream contents');
254
        }
255
256 16
        return $contents;
257
    }
258
259 55
    public function getMetadata($key = null)
260
    {
261 55
        if (!isset($this->stream)) {
262 1
            return $key ? null : [];
263
        }
264
265 55
        $meta = \stream_get_meta_data($this->stream);
266
267 55
        if (null === $key) {
268 1
            return $meta;
269
        }
270
271 55
        return $meta[$key] ?? null;
272
    }
273
}
274