Skip to content

Commit 504b24b

Browse files
committed
Implement iterator structure
1 parent 0913545 commit 504b24b

File tree

6 files changed

+536
-248
lines changed

6 files changed

+536
-248
lines changed

src/AbstractJsonEncoder.php

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
<?php
2+
3+
namespace Violet\StreamingJsonEncoder;
4+
5+
use SebastianBergmann\CodeCoverage\RuntimeException;
6+
7+
/**
8+
* AbstractJsonEncoder.
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+
abstract class AbstractJsonEncoder implements \Iterator
15+
{
16+
/** @var \Generator[] */
17+
private $stack;
18+
19+
/** @var bool[] */
20+
private $stackType;
21+
22+
/** @var bool */
23+
private $first;
24+
25+
/** @var int */
26+
private $options;
27+
28+
/** @var bool */
29+
private $newLine;
30+
31+
/** @var string */
32+
private $indent;
33+
34+
/** @var string[] */
35+
private $errors;
36+
37+
/** @var int */
38+
private $line;
39+
40+
/** @var int */
41+
private $column;
42+
43+
/** @var mixed */
44+
private $initialValue;
45+
46+
/** @var int|null */
47+
private $step;
48+
49+
public function __construct($value)
50+
{
51+
$this->initialValue = $value;
52+
$this->options = 0;
53+
$this->errors = [];
54+
$this->indent = ' ';
55+
$this->step = null;
56+
}
57+
58+
public function setOptions($options)
59+
{
60+
if ($this->step !== null) {
61+
throw new RuntimeException('Cannot change encoding options during encoding');
62+
}
63+
64+
$this->options = (int) $options;
65+
}
66+
67+
public function setIndent($indent)
68+
{
69+
if ($this->step !== null) {
70+
throw new RuntimeException('Cannot change indent during encoding');
71+
}
72+
73+
$this->indent = is_int($indent) ? str_repeat(' ', $indent) : (string) $indent;
74+
}
75+
76+
public function getErrors()
77+
{
78+
return $this->errors;
79+
}
80+
81+
public function key()
82+
{
83+
return $this->step;
84+
}
85+
86+
public function valid()
87+
{
88+
return $this->step !== null;
89+
}
90+
91+
abstract public function current();
92+
93+
public function rewind()
94+
{
95+
$this->stack = [];
96+
$this->stackType = [];
97+
$this->errors = [];
98+
$this->newLine = false;
99+
$this->first = true;
100+
$this->line = 1;
101+
$this->column = 1;
102+
$this->step = 0;
103+
104+
$this->processValue($this->initialValue);
105+
}
106+
107+
public function next()
108+
{
109+
if (!empty($this->stack)) {
110+
$this->step++;
111+
$generator = end($this->stack);
112+
113+
if ($generator->valid()) {
114+
$this->processStack($generator, end($this->stackType));
115+
$generator->next();
116+
} else {
117+
$this->popStack();
118+
}
119+
} else {
120+
$this->step = null;
121+
}
122+
}
123+
124+
private function processStack(\Generator $generator, $isObject)
125+
{
126+
if ($isObject) {
127+
$key = $generator->key();
128+
129+
if (!is_int($key) && !is_string($key)) {
130+
$this->addError('Only string or integer keys are supported');
131+
return;
132+
}
133+
134+
if (!$this->first) {
135+
$this->outputLine(',', Tokens::COMMA);
136+
}
137+
138+
$this->outputJson((string) $key, Tokens::KEY);
139+
$this->output(':', Tokens::SEPARATOR);
140+
141+
if ($this->options & JSON_PRETTY_PRINT) {
142+
$this->output(' ', Tokens::WHITESPACE);
143+
}
144+
} elseif (!$this->first) {
145+
$this->outputLine(',', Tokens::COMMA);
146+
}
147+
148+
$this->first = false;
149+
$this->processValue($generator->current());
150+
}
151+
152+
private function processValue($value)
153+
{
154+
while ($value instanceof \JsonSerializable) {
155+
$value = $value->jsonSerialize();
156+
}
157+
158+
if (is_array($value) || is_object($value)) {
159+
$this->pushStack($value);
160+
} else {
161+
$this->outputJson($value, Tokens::VALUE);
162+
}
163+
}
164+
165+
private function addError($message)
166+
{
167+
$errorMessage = sprintf('Line %d, column %d: %s', $this->line, $this->column, $message);
168+
$this->errors[] = $errorMessage;
169+
170+
if ($this->options & JSON_PARTIAL_OUTPUT_ON_ERROR) {
171+
return;
172+
}
173+
174+
throw new EncodingException($errorMessage);
175+
}
176+
177+
private function pushStack($iterable)
178+
{
179+
$generator = $this->getIterator($iterable);
180+
$isObject = $this->isObject($iterable, $generator);
181+
182+
if ($isObject) {
183+
$this->outputLine('{', Tokens::OPEN_OBJECT);
184+
} else {
185+
$this->outputLine('[', Tokens::OPEN_ARRAY);
186+
}
187+
188+
$this->first = true;
189+
$this->stack[] = $generator;
190+
$this->stackType[] = $isObject;
191+
}
192+
193+
private function getIterator($iterable)
194+
{
195+
foreach ($iterable as $key => $value) {
196+
yield $key => $value;
197+
}
198+
}
199+
200+
private function isObject($iterable, \Generator $generator)
201+
{
202+
if ($this->options & JSON_FORCE_OBJECT) {
203+
return true;
204+
} elseif (is_array($iterable)) {
205+
return $iterable !== [] && array_keys($iterable) !== range(0, count($iterable) - 1);
206+
}
207+
208+
return $generator->valid() && $generator->key() !== 0;
209+
}
210+
211+
private function popStack()
212+
{
213+
if (!$this->first) {
214+
$this->newLine = true;
215+
}
216+
217+
$this->first = false;
218+
array_pop($this->stack);
219+
220+
if (array_pop($this->stackType)) {
221+
$this->output('}', Tokens::CLOSE_OBJECT);
222+
} else {
223+
$this->output(']', Tokens::CLOSE_ARRAY);
224+
}
225+
}
226+
227+
private function outputJson($value, $token)
228+
{
229+
$encoded = json_encode($value, $this->options);
230+
231+
if (json_last_error() !== JSON_ERROR_NONE) {
232+
$this->addError(json_last_error_msg());
233+
}
234+
235+
$this->output($encoded, $token);
236+
}
237+
238+
private function outputLine($string, $token)
239+
{
240+
$this->output($string, $token);
241+
$this->newLine = true;
242+
}
243+
244+
private function output($string, $token)
245+
{
246+
if ($this->newLine && $this->options & JSON_PRETTY_PRINT) {
247+
$indent = str_repeat($this->indent, count($this->stack));
248+
$this->write("\n", Tokens::WHITESPACE);
249+
250+
if ($indent !== '') {
251+
$this->write($indent, Tokens::WHITESPACE);
252+
}
253+
254+
$this->line += 1;
255+
$this->column = strlen($indent) + 1;
256+
}
257+
258+
$this->newLine = false;
259+
$this->write($string, $token);
260+
$this->column += strlen($string);
261+
}
262+
263+
abstract protected function write($string, $token);
264+
}

src/BufferJsonEncoder.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Violet\StreamingJsonEncoder;
4+
5+
/**
6+
* BufferJsonEncoder.
7+
*
8+
* @author Riikka Kalliomäki <[email protected]>
9+
* @copyright Copyright (c) 2016, Riikka Kalliomäki
10+
* @license http://opensource.org/licenses/mit-license.php MIT License
11+
*/
12+
class BufferJsonEncoder extends AbstractJsonEncoder
13+
{
14+
/** @var string */
15+
private $buffer;
16+
17+
public function encode()
18+
{
19+
$json = '';
20+
21+
foreach ($this as $string) {
22+
$json .= $string;
23+
}
24+
25+
return $json;
26+
}
27+
28+
public function rewind()
29+
{
30+
$this->buffer = '';
31+
32+
parent::rewind();
33+
}
34+
35+
public function next()
36+
{
37+
$this->buffer = '';
38+
39+
parent::next();
40+
}
41+
42+
public function current()
43+
{
44+
return $this->valid() ? $this->buffer : null;
45+
}
46+
47+
public function write($string, $token)
48+
{
49+
$this->buffer .= $string;
50+
}
51+
}

src/StreamJsonEncoder.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Violet\StreamingJsonEncoder;
4+
5+
/**
6+
* StreamJsonEncoder.
7+
*
8+
* @author Riikka Kalliomäki <[email protected]>
9+
* @copyright Copyright (c) 2016, Riikka Kalliomäki
10+
* @license http://opensource.org/licenses/mit-license.php MIT License
11+
*/
12+
class StreamJsonEncoder extends AbstractJsonEncoder
13+
{
14+
/** @var callable|null */
15+
private $stream;
16+
17+
/** @var int */
18+
private $bytes;
19+
20+
public function __construct($value, callable $stream = null)
21+
{
22+
parent::__construct($value);
23+
24+
$this->stream = $stream;
25+
}
26+
27+
public function encode()
28+
{
29+
$total = 0;
30+
31+
foreach ($this as $bytes) {
32+
$total += $bytes;
33+
}
34+
35+
return $total;
36+
}
37+
38+
public function rewind()
39+
{
40+
$this->bytes = 0;
41+
42+
parent::rewind();
43+
}
44+
45+
public function next()
46+
{
47+
$this->bytes = 0;
48+
49+
parent::next();
50+
}
51+
52+
public function current()
53+
{
54+
return $this->valid() ? $this->bytes : null;
55+
}
56+
57+
public function write($string, $token)
58+
{
59+
if ($this->stream === null) {
60+
echo $string;
61+
} else {
62+
call_user_func($this->stream, $string, $token);
63+
}
64+
65+
$this->bytes += strlen($string);
66+
}
67+
}

0 commit comments

Comments
 (0)