Skip to content

Commit e95f0d5

Browse files
committed
Add JsonStream class
1 parent 504b24b commit e95f0d5

File tree

5 files changed

+273
-5
lines changed

5 files changed

+273
-5
lines changed

src/AbstractJsonEncoder.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,35 @@ public function getErrors()
7878
return $this->errors;
7979
}
8080

81+
private function initialize()
82+
{
83+
if (!isset($this->stack)) {
84+
$this->rewind();
85+
}
86+
}
87+
8188
public function key()
8289
{
90+
$this->initialize();
91+
8392
return $this->step;
8493
}
8594

8695
public function valid()
8796
{
97+
$this->initialize();
98+
8899
return $this->step !== null;
89100
}
90101

91102
abstract public function current();
92103

93104
public function rewind()
94105
{
106+
if ($this->step === 0) {
107+
return;
108+
}
109+
95110
$this->stack = [];
96111
$this->stackType = [];
97112
$this->errors = [];
@@ -106,6 +121,8 @@ public function rewind()
106121

107122
public function next()
108123
{
124+
$this->initialize();
125+
109126
if (!empty($this->stack)) {
110127
$this->step++;
111128
$generator = end($this->stack);

src/BufferJsonEncoder.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ class BufferJsonEncoder extends AbstractJsonEncoder
1616

1717
public function encode()
1818
{
19-
$json = '';
19+
$json = [];
2020

2121
foreach ($this as $string) {
22-
$json .= $string;
22+
$json[] = $string;
2323
}
2424

25-
return $json;
25+
return implode($json);
2626
}
2727

2828
public function rewind()

src/JsonStream.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
namespace Violet\StreamingJsonEncoder;
4+
5+
use Psr\Http\Message\StreamInterface;
6+
use SebastianBergmann\CodeCoverage\RuntimeException;
7+
8+
/**
9+
* JsonStream.
10+
*
11+
* @author Riikka Kalliomäki <[email protected]>
12+
* @copyright Copyright (c) 2016, Riikka Kalliomäki
13+
* @license http://opensource.org/licenses/mit-license.php MIT License
14+
*/
15+
class JsonStream implements StreamInterface
16+
{
17+
private $encoder;
18+
private $cursor;
19+
private $buffer;
20+
21+
public function __construct($value, $options = 0)
22+
{
23+
$this->encoder = new BufferJsonEncoder($value);
24+
$this->encoder->setOptions($options);
25+
$this->rewind();
26+
}
27+
28+
private function getEncoder()
29+
{
30+
if (!$this->encoder instanceof BufferJsonEncoder) {
31+
throw new RuntimeException("Cannot operate on a closed JSON stream");
32+
}
33+
34+
return $this->encoder;
35+
}
36+
37+
public function __toString()
38+
{
39+
try {
40+
$this->rewind();
41+
return $this->getContents();
42+
} catch (\Exception $exception) {
43+
return '';
44+
}
45+
}
46+
47+
public function close()
48+
{
49+
$this->encoder = null;
50+
}
51+
52+
public function detach()
53+
{
54+
return null;
55+
}
56+
57+
public function getSize()
58+
{
59+
return null;
60+
}
61+
62+
public function tell()
63+
{
64+
$this->getEncoder();
65+
return $this->cursor;
66+
}
67+
68+
public function eof()
69+
{
70+
return $this->buffer === null;
71+
}
72+
73+
public function isSeekable()
74+
{
75+
return true;
76+
}
77+
78+
public function seek($offset, $whence = SEEK_SET)
79+
{
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+
}
87+
88+
if (!isset($this->cursor) || $position < $this->cursor) {
89+
$this->getEncoder()->rewind();
90+
$this->buffer = '';
91+
$this->cursor = 0;
92+
}
93+
94+
while ($this->cursor < $position && !$this->eof()) {
95+
$this->read(min($position - $this->cursor, 8 * 1024));
96+
}
97+
}
98+
99+
public function rewind()
100+
{
101+
$this->seek(0);
102+
}
103+
104+
public function isWritable()
105+
{
106+
return false;
107+
}
108+
109+
public function write($string)
110+
{
111+
throw new \RuntimeException('Cannot write to a JSON stream');
112+
}
113+
114+
public function isReadable()
115+
{
116+
return true;
117+
}
118+
119+
public function read($length)
120+
{
121+
$length = (int) $length;
122+
$encoder = $this->getEncoder();
123+
124+
while (strlen($this->buffer) < $length && $encoder->valid()) {
125+
$this->buffer .= $encoder->current();
126+
$encoder->next();
127+
}
128+
129+
if (strlen($this->buffer) > $length || $encoder->valid()) {
130+
$output = substr($this->buffer, 0, $length);
131+
$this->buffer = substr($this->buffer, $length);
132+
} else {
133+
$output = (string) $this->buffer;
134+
$this->buffer = null;
135+
}
136+
137+
$this->cursor += strlen($output);
138+
139+
return $output;
140+
}
141+
142+
public function getContents()
143+
{
144+
$output = '';
145+
146+
while (!$this->eof()) {
147+
$output .= $this->read(8 * 1024);
148+
}
149+
150+
return $output;
151+
}
152+
153+
public function getMetadata($key = null)
154+
{
155+
return $key === null ? [] : null;
156+
}
157+
}

tests/tests/JsonEncoderTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,8 @@ public function testEncoderIterationKey()
275275
{
276276
$encoder = new BufferJsonEncoder('value');
277277

278-
$this->assertSame(null, $encoder->key());
279-
$this->assertSame(false, $encoder->valid());
278+
$this->assertSame(0, $encoder->key());
279+
$this->assertSame(true, $encoder->valid());
280280

281281
$encoder->rewind();
282282
$this->assertSame(0, $encoder->key());

tests/tests/JsonStreamTest.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace Violet\StreamingJsonEncoder;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
/**
8+
* JsonStreamTest.
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 JsonStreamTest extends TestCase
15+
{
16+
public function testExactReads()
17+
{
18+
$stream = new JsonStream(['key' => 'value']);
19+
20+
$this->assertSame(false, $stream->eof());
21+
$this->assertSame(0, $stream->tell());
22+
$this->assertSame('{', $stream->read(1));
23+
$this->assertSame(1, $stream->tell());
24+
$this->assertSame(false, $stream->eof());
25+
$this->assertSame('"key":"value"}', $stream->read(14));
26+
$this->assertSame(15, $stream->tell());
27+
$this->assertSame(true, $stream->eof());
28+
$this->assertSame('', $stream->read(1));
29+
}
30+
31+
public function testSeek()
32+
{
33+
$stream = new JsonStream(['key' => 'value']);
34+
35+
$stream->seek(8);
36+
$this->assertSame('value', $stream->read(5));
37+
$this->assertSame(13, $stream->tell());
38+
39+
$stream->seek(-6, SEEK_CUR);
40+
$this->assertSame('"', $stream->read(1));
41+
$this->assertSame(8, $stream->tell());
42+
}
43+
44+
public function testReadAfterClose()
45+
{
46+
$stream = new JsonStream('value');
47+
$stream->close();
48+
49+
$this->expectException(\RuntimeException::class);
50+
$stream->read(1);
51+
}
52+
53+
public function testToString()
54+
{
55+
$stream = new JsonStream(['key' => 'value']);
56+
$this->assertSame('{"key":"value"}', (string) $stream);
57+
}
58+
59+
public function testToStringAfterClose()
60+
{
61+
$stream = new JsonStream('value');
62+
$stream->close();
63+
$this->assertSame('', (string) $stream);
64+
}
65+
66+
public function testConstantValueMethods()
67+
{
68+
$stream = new JsonStream('value');
69+
70+
$this->assertSame(null, $stream->detach());
71+
$this->assertSame(null, $stream->getSize());
72+
$this->assertSame(true, $stream->isSeekable());
73+
$this->assertSame(false, $stream->isWritable());
74+
$this->assertSame(true, $stream->isReadable());
75+
$this->assertSame([], $stream->getMetadata());
76+
$this->assertSame(null, $stream->getMetadata('seekable'));
77+
}
78+
79+
public function testWriting()
80+
{
81+
$stream = new JsonStream('value');
82+
83+
$this->expectException(\RuntimeException::class);
84+
$stream->write('string');
85+
}
86+
87+
public function testSeekFromEnd()
88+
{
89+
$stream = new JsonStream('value');
90+
91+
$this->expectException(\RuntimeException::class);
92+
$stream->seek(2, SEEK_END);
93+
}
94+
}

0 commit comments

Comments
 (0)