Skip to content

Commit 2143ab2

Browse files
committed
Change object encoding decision
1 parent 0d2f1e2 commit 2143ab2

File tree

8 files changed

+180
-16
lines changed

8 files changed

+180
-16
lines changed

.php_cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,30 @@ return \PhpCsFixer\Config::create()
88
->setRules([
99
'@PSR2' => true,
1010

11-
'array_syntax' => ['syntax' => 'short'],
11+
'array_syntax' => [
12+
'syntax' => 'short'
13+
],
1214
'binary_operator_spaces' => [
13-
'align_equals' => false,
1415
'align_double_arrow' => null,
16+
'align_equals' => false,
1517
],
1618
'blank_line_after_opening_tag' => true,
1719
'cast_spaces' => true,
18-
'concat_space' => ['spacing' => 'one'],
20+
'combine_consecutive_unsets' => true,
21+
'concat_space' => [
22+
'spacing' => 'one'
23+
],
1924
'declare_equal_normalize' => true,
2025
'dir_constant' => true,
2126
'ereg_to_preg' => true,
27+
'function_to_constant' => true,
2228
'function_typehint_space' => true,
2329
'hash_to_slash_comment' => true,
2430
'heredoc_to_nowdoc' => true,
2531
'include' => true,
32+
'is_null' => true,
2633
'lowercase_cast' => true,
34+
'magic_constant_casing' => true,
2735
'method_separation' => true,
2836
'modernize_types_casting' => true,
2937
'native_function_casing' => true,
@@ -39,6 +47,7 @@ return \PhpCsFixer\Config::create()
3947
'no_leading_namespace_whitespace' => true,
4048
'no_mixed_echo_print' => true,
4149
'no_multiline_whitespace_around_double_arrow' => true,
50+
'no_multiline_whitespace_before_semicolons' => true,
4251
'no_php4_constructor' => true,
4352
'no_short_bool_cast' => true,
4453
'no_singleline_whitespace_before_semicolons' => true,
@@ -50,8 +59,12 @@ return \PhpCsFixer\Config::create()
5059
'no_useless_return' => true,
5160
'no_whitespace_before_comma_in_array' => true,
5261
'no_whitespace_in_blank_line' => true,
62+
'non_printable_character' => true,
5363
'normalize_index_brace' => true,
5464
'object_operator_without_whitespace' => true,
65+
'ordered_class_elements' => [
66+
'order' => ['use_trait', 'constant', 'property', 'construct', 'method'],
67+
],
5568
'ordered_imports' => true,
5669
'php_unit_construct' => true,
5770
'php_unit_dedicate_assert' => true,

CHANGES.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog #
22

3+
## v1.1.0 (2017-06-18) ##
4+
5+
* Empty objects and objects that don't implement `Traversable` are now always
6+
encoded as JSON objects (for `json_encode()` compatibility).
7+
* Associative array testing now returns faster and more memory efficiently
8+
* JSON encoding errors now contain the error constant
9+
* The visibility for `write()` method in `StreamJsonEncoder` and
10+
`BufferJsonEncoder` has been changed to protected (as originally intended)
11+
* A protected method `getValueStack()` has been added to `AbstractJsonEncoder`
12+
that returns current unresolved value stack (for special write method
13+
implementations).
14+
* An overridable protected method `resolveValue()` has been added to
15+
`AbstractJsonEncoder` which is used to resolve closures and objects
16+
implementing `JsonSerializable`.
17+
318
## v1.0.0 (2017-02-26) ##
419

520
* Initial release

examples/benchmark.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
require __DIR__ . '/../vendor/autoload.php';
4+
5+
ini_set('memory_limit', '2048M');
6+
7+
$payload = [];
8+
$longString = str_repeat('Lorem ipsum ', 1000);
9+
$node = [
10+
'boolean' => true,
11+
'integer' => 1879234,
12+
'float' => 1.234900,
13+
'string' => 'stringy thingy',
14+
'array' => [
15+
'value 1',
16+
'value 2',
17+
'value 3',
18+
],
19+
'very_long' => $longString,
20+
];
21+
22+
for ($i = 0; $i < 10000; $i++) {
23+
$payload[] = $node;
24+
}
25+
26+
function benchmark(Closure $callback)
27+
{
28+
$timer = microtime(true);
29+
30+
ob_start(function () {
31+
return '';
32+
}, 1024 * 8);
33+
34+
$bytes = $callback();
35+
36+
ob_end_flush();
37+
printf(
38+
'Output: %s kb, %d ms, Mem %d mb',
39+
number_format($bytes / 1024),
40+
(microtime(true) - $timer) * 1000,
41+
memory_get_peak_usage(true) / 1024 / 1024
42+
);
43+
}
44+
45+
echo 'Streaming: ';
46+
benchmark(function () use ($payload) {
47+
$encoder = new \Violet\StreamingJsonEncoder\StreamJsonEncoder($payload);
48+
$encoder->setOptions(JSON_PRETTY_PRINT);
49+
return $encoder->encode();
50+
});
51+
52+
echo "\nDirect: ";
53+
benchmark(function () use ($payload) {
54+
$output = json_encode($payload, JSON_PRETTY_PRINT);
55+
echo $output;
56+
return strlen($output);
57+
});

src/AbstractJsonEncoder.php

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ abstract class AbstractJsonEncoder implements \Iterator
1717
/** @var bool[] True for every object in the stack, false for an array */
1818
private $stackType;
1919

20+
/** @var array Stack of values being encoded */
21+
private $valueStack;
22+
2023
/** @var bool Whether the next value is the first value in an array or an object */
2124
private $first;
2225

@@ -97,12 +100,21 @@ public function getErrors()
97100
return $this->errors;
98101
}
99102

103+
/**
104+
* Returns the current encoding value stack.
105+
* @return array The current encoding value stack
106+
*/
107+
protected function getValueStack()
108+
{
109+
return $this->valueStack;
110+
}
111+
100112
/**
101113
* Initializes the iterator if it has not been initialized yet.
102114
*/
103115
private function initialize()
104116
{
105-
if ($this->stack === null) {
117+
if (!isset($this->stack)) {
106118
$this->rewind();
107119
}
108120
}
@@ -146,6 +158,7 @@ public function rewind()
146158

147159
$this->stack = [];
148160
$this->stackType = [];
161+
$this->valueStack = [];
149162
$this->errors = [];
150163
$this->newLine = false;
151164
$this->first = true;
@@ -229,12 +242,14 @@ private function processKey($key)
229242
*/
230243
private function processValue($value)
231244
{
245+
$this->valueStack[] = $value;
232246
$value = $this->resolveValue($value);
233247

234248
if (is_array($value) || is_object($value)) {
235249
$this->pushStack($value);
236250
} else {
237251
$this->outputJson($value, JsonToken::T_VALUE);
252+
array_pop($this->valueStack);
238253
}
239254
}
240255

@@ -251,11 +266,9 @@ protected function resolveValue($value)
251266
} elseif ($value instanceof \Closure) {
252267
$value = $value();
253268
} else {
254-
break;
269+
return $value;
255270
}
256271
} while (true);
257-
258-
return $value;
259272
}
260273

261274
/**
@@ -322,11 +335,11 @@ private function isObject($iterable, \Iterator $iterator)
322335
return true;
323336
}
324337

325-
if (is_array($iterable)) {
326-
return $this->isAssociative($iterable);
338+
if ($iterable instanceof \Traversable) {
339+
return $iterator->valid() && $iterator->key() !== 0;
327340
}
328341

329-
return $iterator->valid() ? $iterator->key() !== 0 : !$iterable instanceof \Traversable;
342+
return is_object($iterable) || $this->isAssociative($iterable);
330343
}
331344

332345
/**
@@ -368,6 +381,8 @@ private function popStack()
368381
} else {
369382
$this->output(']', JsonToken::T_RIGHT_BRACKET);
370383
}
384+
385+
array_pop($this->valueStack);
371386
}
372387

373388
/**
@@ -381,7 +396,7 @@ private function outputJson($value, $token)
381396
$error = json_last_error();
382397

383398
if ($error !== JSON_ERROR_NONE) {
384-
$this->addError(sprintf('%s (%s)', json_last_error_msg(), $this->getJsonErrorName($error));
399+
$this->addError(sprintf('%s (%s)', json_last_error_msg(), $this->getJsonErrorName($error)));
385400
}
386401

387402
$this->output($encoded, $token);
@@ -403,8 +418,6 @@ private function getJsonErrorName($error)
403418
return $match;
404419
}
405420
}
406-
407-
return 'UNKNOWN';
408421
}
409422

410423
/**

src/BufferJsonEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function current()
5959
* @param string $string The JSON string to write
6060
* @param int $token The type of the token
6161
*/
62-
public function write($string, $token)
62+
protected function write($string, $token)
6363
{
6464
$this->buffer .= $string;
6565
}

src/StreamJsonEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public function current()
8282
* @param string $string The string to output
8383
* @param int $token The type of the string
8484
*/
85-
public function write($string, $token)
85+
protected function write($string, $token)
8686
{
8787
if ($this->stream === null) {
8888
echo $string;

tests/classes/DateEncoder.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Violet\StreamingJsonEncoder\Test;
4+
5+
use Violet\StreamingJsonEncoder\BufferJsonEncoder;
6+
use Violet\StreamingJsonEncoder\JsonToken;
7+
8+
class DateEncoder extends BufferJsonEncoder
9+
{
10+
protected function resolveValue($value)
11+
{
12+
$value = parent::resolveValue($value);
13+
14+
if ($value instanceof \DateTimeInterface) {
15+
$value = $value->format('r');
16+
}
17+
18+
return $value;
19+
}
20+
21+
protected function write($string, $token)
22+
{
23+
if ($token === JsonToken::T_VALUE) {
24+
$stack = $this->getValueStack();
25+
$value = end($stack);
26+
27+
if ($value instanceof \DateTimeInterface) {
28+
$string = sprintf('"<time datetime="%s">%s</time>"', $value->format('c'), substr($string, 1, -1));
29+
}
30+
}
31+
32+
parent::write($string, $token);
33+
}
34+
}

tests/tests/JsonEncoderTest.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Violet\StreamingJsonEncoder;
44

55
use PHPUnit\Framework\TestCase;
6+
use Violet\StreamingJsonEncoder\Test\DateEncoder;
67
use Violet\StreamingJsonEncoder\Test\SerializableData;
78

89
/**
@@ -14,6 +15,26 @@
1415
*/
1516
class JsonEncoderTest extends TestCase
1617
{
18+
public function testEmptyEncoding()
19+
{
20+
$this->assertEncodingResult('[]', [], []);
21+
$this->assertEncodingResult('[]', [], new \ArrayObject([]));
22+
$this->assertEncodingResult('{}', [], (object) []);
23+
}
24+
25+
public function testObjectTyping()
26+
{
27+
$this->assertEncodingResult('["foo"]', ['foo'], ['foo']);
28+
$this->assertEncodingResult('{"0":"foo"}', ['foo'], (object) ['foo']);
29+
$this->assertEncodingResult('{"1":"foo"}', [1 => 'foo'], [1 => 'foo']);
30+
31+
$this->assertEncodingResult('["foo"]', ['foo'], new \ArrayObject(['foo']));
32+
$this->assertEncodingResult('{"1":"foo"}', [1 => 'foo'], new \ArrayObject([1 => 'foo']));
33+
$this->assertEncodingResult('{"0":"foo"}', ['foo'], function () {
34+
yield '0' => 'foo';
35+
});
36+
}
37+
1738
public function testPrettyPrint()
1839
{
1940
$expectedJson = <<<'JSON'
@@ -138,7 +159,7 @@ public function testNullOnInvalid()
138159

139160
$encoder = $this->assertEncodingResult($expectedJson, $result, $array, JSON_PARTIAL_OUTPUT_ON_ERROR);
140161
$this->assertSame([
141-
'Line 1, column 10: Type is not supported',
162+
'Line 1, column 10: Type is not supported (JSON_ERROR_UNSUPPORTED_TYPE)',
142163
'Line 1, column 35: Only string or integer keys are supported',
143164
], $encoder->getErrors());
144165
}
@@ -313,6 +334,17 @@ public function testClosureResolving()
313334
$this->assertSame('[0,1,2,3,4,5,6,7,8,9]', $encoder->encode());
314335
}
315336

337+
public function testValueStackResolving()
338+
{
339+
$date = new \DateTime();
340+
$encoder = new DateEncoder([$date]);
341+
342+
$this->assertSame(
343+
sprintf('["<time datetime="%s">%s</time>"]', $date->format('c'), $date->format('r')),
344+
$encoder->encode()
345+
);
346+
}
347+
316348
public function assertEncodingResult($expectedJson, $expectedData, $initialData, $options = 0)
317349
{
318350
$encoder = new BufferJsonEncoder($initialData);

0 commit comments

Comments
 (0)