Completed
Push — master ( 44b103...5bf1b6 )
by Riikka
02:48
created

JsonStream::seek()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 12
ccs 9
cts 9
cp 1
rs 9.4285
c 1
b 0
f 0
cc 3
eloc 7
nc 2
nop 2
crap 3
1
<?php
2
3
namespace Violet\StreamingJsonEncoder;
4
5
use Psr\Http\Message\StreamInterface;
6
7
/**
8
 * Provides a http stream interface for encoding JSON.
9
 *
10
 * @author Riikka Kalliomäki <[email protected]>
11
 * @copyright Copyright (c) 2016, Riikka Kalliomäki
12
 * @license http://opensource.org/licenses/mit-license.php MIT License
13
 */
14
class JsonStream implements StreamInterface
15
{
16
    /** @var BufferJsonEncoder The encoder used to produce the JSON stream */
17
    private $encoder;
18
19
    /** @var int The current position of the cursor in the JSON stream */
20
    private $cursor;
21
22
    /** @var string Buffered output from encoding the value as JSON */
23
    private $buffer;
24
25
    /**
26
     * JsonStream constructor.
27
     * @param BufferJsonEncoder|mixed $value A JSON encoder to use or a value to encode
28
     */
29 20
    public function __construct($value)
30
    {
31 20
        if (!$value instanceof BufferJsonEncoder) {
32 18
            $value = new BufferJsonEncoder($value);
33 9
        }
34
35 20
        $this->encoder = $value;
36 20
        $this->rewind();
37 10
    }
38
39
    /**
40
     * Returns the JSON encoder used for the JSON stream.
41
     * @return BufferJsonEncoder The currently used JSON encoder
42
     * @throws \RuntimeException If the stream has been closed
43
     */
44 20
    private function getEncoder()
45
    {
46 20
        if (!$this->encoder instanceof BufferJsonEncoder) {
47 4
            throw new \RuntimeException('Cannot operate on a closed JSON stream');
48
        }
49
50 20
        return $this->encoder;
51
    }
52
53
    /**
54
     * Returns the entire JSON stream as a string.
55
     *
56
     * Note that this operation performs rewind operation on the JSON encoder. Whether
57
     * this works or not is dependant on the underlying value being encoded. An empty
58
     * string is returned if the value cannot be encoded.
59
     *
60
     * @return string The entire JSON stream as a string
61
     */
62 4
    public function __toString()
63
    {
64
        try {
65 4
            $this->rewind();
66 2
            return $this->getContents();
67 2
        } catch (\Exception $exception) {
68 2
            return '';
69
        }
70
    }
71
72
    /**
73
     * Frees the JSON encoder from memory and prevents further reading from the JSON stream.
74
     */
75 4
    public function close()
76
    {
77 4
        $this->encoder = null;
78 2
    }
79
80
    /**
81
     * Detaches the underlying PHP stream and returns it.
82
     * @return null Always returns null as no underlying PHP stream exists
83
     */
84 2
    public function detach()
85
    {
86 2
        return null;
87
    }
88
89
    /**
90
     * Returns the total size of the JSON stream.
91
     * @return null Always returns null as the total size cannot be determined
92
     */
93 2
    public function getSize()
94
    {
95 2
        return null;
96
    }
97
98
    /**
99
     * Returns the current position of the cursor in the JSON stream.
100
     * @return int Current position of the cursor
101
     */
102 4
    public function tell()
103
    {
104 4
        $this->getEncoder();
105 4
        return $this->cursor;
106
    }
107
108
    /**
109
     * Tells if there are no more bytes to read from the JSON stream.
110
     * @return bool True if there are no more bytes to read, false if there are
111
     */
112 6
    public function eof()
113
    {
114 6
        return $this->buffer === null;
115
    }
116
117
    /**
118
     * Tells if the JSON stream is seekable or not.
119
     * @return bool Always returns true as JSON streams as always seekable
120
     */
121 2
    public function isSeekable()
122
    {
123 2
        return true;
124
    }
125
126
    /**
127
     * Seeks the given cursor position in the JSON stream.
128
     *
129
     * If the provided seek position is less than the current cursor position, a rewind
130
     * operation is performed on the underlying JSON encoder. Whether this works or not
131
     * depends on whether the encoded value supports rewinding.
132
     *
133
     * Note that since it's not possible to determine the end of the JSON stream without
134
     * encoding the entire value, it's not possible to set the cursor using SEEK_END
135
     * constant and doing so will result in an exception.
136
     *
137
     * @param int $offset The offset for the cursor.
138
     * @param int $whence Either SEEK_CUR or SEEK_SET to determine new cursor position
139
     */
140 20
    public function seek($offset, $whence = SEEK_SET)
141
    {
142 20
        $position = $this->calculatePosition($offset, $whence);
143
144 20
        if (!isset($this->cursor) || $position < $this->cursor) {
145 20
            $this->getEncoder()->rewind();
146 20
            $this->buffer = '';
147 20
            $this->cursor = 0;
148 10
        }
149
150 20
        $this->forward($position);
151 10
    }
152
153
    /**
154
     * Calculates new position for the cursor based on offset and whence.
155
     * @param int $offset The cursor offset
156
     * @param int $whence One of the SEEK_* constants
157
     * @return int The new cursor position
158
     */
159 20
    private function calculatePosition($offset, $whence)
160
    {
161 20
        if ($whence === SEEK_CUR) {
162 2
            return max(0, $this->cursor + (int) $offset);
163 20
        } elseif ($whence === SEEK_SET) {
164 20
            return max(0, (int) $offset);
165 4
        } elseif ($whence === SEEK_END) {
166 2
            throw new \RuntimeException('Cannot set cursor position from the end of a JSON stream');
167
        }
168
169 2
        throw new \InvalidArgumentException("Invalid cursor relative position '$whence'");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $whence instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
170
    }
171
172
    /**
173
     * Forwards the JSON stream reading cursor to the given position or to the end of stream.
174
     * @param int $position The new position of the cursor.
175
     */
176 20
    private function forward($position)
177
    {
178 20
        $encoder = $this->getEncoder();
179
180 20
        while ($this->cursor < $position) {
181 2
            $length = strlen($this->buffer);
182
183 2
            if ($this->cursor + $length > $position) {
184 2
                $this->buffer = substr($this->buffer, $position - $this->cursor);
185 2
                $this->cursor = $position;
186 2
                break;
187
            }
188
189 2
            $this->cursor += $length;
190 2
            $this->buffer = '';
191
192 2
            if (!$encoder->valid()) {
193 2
                $this->buffer = null;
194 2
                break;
195
            }
196
197 2
            $this->buffer = $encoder->current();
198 2
            $encoder->next();
199 1
        }
200 10
    }
201
202
    /**
203
     * Seeks the beginning of the JSON stream.
204
     *
205
     * If the encoding has already been started, rewinding the encoder may not work,
206
     * if the underlying value being encoded does not support rewinding.
207
     */
208 20
    public function rewind()
209
    {
210 20
        $this->seek(0);
211 10
    }
212
213
    /**
214
     * Tells if the JSON stream is writable or not.
215
     * @return bool Always returns false as JSON streams are never writable
216
     */
217 2
    public function isWritable()
218
    {
219 2
        return false;
220
    }
221
222
    /**
223
     * Writes the given bytes to the JSON stream.
224
     *
225
     * As the JSON stream does not represent a writable stream, this method will
226
     * always throw a runtime exception.
227
     *
228
     * @param string $string The bytes to write
229
     * @return int The number of bytes written
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use NoType.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
230
     * @throws \RuntimeException Always throws a runtime exception
231
     */
232 2
    public function write($string)
233
    {
234 2
        throw new \RuntimeException('Cannot write to a JSON stream');
235
    }
236
237
    /**
238
     * Tells if the JSON stream is readable or not.
239
     * @return bool Always returns true as JSON streams are always readable
240
     */
241 2
    public function isReadable()
242
    {
243 2
        return true;
244
    }
245
246
    /**
247
     * Returns the given number of bytes from the JSON stream.
248
     *
249
     * The underlying value is encoded into JSON until enough bytes have been
250
     * generated to fulfill the requested number of bytes. The extraneous bytes are
251
     * then buffered for the next read from the JSON stream. The stream may return
252
     * fewer number of bytes if the entire value has been encoded and there are no
253
     * more bytes to return.
254
     *
255
     * @param int $length The number of bytes to return
256
     * @return string The bytes read from the JSON stream
257
     */
258 6
    public function read($length)
259
    {
260 6
        $length = max(0, (int) $length);
261 6
        $encoder = $this->getEncoder();
262
263 4
        while (strlen($this->buffer) < $length && $encoder->valid()) {
264 2
            $this->buffer .= $encoder->current();
265 2
            $encoder->next();
266 1
        }
267
268 4
        if (strlen($this->buffer) > $length || $encoder->valid()) {
269 4
            $output = substr($this->buffer, 0, $length);
270 4
            $this->buffer = substr($this->buffer, $length);
271 2
        } else {
272 4
            $output = (string) $this->buffer;
273 4
            $this->buffer = null;
274
        }
275
276 4
        $this->cursor += strlen($output);
277
278 4
        return $output;
279
    }
280
281
    /**
282
     * Returns the remaining bytes from the JSON stream.
283
     * @return string The remaining bytes from JSON stream
284
     */
285 4
    public function getContents()
286
    {
287 4
        $encoder = $this->getEncoder();
288 4
        $output = '';
289
290 4
        while ($encoder->valid()) {
291 4
            $output .= $encoder->current();
292 4
            $encoder->next();
293 2
        }
294
295 4
        $this->cursor += strlen($output);
296 4
        $this->buffer = null;
297
298 4
        return $output;
299
    }
300
301
    /**
302
     * Returns the metadata from the underlying PHP stream.
303
     *
304
     * As no underlying PHP stream exists for the JSON stream, this method will
305
     * always return an empty array or a null.
306
     *
307
     * @param string|null The key of the value to return
308
     * @return array|null Always returns an empty array or a null
309
     */
310 2
    public function getMetadata($key = null)
311
    {
312 2
        return $key === null ? [] : null;
313
    }
314
}
315