Passed
Push — master ( a6853f...a735f8 )
by Aleksei
31:53 queued 20:52
created

StreamWrapper::stream_stat()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
ccs 3
cts 4
cp 0.75
rs 10
cc 2
nc 2
nop 0
crap 2.0625
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Streams;
6
7
use Psr\Http\Message\StreamInterface;
8
use Spiral\Streams\Exception\WrapperException;
9
10
/**
11
 * Spiral converter of PSR-7 streams to virtual filenames. Static as hell.
12
 */
13
final class StreamWrapper
14
{
15
    /**
16
     * Stream context.
17
     *
18
     * @var resource
19
     */
20
    public mixed $context = null;
21
22
    private static bool $registered = false;
23
24
    /**
25
     * Uris associated with StreamInterfaces.
26
     */
27
    private static array $uris = [];
28
29
    private static array $modes = [
30
        'r'   => 33060,
31
        'rb'  => 33060,
32
        'r+'  => 33206,
33
        'rb+' => 33206,
34
        'w'   => 33188,
35
        'wb'  => 33188,
36
    ];
37
    private ?StreamInterface $stream = null;
38
    private string $mode = '';
39
40
    /**
41
     * Register stream wrapper.
42
     */
43 4
    public static function register(): void
44
    {
45 4
        if (self::$registered) {
46 3
            return;
47
        }
48
49 1
        \stream_wrapper_register('spiral', self::class);
50
51 1
        self::$registered = true;
52
    }
53
54
    /**
55
     * Check if given uri or stream has been allocated.
56
     */
57 2
    public static function has(StreamInterface|string $file): bool
58
    {
59 2
        if ($file instanceof StreamInterface) {
60 1
            $file = 'spiral://' . \spl_object_hash($file);
61
        }
62
63 2
        return isset(self::$uris[$file]);
64
    }
65
66
    /**
67
     * Create StreamInterface associated resource.
68
     *
69
     * @return resource
70
     * @throws WrapperException
71
     */
72 1
    public static function getResource(StreamInterface $stream)
73
    {
74 1
        $mode = null;
75 1
        if ($stream->isReadable()) {
76 1
            $mode = 'r';
77
        }
78
79 1
        if ($stream->isWritable()) {
80 1
            $mode = !empty($mode) ? 'r+' : 'w';
81
        }
82
83 1
        if (empty($mode)) {
84
            throw new WrapperException('Stream is not available in read or write modes');
85
        }
86
87 1
        $result = \fopen(self::getFilename($stream), $mode);
88 1
        return $result === false
89
            ? throw new WrapperException(\sprintf('Unable to open stream `%s`.', $stream->getMetadata('uri')))
0 ignored issues
show
Bug introduced by
It seems like $stream->getMetadata('uri') can also be of type array; however, parameter $values of sprintf() does only seem to accept double|integer|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

89
            ? throw new WrapperException(\sprintf('Unable to open stream `%s`.', /** @scrutinizer ignore-type */ $stream->getMetadata('uri')))
Loading history...
90 1
            : $result;
91
    }
92
93
    /**
94
     * Register StreamInterface and get unique url for it.
95
     */
96 4
    public static function getFilename(StreamInterface $stream): string
97
    {
98 4
        self::register();
99
100 4
        $uri = 'spiral://' . \spl_object_hash($stream);
101 4
        self::$uris[$uri] = $stream;
102
103 4
        return $uri;
104
    }
105
106
    /**
107
     * Free uri dedicated to specified StreamInterface. Method is useful for long living
108
     * applications. You must close resource by yourself!
109
     *
110
     * @param string|StreamInterface $file String uri or StreamInterface.
111
     */
112 1
    public static function release(StreamInterface|string $file): void
113
    {
114 1
        if ($file instanceof StreamInterface) {
115
            $file = 'spiral://' . \spl_object_hash($file);
116
        }
117
118 1
        unset(self::$uris[$file]);
119
    }
120
121
    /**
122
     * Check if StreamInterface ended.
123
     */
124 4
    public function stream_eof(): bool
125
    {
126 4
        if ($this->stream === null) {
127
            throw new WrapperException('Stream is not opened.');
128
        }
129
130 4
        return $this->stream->eof();
131
    }
132
133
    /**
134
     * Open pre-mocked StreamInterface by it's unique uri.
135
     */
136 5
    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

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

136
    public function stream_open(string $path, string $mode, /** @scrutinizer ignore-unused */ int $options, ?string &$opened_path): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $opened_path is not used and could be removed. ( Ignorable by Annotation )

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

136
    public function stream_open(string $path, string $mode, int $options, /** @scrutinizer ignore-unused */ ?string &$opened_path): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
137
    {
138 5
        if (!isset(self::$uris[$path])) {
139 1
            return false;
140
        }
141
142 4
        $this->stream = self::$uris[$path];
143 4
        $this->mode = $mode;
144
145 4
        $this->stream->rewind();
0 ignored issues
show
Bug introduced by
The method rewind() does not exist on null. ( Ignorable by Annotation )

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

145
        $this->stream->/** @scrutinizer ignore-call */ 
146
                       rewind();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
146
147 4
        return true;
148
    }
149
150
    /**
151
     * Read data from StreamInterface.
152
     */
153 4
    public function stream_read(int $length): string|false
154
    {
155 4
        if ($this->stream === null) {
156
            throw new WrapperException('Stream is not opened.');
157
        }
158
159 4
        return $this->stream->isReadable() ? $this->stream->read($length) : false;
160
    }
161
162
    /**
163
     * Seek into StreamInterface.
164
     */
165 1
    public function stream_seek(int $offset, int $whence = SEEK_SET): bool
166
    {
167 1
        if ($this->stream === null) {
168
            throw new WrapperException('Stream is not opened.');
169
        }
170
171
        //Note, MongoDB native streams DO NOT support seeking at the moment
172
        //@see https://jira.mongodb.org/browse/PHPLIB-213
173 1
        $this->stream->seek($offset, $whence);
174
175 1
        return true;
176
    }
177
178
    /**
179
     * Get StreamInterface stats.
180
     *
181
     * @see stat()
182
     */
183 3
    public function stream_stat(): array
184
    {
185 3
        if ($this->stream === null) {
186
            throw new WrapperException('Stream is not opened.');
187
        }
188
189 3
        return $this->getStreamStats($this->stream);
190
    }
191
192
    /**
193
     * Get StreamInterface position.
194
     */
195 1
    public function stream_tell(): int
196
    {
197 1
        if ($this->stream === null) {
198
            throw new WrapperException('Stream is not opened.');
199
        }
200
201
        //Note, MongoDB native streams DO NOT support seeking at the moment
202
        //@see https://jira.mongodb.org/browse/PHPLIB-213
203 1
        return $this->stream->tell();
204
    }
205
206
    /**
207
     * Write content into StreamInterface.
208
     */
209 1
    public function stream_write(string $data): int
210
    {
211 1
        if ($this->stream === null) {
212
            throw new WrapperException('Stream is not opened.');
213
        }
214
215 1
        return $this->stream->write($data);
216
    }
217
218
    /**
219
     * Get stats based on wrapped StreamInterface by it's mocked uri.
220
     *
221
     * @see stat()
222
     */
223 3
    public function url_stat(string $path, int $flag): array|false
224
    {
225
        try {
226 3
            if (isset(self::$uris[$path])) {
227 3
                return $this->getStreamStats(self::$uris[$path]);
228
            }
229
        } catch (\Throwable $e) {
230
            if (($flag & \STREAM_URL_STAT_QUIET) === \STREAM_URL_STAT_QUIET) {
231
                return false;
232
            }
233
            \trigger_error($e->getMessage(), \E_USER_ERROR);
234
        }
235
236 1
        return false;
237
    }
238
239
    /**
240
     * Helper method used to correctly resolve StreamInterface stats.
241
     */
242 4
    private function getStreamStats(StreamInterface $stream): array
243
    {
244 4
        $mode = $this->mode;
245 4
        if (empty($mode)) {
246 2
            if ($stream->isReadable()) {
247 2
                $mode = 'r';
248
            }
249
250 2
            if ($stream->isWritable()) {
251 1
                $mode = !empty($mode) ? 'r+' : 'w';
252
            }
253
        }
254
255 4
        return [
256 4
            'dev'     => 0,
257 4
            'ino'     => 0,
258 4
            'mode'    => self::$modes[$mode],
259 4
            'nlink'   => 0,
260 4
            'uid'     => 0,
261 4
            'gid'     => 0,
262 4
            'rdev'    => 0,
263 4
            'size'    => (string) $stream->getSize(),
264 4
            'atime'   => 0,
265 4
            'mtime'   => 0,
266 4
            'ctime'   => 0,
267 4
            'blksize' => 0,
268 4
            'blocks'  => 0,
269 4
        ];
270
    }
271
}
272