Test Setup Failed
Push — master ( bbcf9b...73bf86 )
by Kirill
02:39
created

Stream::handleOpen()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 3
nop 1
dl 0
loc 15
ccs 8
cts 8
cp 1
crap 4
rs 9.7666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Stream 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
declare(strict_types=1);
11
12
namespace Serafim\Stream;
13
14
use Serafim\Stream\Exception\NotFoundException;
15
use Serafim\Stream\Exception\NotReadableException;
16
use Serafim\Stream\Exception\StreamCreatingException;
17
use Serafim\Stream\Wrapper\ReadStreamWrapper;
18
19
class Stream implements StreamInterface
20
{
21
    /**
22
     * Default streaming class. Must implement StreamWrapperInterface.
23
     *
24
     * @var string
25
     */
26
    private const DEFAULT_STREAM_WRAPPER = ReadStreamWrapper::class;
27
28
    /**
29
     * Error message in the case of stream name conflicts.
30
     *
31
     * @var string
32
     */
33
    private const STREAM_DUPLICATION_EXCEPTION =
34
        'Could not create stream "%s", because stream ' .
35
        'with same name already has been registered.';
36
37
    /**
38
     * List of registered stream handlers.
39
     *
40
     * @var array<StreamInterface>
41
     */
42
    protected static array $streams = [];
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_ARRAY, expecting T_FUNCTION or T_CONST
Loading history...
43
44
    /**
45
     * Current stream name.
46
     *
47
     * @var string
48
     */
49
    private string $name;
50
51
    /**
52
     * List of handlers processing the source code of the stream.
53
     *
54
     * @var array<\Closure>
55
     */
56
    private array $readHandlers = [];
57
58
    /**
59
     * List of handlers processing file read attempts.
60
     *
61
     * @var array<\Closure>
62
     */
63
    private array $openHandlers = [];
64
65
    /**
66
     * @param string $name
67
     */
68
    private function __construct(string $name)
69
    {
70 25
        $this->name = $name;
71
    }
72 25
73 25
    /**
74
     * Returns a positive result if the handler is valid and allows processing
75
     * of the result.
76
     *
77
     * In order to remove the handler's registration, you need to call
78
     * <code>
79
     * Stream::unregister($stream->getName());
80
     * </code>
81
     *
82
     * @return bool
83
     */
84
    public function isRegistered(): bool
85
    {
86 1
        return static::exists($this->name);
87
    }
88 1
89
    /**
90
     * Creates a new arbitrary stream handler with a randomly generated name.
91
     *
92
     * @param positive-int $complexity Suffix length for the generated stream handler name.
93
     * @param string $wrapper A wrapper class where the read/write stream will be redirected.
94
     * @return Stream|static
95
     * @throws StreamCreatingException
96
     * @throws \Exception
97
     */
98
    public static function new(int $complexity = 8, string $wrapper = self::DEFAULT_STREAM_WRAPPER): self
99
    {
100 15
        assert($complexity > 0, 'Name complexity should be greater than 0');
101
102 15
        $name = 'stream' . \bin2hex(\random_bytes($complexity));
103
104 15
        return static::create($name, $wrapper);
105
    }
106 15
107
    /**
108
     * Creating and registering a new stream with the specified name.
109
     *
110
     * If the existing handler is a system handler (e.g. "php", "memory",
111
     * "phar", "http", etc.), an exception will be thrown. In the event that
112
     * the non-system handler already exists, the existing one will be returned.
113
     *
114
     * @param string $protocol The name of the protocol. May contain
115
     *      alphanumeric sequences, but must begin with a letter.
116
     * @param string $wrapper A wrapper class where the read/write stream
117
     *      will be redirected.
118
     * @return Stream|static
119
     * @throws StreamCreatingException
120
     */
121
    public static function create(string $protocol, string $wrapper = self::DEFAULT_STREAM_WRAPPER): self
122
    {
123 25
        if (isset(static::$streams[$protocol])) {
124
            return static::$streams[$protocol];
125 25
        }
126 1
127
        static::register($protocol, $stream = new static($protocol), $wrapper);
128
129 25
        return $stream;
130
    }
131 24
132
    /**
133
     * Registration of stream protocol handler.
134
     *
135
     * @param string $protocol The name of the protocol. May contain
136
     *      alphanumeric sequences, but must begin with a letter.
137
     * @param string $wrapper A wrapper class where the read/write stream
138
     *      will be redirected.
139
     * @param StreamInterface $stream Instance of StreamInterface where
140
     *      read/write stream will be redirected.
141
     * @return StreamInterface
142
     * @throws StreamCreatingException
143
     */
144
    public static function register(
145
        string $protocol,
146 25
        StreamInterface $stream,
147
        string $wrapper = self::DEFAULT_STREAM_WRAPPER
148
    ): StreamInterface {
149
        static::$streams[$protocol] = $stream;
150
151 25
        if (static::exists($protocol)) {
152
            throw new StreamCreatingException(\sprintf(self::STREAM_DUPLICATION_EXCEPTION, $protocol));
153 25
        }
154 2
155
        \stream_wrapper_register($protocol, $wrapper);
156
157 24
        return $stream;
158
    }
159 24
160
    /**
161
     * Cancels custom handler registration. Ignores built-in handlers, or
162
     * handlers that have been registered with other registrars
163
     * (other than this class).
164
     *
165
     * @param string $protocol The name of the protocol. May contain
166
     *      alphanumeric sequences, but must begin with a letter.
167
     * @return bool Returns true in the event that the deletion of registration
168
     *      was made successfully and false otherwise.
169
     */
170
    public static function unregister(string $protocol): bool
171
    {
172 2
        if (isset(static::$streams[$protocol])) {
173
            unset(static::$streams[$protocol]);
174 2
            \stream_wrapper_unregister($protocol);
175 1
176 1
            return true;
177
        }
178 1
179
        return false;
180
    }
181 1
182
    /**
183
     * Returns a handler by protocol name.
184
     *
185
     * @param string $protocol The name of the protocol. May contain
186
     *      alphanumeric sequences, but must begin with a letter.
187
     * @return StreamInterface
188
     * @throws StreamCreatingException
189
     */
190
    public static function get(string $protocol): StreamInterface
191
    {
192 8
        if (! isset(static::$streams[$protocol])) {
193
            $error = \sprintf('Protocol "%s://" should be registered', $protocol);
194 8
            throw new StreamCreatingException($error);
195 1
        }
196 1
197
        return static::$streams[$protocol];
198
    }
199 7
200
    /**
201
     * Adds a read attempt handler. If no such handler has been registered for
202
     * this stream handler, it will be opened by calling the file_get_contents
203
     * function.
204
     *
205
     * <code>
206
     *  $stream->tryRead(function (string $pathname): ?string {
207
     *      return \is_file($pathname) ? \file_get_contents($pathname) : null;
208
     *  });
209
     * </code>
210
     *
211
     * @param \Closure $then
212
     * @return Stream
213
     */
214
    public function tryRead(\Closure $then): self
215
    {
216 1
        $this->openHandlers[] = $then;
217
218 1
        return $this;
219
    }
220 1
221
    /**
222
     * Adds a source code handler. Each closure passed to this method takes
223
     * the source text as a string and must return a line with the new text.
224
     *
225
     * <code>
226
     *  $stream->onRead(function (string $sources): string {
227
     *      return $sources;
228
     *  });
229
     * </code>
230
     *
231
     * @param \Closure $then
232
     * @return Stream|$this
233
     */
234
    public function onRead(\Closure $then): self
235
    {
236 15
        $this->readHandlers[] = $then;
237
238 15
        return $this;
239
    }
240 15
241
    /**
242
     * Returns the full path with the protocol for the passed name.
243
     *
244
     * @param string $pathname The path to the file string.
245
     * @return string
246
     */
247
    public function pathname(string $pathname): string
248
    {
249 7
        return \sprintf('%s://%s', $this->getName(), $pathname);
250
    }
251 7
252
    /**
253
     * Returns the name of the current stream.
254
     *
255
     * @return string
256
     */
257
    public function getName(): string
258
    {
259 9
        return $this->name;
260
    }
261 9
262
    /**
263
     * Reads a file using the actual file path, using all the registered
264
     * read and processing handlers.
265
     *
266
     * <code>
267
     *  echo Stream::get('protocol')->read('path/to/file.txt');
268
     * </code>
269
     *
270
     * @param string $pathname The path to the file string.
271
     * @return string
272
     * @throws NotFoundException
273
     * @throws NotReadableException
274
     */
275
    public function read(string $pathname): string
276
    {
277 7
        return $this->handleRead($this->handleOpen($pathname));
278
    }
279 7
280
    /**
281
     * @param string $sources
282
     * @return string
283
     */
284
    private function handleRead(string $sources): string
285
    {
286 4
        foreach ($this->readHandlers as $handler) {
287
            $sources = $handler($sources);
288 4
        }
289 1
290
        return $sources;
291
    }
292 4
293
    /**
294
     * @param string $pathname
295
     * @return string
296
     * @throws NotFoundException
297
     * @throws NotReadableException
298
     */
299
    private function handleOpen(string $pathname): string
300
    {
301 7
        if (\count($this->openHandlers)) {
302
            foreach ($this->openHandlers as $handler) {
303 7
                if (\is_string($result = $handler($pathname))) {
304 1
                    return $result;
305 1
                }
306 1
            }
307
        }
308
309
        $this->assertIsFile($pathname);
310
        $this->assertIsReadable($pathname);
311 6
312 4
        return \file_get_contents($pathname);
313
    }
314 3
315
    /**
316
     * @param string $pathname
317
     * @return void
318
     * @throws NotFoundException
319
     */
320
    private function assertIsFile(string $pathname): void
321
    {
322 6
        if (! \is_file($pathname)) {
323
            $error = \sprintf('File %s not found', $pathname);
324 6
            throw new NotFoundException($error);
325 2
        }
326 2
    }
327
328 4
    /**
329
     * @param string $pathname
330
     * @return void
331
     * @throws NotReadableException
332
     */
333
    private function assertIsReadable(string $pathname): void
334
    {
335 4
        if (! \is_readable($pathname)) {
336
            $error = \sprintf('File %s not readable', $pathname);
337 4
            throw new NotReadableException($error);
338 1
        }
339 1
    }
340
341 3
    /**
342
     * Returns whether the transferred protocol is registered.
343
     *
344
     * @param string $protocol
345
     * @return bool
346
     */
347
    public static function exists(string $protocol): bool
348
    {
349 25
        return \in_array($protocol, \stream_get_wrappers(), true);
350
    }
351
}
352