Skip to content

Commit 532bd66

Browse files
committed
Initial commit
0 parents  commit 532bd66

File tree

6 files changed

+312
-0
lines changed

6 files changed

+312
-0
lines changed

composer.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "violet/streaming-json-encoder",
3+
"type": "library",
4+
"description": "Library for encoding large data sets in JSON",
5+
"homepage": "http://violet.riimu.net",
6+
"keywords": [
7+
"json",
8+
"encoder"
9+
],
10+
"license": "MIT",
11+
"authors": [
12+
{
13+
"name": "Riikka Kalliomäki",
14+
"email": "[email protected]",
15+
"homepage": "http://riimu.net"
16+
}
17+
],
18+
"require": {
19+
"php": ">=5.6.0"
20+
},
21+
"require-dev": {
22+
"psr/http-message": "^1.0"
23+
},
24+
"autoload": {
25+
"psr-4": {
26+
"Violet\\StreamingJsonEncoder\\": "src/"
27+
}
28+
}
29+
}

phpunit.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit bootstrap="tests/bootstrap.php">
3+
<testsuites>
4+
<testsuite name="Default">
5+
<directory suffix="Test.php">tests/tests/</directory>
6+
</testsuite>
7+
</testsuites>
8+
<filter>
9+
<whitelist processUncoveredFilesFromWhitelist="true">
10+
<directory suffix=".php">src/</directory>
11+
<exclude>
12+
<file>src/autoload.php</file>
13+
</exclude>
14+
</whitelist>
15+
</filter>
16+
</phpunit>

src/EncodingException.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Violet\StreamingJsonEncoder;
4+
5+
/**
6+
* EncodingException.
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 EncodingException extends \Exception
13+
{
14+
15+
}

src/StreamingJsonEncoder.php

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php
2+
3+
namespace Violet\StreamingJsonEncoder;
4+
5+
/**
6+
* StreamingJsonEncoder.
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 StreamingJsonEncoder
13+
{
14+
/** @var \Generator[] */
15+
private $generatorStack;
16+
17+
/** @var bool[] */
18+
private $typeStack;
19+
20+
/** @var bool */
21+
private $first;
22+
23+
/** @var int */
24+
private $options;
25+
26+
/** @var bool */
27+
private $newLine;
28+
29+
/** @var string */
30+
private $indent;
31+
32+
/** @var string[] */
33+
private $encodingErrors;
34+
35+
/** @var int */
36+
private $line;
37+
38+
/** @var int */
39+
private $column;
40+
41+
public function __construct()
42+
{
43+
$this->indent = ' ';
44+
$this->newLine = false;
45+
}
46+
47+
public function encode($value, $options = 0)
48+
{
49+
$this->encodingErrors = [];
50+
$this->options = $options;
51+
$this->newLine = false;
52+
$this->line = 1;
53+
$this->column = 1;
54+
55+
while ($value instanceof \JsonSerializable) {
56+
$value = $value->jsonSerialize();
57+
}
58+
59+
if (is_array($value) || is_object($value)) {
60+
$this->traverse($value);
61+
} else {
62+
$this->output($this->encodeValue($value));
63+
}
64+
}
65+
66+
private function traverse($traversable)
67+
{
68+
$this->generatorStack = [];
69+
$this->typeStack = [];
70+
$this->first = true;
71+
72+
$this->pushIterable($traversable);
73+
$keySeparator = $this->options & JSON_PRETTY_PRINT ? ': ' : ':';
74+
$null = json_encode(null);
75+
76+
foreach ($this->traverseStack() as $key => $value) {
77+
if (!$this->first) {
78+
$this->outputLine(',');
79+
}
80+
81+
$this->first = false;
82+
83+
if (end($this->typeStack)) {
84+
$encoded = $this->encodeValue((string) $key);
85+
86+
if ($encoded === $null) {
87+
continue;
88+
}
89+
90+
$this->output($encoded . $keySeparator);
91+
}
92+
93+
while ($value instanceof \JsonSerializable) {
94+
$value = $value->jsonSerialize();
95+
}
96+
97+
if (is_array($value) || is_object($value)) {
98+
$this->pushIterable($value);
99+
} else {
100+
$this->output($this->encodeValue($value));
101+
}
102+
}
103+
}
104+
105+
private function pushIterable($iterable)
106+
{
107+
$this->generatorStack[] = $this->iterate($iterable);
108+
$this->first = true;
109+
110+
if ($this->options & JSON_FORCE_OBJECT) {
111+
$object = true;
112+
} elseif (is_array($iterable)) {
113+
$object = array_keys($iterable) !== range(0, count($iterable) - 1);
114+
} else {
115+
$generator = end($this->generatorStack);
116+
$object = $generator->valid() && $generator->key() === 0;
117+
}
118+
119+
$this->outputLine($object ? '{' : '[');
120+
$this->typeStack[] = $object;
121+
}
122+
123+
private function popIterable()
124+
{
125+
if (!$this->first) {
126+
$this->outputLine('');
127+
}
128+
129+
$this->first = false;
130+
array_pop($this->generatorStack);
131+
$object = array_pop($this->typeStack);
132+
$this->output($object ? '}' : ']');
133+
}
134+
135+
public function traverseStack()
136+
{
137+
while ($this->generatorStack) {
138+
$active = end($this->generatorStack);
139+
140+
if ($active->valid()) {
141+
yield $active->key() => $active->current();
142+
$active->next();
143+
} else {
144+
$this->popIterable();
145+
}
146+
}
147+
}
148+
149+
public function iterate($iterable)
150+
{
151+
foreach ($iterable as $key => $value) {
152+
yield $key => $value;
153+
}
154+
}
155+
156+
private function output($string)
157+
{
158+
if ($this->newLine && $this->options & JSON_PRETTY_PRINT) {
159+
$this->write("\n");
160+
$this->line++;
161+
$this->column = 1;
162+
$this->write(str_repeat($this->indent, count($this->typeStack)));
163+
}
164+
165+
$this->newLine = false;
166+
$this->write($string);
167+
}
168+
169+
private function outputLine($string)
170+
{
171+
$this->output($string);
172+
$this->newLine = true;
173+
}
174+
175+
private function write($string)
176+
{
177+
echo $string;
178+
$this->column += strlen($string);
179+
}
180+
181+
private function encodeValue($value)
182+
{
183+
$encoded = json_encode($value, $this->options);
184+
185+
if (json_last_error() !== JSON_ERROR_NONE) {
186+
$this->encodingErrors[] =
187+
sprintf('Line %d, column %d: %s', $this->line, $this->column, json_last_error_msg());
188+
189+
if ($this->options & JSON_PARTIAL_OUTPUT_ON_ERROR) {
190+
return $encoded === false ? json_encode(null) : $encoded;
191+
}
192+
193+
throw new EncodingException(end($this->encodingErrors));
194+
}
195+
196+
return $encoded;
197+
}
198+
}

tests/bootstrap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../vendor/autoload.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+
use PHPUnit\Framework\TestCase;
6+
7+
/**
8+
* StreamingJsonEncoderTest.
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 StreamingJsonEncoderTest extends TestCase
15+
{
16+
public function testPrettyObjectArray()
17+
{
18+
$expectedJson = <<<'JSON'
19+
{
20+
"key 1": "value 1",
21+
"key 2": "value 2"
22+
}
23+
JSON;
24+
25+
$array = [
26+
'key 1' => 'value 1',
27+
'key 2' => 'value 2',
28+
];
29+
30+
$this->assertEncodingResult($expectedJson, $array, $array, JSON_PRETTY_PRINT);
31+
}
32+
33+
public function testObjectArray()
34+
{
35+
$expectedJson = '{"key 1":"value 1","key 2":"value 2"}';
36+
$array = [
37+
'key 1' => 'value 1',
38+
'key 2' => 'value 2',
39+
];
40+
41+
$this->assertEncodingResult($expectedJson, $array, $array);
42+
}
43+
44+
public function assertEncodingResult($expectedJson, $expectedData, $initialData, $options = 0)
45+
{
46+
$encoder = new StreamingJsonEncoder();
47+
$this->expectOutputString($expectedJson);
48+
$encoder->encode($initialData, $options);
49+
$this->assertSame($expectedData, json_decode($expectedJson, true));
50+
}
51+
}

0 commit comments

Comments
 (0)