3
3
namespace Violet \StreamingJsonEncoder ;
4
4
5
5
use Psr \Http \Message \StreamInterface ;
6
- use SebastianBergmann \CodeCoverage \RuntimeException ;
7
6
8
7
/**
9
- * JsonStream .
8
+ * Provides a http stream interface for encoding JSON .
10
9
*
11
10
* @author Riikka Kalliomäki <[email protected] >
12
11
* @copyright Copyright (c) 2016, Riikka Kalliomäki
13
12
* @license http://opensource.org/licenses/mit-license.php MIT License
14
13
*/
15
14
class JsonStream implements StreamInterface
16
15
{
16
+ /** @var BufferJsonEncoder The encoder used to produce the JSON stream */
17
17
private $ encoder ;
18
+
19
+ /** @var int The current position of the cursor in the JSON stream */
18
20
private $ cursor ;
21
+
22
+ /** @var string Buffered output from encoding the value as JSON */
19
23
private $ buffer ;
20
24
21
- public function __construct ($ value , $ options = 0 )
25
+ /**
26
+ * JsonStream constructor.
27
+ * @param BufferJsonEncoder|mixed $value A JSON encoder to use or a value to encode
28
+ */
29
+ public function __construct ($ value )
22
30
{
23
- $ this ->encoder = new BufferJsonEncoder ($ value );
24
- $ this ->encoder ->setOptions ($ options );
31
+ if (!$ value instanceof BufferJsonEncoder) {
32
+ $ value = new BufferJsonEncoder ($ value );
33
+ }
34
+
35
+ $ this ->encoder = $ value ;
25
36
$ this ->rewind ();
26
37
}
27
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
+ */
28
44
private function getEncoder ()
29
45
{
30
46
if (!$ this ->encoder instanceof BufferJsonEncoder) {
31
- throw new RuntimeException ('Cannot operate on a closed JSON stream ' );
47
+ throw new \ RuntimeException ('Cannot operate on a closed JSON stream ' );
32
48
}
33
49
34
50
return $ this ->encoder ;
35
51
}
36
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
+ */
37
62
public function __toString ()
38
63
{
39
64
try {
@@ -44,81 +69,195 @@ public function __toString()
44
69
}
45
70
}
46
71
72
+ /**
73
+ * Frees the JSON encoder from memory and prevents further reading from the JSON stream.
74
+ */
47
75
public function close ()
48
76
{
49
77
$ this ->encoder = null ;
50
78
}
51
79
80
+ /**
81
+ * Detaches the underlying PHP stream and returns it.
82
+ * @return null Always returns null as no underlying PHP stream exists
83
+ */
52
84
public function detach ()
53
85
{
54
86
return null ;
55
87
}
56
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
+ */
57
93
public function getSize ()
58
94
{
59
95
return null ;
60
96
}
61
97
98
+ /**
99
+ * Returns the current position of the cursor in the JSON stream.
100
+ * @return int Current position of the cursor
101
+ */
62
102
public function tell ()
63
103
{
64
104
$ this ->getEncoder ();
65
105
return $ this ->cursor ;
66
106
}
67
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
+ */
68
112
public function eof ()
69
113
{
70
114
return $ this ->buffer === null ;
71
115
}
72
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
+ */
73
121
public function isSeekable ()
74
122
{
75
123
return true ;
76
124
}
77
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
+ */
78
140
public function seek ($ offset , $ whence = SEEK_SET )
79
141
{
80
- if ($ whence === SEEK_CUR ) {
81
- $ position = max (0 , $ this ->cursor + (int ) $ offset );
82
- } elseif ($ whence === SEEK_END ) {
83
- throw new \RuntimeException ('Cannot set cursor position from the end of a JSON stream ' );
84
- } else {
85
- $ position = max (0 , (int ) $ offset );
86
- }
142
+ $ position = $ this ->calculatePosition ($ offset , $ whence );
87
143
88
144
if (!isset ($ this ->cursor ) || $ position < $ this ->cursor ) {
89
145
$ this ->getEncoder ()->rewind ();
90
146
$ this ->buffer = '' ;
91
147
$ this ->cursor = 0 ;
92
148
}
93
149
94
- while ($ this ->cursor < $ position && !$ this ->eof ()) {
95
- $ this ->read (min ($ position - $ this ->cursor , 8 * 1024 ));
150
+ $ this ->forward ($ position );
151
+ }
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
+ private function calculatePosition ($ offset , $ whence )
160
+ {
161
+ if ($ whence === SEEK_CUR ) {
162
+ return max (0 , $ this ->cursor + (int ) $ offset );
163
+ } elseif ($ whence === SEEK_SET ) {
164
+ return max (0 , (int ) $ offset );
165
+ } elseif ($ whence === SEEK_END ) {
166
+ throw new \RuntimeException ('Cannot set cursor position from the end of a JSON stream ' );
167
+ }
168
+
169
+ throw new \InvalidArgumentException ("Invalid cursor relative position ' $ whence' " );
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
+ private function forward ($ position )
177
+ {
178
+ $ encoder = $ this ->getEncoder ();
179
+
180
+ while ($ this ->cursor < $ position ) {
181
+ $ length = strlen ($ this ->buffer );
182
+
183
+ if ($ this ->cursor + $ length > $ position ) {
184
+ $ this ->buffer = substr ($ this ->buffer , $ position - $ this ->cursor );
185
+ $ this ->cursor = $ position ;
186
+ break ;
187
+ }
188
+
189
+ $ this ->cursor += $ length ;
190
+ $ this ->buffer = '' ;
191
+
192
+ if (!$ encoder ->valid ()) {
193
+ $ this ->buffer = null ;
194
+ break ;
195
+ }
196
+
197
+ $ this ->buffer = $ encoder ->current ();
198
+ $ encoder ->next ();
96
199
}
97
200
}
98
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
+ */
99
208
public function rewind ()
100
209
{
101
210
$ this ->seek (0 );
102
211
}
103
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
+ */
104
217
public function isWritable ()
105
218
{
106
219
return false ;
107
220
}
108
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
230
+ * @throws \RuntimeException Always throws a runtime exception
231
+ */
109
232
public function write ($ string )
110
233
{
111
234
throw new \RuntimeException ('Cannot write to a JSON stream ' );
112
235
}
113
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
+ */
114
241
public function isReadable ()
115
242
{
116
243
return true ;
117
244
}
118
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
+ */
119
258
public function read ($ length )
120
259
{
121
- $ length = ( int ) $ length ;
260
+ $ length = max ( 0 , ( int ) $ length) ;
122
261
$ encoder = $ this ->getEncoder ();
123
262
124
263
while (strlen ($ this ->buffer ) < $ length && $ encoder ->valid ()) {
@@ -139,17 +278,35 @@ public function read($length)
139
278
return $ output ;
140
279
}
141
280
281
+ /**
282
+ * Returns the remaining bytes from the JSON stream.
283
+ * @return string The remaining bytes from JSON stream
284
+ */
142
285
public function getContents ()
143
286
{
287
+ $ encoder = $ this ->getEncoder ();
144
288
$ output = '' ;
145
289
146
- while (!$ this ->eof ()) {
147
- $ output .= $ this ->read (8 * 1024 );
290
+ while ($ encoder ->valid ()) {
291
+ $ output .= $ encoder ->current ();
292
+ $ encoder ->next ();
148
293
}
149
294
295
+ $ this ->cursor += strlen ($ output );
296
+ $ this ->buffer = null ;
297
+
150
298
return $ output ;
151
299
}
152
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
+ */
153
310
public function getMetadata ($ key = null )
154
311
{
155
312
return $ key === null ? [] : null ;
0 commit comments