11
11
*/
12
12
abstract class AbstractJsonEncoder implements \Iterator
13
13
{
14
- /** @var \Generator [] Current value stack in encoding */
14
+ /** @var \Iterator [] Current value stack in encoding */
15
15
private $ stack ;
16
16
17
17
/** @var bool[] True for every object in the stack, false for an array */
@@ -54,13 +54,13 @@ public function __construct($value)
54
54
$ this ->options = 0 ;
55
55
$ this ->errors = [];
56
56
$ this ->indent = ' ' ;
57
- $ this ->step = null ;
58
57
}
59
58
60
59
/**
61
60
* Sets the JSON encoding options.
62
61
* @param int $options The JSON encoding options that are used by json_encode
63
62
* @return $this Returns self for call chaining
63
+ * @throws \RuntimeException If changing encoding options during encoding operation
64
64
*/
65
65
public function setOptions ($ options )
66
66
{
@@ -76,6 +76,7 @@ public function setOptions($options)
76
76
* Sets the indent for the JSON output.
77
77
* @param string|int $indent A string to use as indent or the number of spaces
78
78
* @return $this Returns self for call chaining
79
+ * @throws \RuntimeException If changing indent during encoding operation
79
80
*/
80
81
public function setIndent ($ indent )
81
82
{
@@ -101,7 +102,7 @@ public function getErrors()
101
102
*/
102
103
private function initialize ()
103
104
{
104
- if (! isset ( $ this ->stack ) ) {
105
+ if ($ this ->stack === null ) {
105
106
$ this ->rewind ();
106
107
}
107
108
}
@@ -164,11 +165,11 @@ public function next()
164
165
165
166
if (!empty ($ this ->stack )) {
166
167
$ this ->step ++;
167
- $ generator = end ($ this ->stack );
168
+ $ iterator = end ($ this ->stack );
168
169
169
- if ($ generator ->valid ()) {
170
- $ this ->processStack ($ generator , end ($ this ->stackType ));
171
- $ generator ->next ();
170
+ if ($ iterator ->valid ()) {
171
+ $ this ->processStack ($ iterator , end ($ this ->stackType ));
172
+ $ iterator ->next ();
172
173
} else {
173
174
$ this ->popStack ();
174
175
}
@@ -178,22 +179,22 @@ public function next()
178
179
}
179
180
180
181
/**
181
- * Handles the next value from the generator to be encoded as JSON.
182
- * @param \Generator $generator The generator used to generate the next value
183
- * @param bool $isObject True if the generator is being handled as an object, false if not
182
+ * Handles the next value from the iterator to be encoded as JSON.
183
+ * @param \Iterator $iterator The iterator used to generate the next value
184
+ * @param bool $isObject True if the iterator is being handled as an object, false if not
184
185
*/
185
- private function processStack (\Generator $ generator , $ isObject )
186
+ private function processStack (\Iterator $ iterator , $ isObject )
186
187
{
187
188
if ($ isObject ) {
188
- if (!$ this ->processKey ($ generator ->key ())) {
189
+ if (!$ this ->processKey ($ iterator ->key ())) {
189
190
return ;
190
191
}
191
192
} elseif (!$ this ->first ) {
192
193
$ this ->outputLine (', ' , JsonToken::T_COMMA );
193
194
}
194
195
195
196
$ this ->first = false ;
196
- $ this ->processValue ($ generator ->current ());
197
+ $ this ->processValue ($ iterator ->current ());
197
198
}
198
199
199
200
/**
@@ -228,13 +229,7 @@ private function processKey($key)
228
229
*/
229
230
private function processValue ($ value )
230
231
{
231
- while ($ this ->isResolvable ($ value )) {
232
- if ($ value instanceof \JsonSerializable) {
233
- $ value = $ value ->jsonSerialize ();
234
- } elseif ($ value instanceof \Closure) {
235
- $ value = $ value ();
236
- }
237
- }
232
+ $ value = $ this ->resolveValue ($ value );
238
233
239
234
if (is_array ($ value ) || is_object ($ value )) {
240
235
$ this ->pushStack ($ value );
@@ -244,13 +239,23 @@ private function processValue($value)
244
239
}
245
240
246
241
/**
247
- * Tells if the given value should be resolved prior to encoding .
248
- * @param mixed $value The value to test
249
- * @return bool true if the value is resolvable, false if not
242
+ * Resolves the actual value of any given value that is about to be processed .
243
+ * @param mixed $value The value to resolve
244
+ * @return mixed The resolved value
250
245
*/
251
- private function isResolvable ($ value )
246
+ protected function resolveValue ($ value )
252
247
{
253
- return $ value instanceof \JsonSerializable || $ value instanceof \Closure;
248
+ do {
249
+ if ($ value instanceof \JsonSerializable) {
250
+ $ value = $ value ->jsonSerialize ();
251
+ } elseif ($ value instanceof \Closure) {
252
+ $ value = $ value ();
253
+ } else {
254
+ break ;
255
+ }
256
+ } while (true );
257
+
258
+ return $ value ;
254
259
}
255
260
256
261
/**
@@ -279,8 +284,8 @@ private function addError($message)
279
284
*/
280
285
private function pushStack ($ iterable )
281
286
{
282
- $ generator = $ this ->getIterator ($ iterable );
283
- $ isObject = $ this ->isObject ($ iterable , $ generator );
287
+ $ iterator = $ this ->getIterator ($ iterable );
288
+ $ isObject = $ this ->isObject ($ iterable , $ iterator );
284
289
285
290
if ($ isObject ) {
286
291
$ this ->outputLine ('{ ' , JsonToken::T_LEFT_BRACE );
@@ -289,7 +294,7 @@ private function pushStack($iterable)
289
294
}
290
295
291
296
$ this ->first = true ;
292
- $ this ->stack [] = $ generator ;
297
+ $ this ->stack [] = $ iterator ;
293
298
$ this ->stackType [] = $ isObject ;
294
299
}
295
300
@@ -308,18 +313,42 @@ private function getIterator($iterable)
308
313
/**
309
314
* Tells if the given iterable should be handled as a JSON object or not.
310
315
* @param object|array $iterable The iterable value to test
311
- * @param \Generator $generator Generator created from the iterable value
316
+ * @param \Iterator $iterator An Iterator created from the iterable value
312
317
* @return bool True if the given iterable should be treated as object, false if not
313
318
*/
314
- private function isObject ($ iterable , \Generator $ generator )
319
+ private function isObject ($ iterable , \Iterator $ iterator )
315
320
{
316
321
if ($ this ->options & JSON_FORCE_OBJECT ) {
317
322
return true ;
318
- } elseif (is_array ($ iterable )) {
319
- return $ iterable !== [] && array_keys ($ iterable ) !== range (0 , count ($ iterable ) - 1 );
320
323
}
321
324
322
- return $ generator ->valid () && $ generator ->key () !== 0 ;
325
+ if (is_array ($ iterable )) {
326
+ return $ this ->isAssociative ($ iterable );
327
+ }
328
+
329
+ return $ iterator ->valid () ? $ iterator ->key () !== 0 : !$ iterable instanceof \Traversable;
330
+ }
331
+
332
+ /**
333
+ * Tells if the given array is an associative array.
334
+ * @param array $array The array to test
335
+ * @return bool True if the array is associative, false if not
336
+ */
337
+ private function isAssociative (array $ array )
338
+ {
339
+ if ($ array === []) {
340
+ return false ;
341
+ }
342
+
343
+ $ expected = 0 ;
344
+
345
+ foreach ($ array as $ key => $ _ ) {
346
+ if ($ key !== $ expected ++) {
347
+ return true ;
348
+ }
349
+ }
350
+
351
+ return false ;
323
352
}
324
353
325
354
/**
@@ -349,14 +378,35 @@ private function popStack()
349
378
private function outputJson ($ value , $ token )
350
379
{
351
380
$ encoded = json_encode ($ value , $ this ->options );
381
+ $ error = json_last_error ();
352
382
353
- if (json_last_error () !== JSON_ERROR_NONE ) {
354
- $ this ->addError (json_last_error_msg ());
383
+ if ($ error !== JSON_ERROR_NONE ) {
384
+ $ this ->addError (sprintf ( ' %s (%s) ' , json_last_error_msg (), $ this -> getJsonErrorName ( $ error ));
355
385
}
356
386
357
387
$ this ->output ($ encoded , $ token );
358
388
}
359
389
390
+ /**
391
+ * Returns the name of the JSON error constant.
392
+ * @param int $error The error code to find
393
+ * @return string The name for the error code
394
+ */
395
+ private function getJsonErrorName ($ error )
396
+ {
397
+ $ matches = array_keys (get_defined_constants (), $ error , true );
398
+ $ prefix = 'JSON_ERROR_ ' ;
399
+ $ prefixLength = strlen ($ prefix );
400
+
401
+ foreach ($ matches as $ match ) {
402
+ if (strncmp ($ match , $ prefix , $ prefixLength ) === 0 ) {
403
+ return $ match ;
404
+ }
405
+ }
406
+
407
+ return 'UNKNOWN ' ;
408
+ }
409
+
360
410
/**
361
411
* Passes the given token to the output stream and ensures the next token is preceded by a newline.
362
412
* @param string $string The token to write to the output stream
@@ -383,7 +433,7 @@ private function output($string, $token)
383
433
$ this ->write ($ indent , JsonToken::T_WHITESPACE );
384
434
}
385
435
386
- $ this ->line += 1 ;
436
+ $ this ->line ++ ;
387
437
$ this ->column = strlen ($ indent ) + 1 ;
388
438
}
389
439
0 commit comments