Passed
Push — master ( f41556...5b26a6 )
by Zaahid
13:23
created

DecoratedCachingStream::cacheBytes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 8
ccs 7
cts 7
cp 1
crap 3
rs 10
1
<?php
2
/**
3
 * This file is part of the ZBateson\StreamDecorators project.
4
 *
5
 * @license http://opensource.org/licenses/bsd-license.php BSD
6
 */
7
8
namespace ZBateson\StreamDecorators;
9
10
use Psr\Http\Message\StreamInterface;
11
use GuzzleHttp\Psr7\StreamDecoratorTrait;
12
use GuzzleHttp\Psr7\BufferStream;
13
use GuzzleHttp\Psr7\CachingStream;
14
use GuzzleHttp\Psr7\FnStream;
15
use GuzzleHttp\Psr7\Utils;
16
17
/**
18
 * A version of Guzzle's CachingStream that will read bytes from one stream,
19
 * write them into another decorated stream, and read them back from a 3rd,
20
 * undecorated, buffered stream where the bytes are written to.
21
 *
22
 * A read operation is basically:
23
 *
24
 * Read from A, write to B (which decorates C), read and return from C (which is
25
 * backed by a BufferedStream).
26
 *
27
 * Note that the DecoratedCachingStream doesn't support write operations.
28
 */
29
class DecoratedCachingStream implements StreamInterface
30
{
31
    use StreamDecoratorTrait;
32
33
    /**
34
     *  @var StreamInterface the stream to read from and fill writeStream with
35
     */
36
    private StreamInterface $readStream;
37
38
    /**
39
     * @var StreamInterface the underlying undecorated stream to read from,
40
     *      where $writeStream is being written to
41
     */
42
    private StreamInterface $stream;
43
44
    /**
45
     * @var StreamInterface decorated $stream that will be written to for
46
     *      caching that wraps $stream.  Once filled, the stream is closed so it
47
     *      supports a Base64Stream which writes bytes at the end.
48
     */
49
    private ?StreamInterface $writeStream;
50
51
    /**
52
     * @var int Minimum buffer read length. At least this many bytes will be
53
     *      read and cached into $writeStream on each call to read from
54
     *      $readStream
55
     */
56
    private int $minBytesCache;
57
58
    /**
59
     * @param StreamInterface $stream Stream to cache. The cursor is assumed to
60
     *        be at the beginning of the stream.
61
     * @param callable(StreamInterface) : StreamInterface $decorator takes the
0 ignored issues
show
Documentation Bug introduced by
The doc comment : StreamInterface at position 0 could not be parsed: Unknown type name ':' at position 0 in : StreamInterface.
Loading history...
62
     *        passed StreamInterface and decorates it, and returns the decorated
63
     *        StreamInterface
64
     */
65 7
    public function __construct(
66
        StreamInterface $stream,
67
        callable $decorator,
68
        int $minBytesCache = 16384
69
    ) {
70 7
        $this->readStream = $stream;
71 7
        $bufferStream = new TellZeroStream(new BufferStream());
72 7
        $this->stream = new CachingStream($bufferStream);
73 7
        $this->writeStream = $decorator(new NonClosingStream($bufferStream));
74 7
        $this->minBytesCache = $minBytesCache;
75
    }
76
77 2
    public function getSize(): ?int
78
    {
79
        // the decorated stream could be a different size
80 2
        $this->cacheEntireStream();
81 2
        return $this->stream->getSize();
82
    }
83
84 2
    public function rewind(): void
85
    {
86 2
        $this->seek(0);
87
    }
88
89 2
    public function seek($offset, $whence = SEEK_SET): void
90
    {
91 2
        if ($whence === SEEK_SET) {
92 2
            $byte = $offset;
93 1
        } elseif ($whence === SEEK_CUR) {
94 1
            $byte = $offset + $this->tell();
95 1
        } elseif ($whence === SEEK_END) {
96 1
            $size = $this->getSize();
97 1
            $byte = $size + $offset;
98
        } else {
99
            throw new \InvalidArgumentException('Invalid whence');
100
        }
101
102 2
        $diff = $byte - $this->stream->getSize();
103
104 2
        if ($diff > 0) {
105
            // Read the remoteStream until we have read in at least the amount
106
            // of bytes requested, or we reach the end of the file.
107 1
            while ($diff > 0 && !$this->readStream->eof()) {
108 1
                $this->read($diff);
109 1
                $diff = $byte - $this->stream->getSize();
110
            }
111
        } else {
112
            // We can just do a normal seek since we've already seen this byte.
113 2
            $this->stream->seek($byte);
114
        }
115
    }
116
117 6
    private function cacheBytes(int $size) : void {
118 6
        if (!$this->readStream->eof()) {
119 6
            $data = $this->readStream->read(max($this->minBytesCache, $size));
120 6
            $this->writeStream->write($data);
0 ignored issues
show
Bug introduced by
The method write() 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

120
            $this->writeStream->/** @scrutinizer ignore-call */ 
121
                                write($data);

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...
121 6
            if ($this->readStream->eof()) {
122
                // needed because Base64Stream writes bytes on closing
123 6
                $this->writeStream->close();
124 6
                $this->writeStream = null;
125
            }
126
        }
127
    }
128
129 6
    public function read($length): string
130
    {
131 6
        $data = $this->stream->read($length);
132 6
        $remaining = $length - strlen($data);
133 6
        if ($remaining > 0) {
134 6
            $this->cacheBytes($remaining);
135 6
            $data .= $this->stream->read($remaining);
136
        }
137 6
        return $data;
138
    }
139
140 1
    public function isWritable(): bool
141
    {
142 1
        return false;
143
    }
144
145 1
    public function write($string): int
146
    {
147 1
        throw new \RuntimeException('Cannot write to a DecoratedCachingStream');
148
    }
149
150 5
    public function eof(): bool
151
    {
152 5
        return $this->stream->eof() && $this->readStream->eof();
153
    }
154
155
    /**
156
     * Close both the remote stream and buffer stream
157
     */
158 2
    public function close(): void
159
    {
160 2
        $this->readStream->close();
161 2
        $this->stream->close();
162 2
        if ($this->writeStream !== null) {
163
            $this->writeStream->close();
164
        }
165
    }
166
167 2
    private function cacheEntireStream(): int
168
    {
169
        // as-is from CachingStream
170 2
        $target = new FnStream(['write' => 'strlen']);
171 2
        Utils::copyToStream($this, $target);
172
173 2
        return $this->tell();
174
    }
175
}