/
Json.php
430 lines (378 loc) · 13.8 KB
/
Json.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
<?php
namespace Laminas\Json;
use Laminas\Json\Exception\RuntimeException;
use SplQueue;
use function array_pop;
use function count;
use function end;
use function function_exists;
use function is_array;
use function is_int;
use function is_object;
use function json_decode;
use function json_encode;
use function json_last_error;
use function method_exists;
use function preg_match;
use function sprintf;
use function str_repeat;
use function str_replace;
use function strlen;
use function trim;
use const JSON_ERROR_CTRL_CHAR;
use const JSON_ERROR_DEPTH;
use const JSON_ERROR_NONE;
use const JSON_ERROR_SYNTAX;
use const JSON_HEX_AMP;
use const JSON_HEX_APOS;
use const JSON_HEX_QUOT;
use const JSON_HEX_TAG;
use const JSON_PRETTY_PRINT;
/**
* Class for encoding to and decoding from JSON.
*/
class Json
{
/**
* How objects should be encoded: as arrays or as stdClass.
*
* TYPE_ARRAY is 1, which also conveniently evaluates to a boolean true
* value, allowing it to be used with ext/json's functions.
*/
public const TYPE_ARRAY = 1;
public const TYPE_OBJECT = 0;
/**
* Whether or not to use the built-in PHP functions.
*
* @var bool
*/
public static $useBuiltinEncoderDecoder = false;
/**
* Decodes the given $encodedValue string from JSON.
*
* Uses json_decode() from ext/json if available.
*
* @param string $encodedValue Encoded in JSON format
* @param int $objectDecodeType Optional; flag indicating how to decode
* objects. See {@link Decoder::decode()} for details.
* @return mixed
* @throws RuntimeException
*/
public static function decode($encodedValue, $objectDecodeType = self::TYPE_OBJECT)
{
$encodedValue = (string) $encodedValue;
if (function_exists('json_decode') && static::$useBuiltinEncoderDecoder !== true) {
return self::decodeViaPhpBuiltIn($encodedValue, $objectDecodeType);
}
return Decoder::decode($encodedValue, $objectDecodeType);
}
/**
* Encode the mixed $valueToEncode into the JSON format
*
* Encodes using ext/json's json_encode() if available.
*
* NOTE: Object should not contain cycles; the JSON format
* does not allow object reference.
*
* NOTE: Only public variables will be encoded
*
* NOTE: Encoding native javascript expressions are possible using Laminas\Json\Expr.
* You can enable this by setting $options['enableJsonExprFinder'] = true
*
* @see Laminas\Json\Expr
*
* @param bool $cycleCheck Optional; whether or not to check for object recursion; off by default
* @param array $options Additional options used during encoding
* @return string JSON encoded object
*/
public static function encode(mixed $valueToEncode, $cycleCheck = false, array $options = [])
{
if (is_object($valueToEncode)) {
if (method_exists($valueToEncode, 'toJson')) {
return $valueToEncode->toJson();
}
if (method_exists($valueToEncode, 'toArray')) {
return static::encode($valueToEncode->toArray(), $cycleCheck, $options);
}
}
// Pre-process and replace javascript expressions with placeholders
$javascriptExpressions = new SplQueue();
if (
isset($options['enableJsonExprFinder'])
&& $options['enableJsonExprFinder'] === true
) {
$valueToEncode = static::recursiveJsonExprFinder($valueToEncode, $javascriptExpressions);
}
// Encoding
$prettyPrint = isset($options['prettyPrint']) && ($options['prettyPrint'] === true);
$encodedResult = self::encodeValue($valueToEncode, $cycleCheck, $options, $prettyPrint);
// Post-process to revert back any Laminas\Json\Expr instances.
$encodedResult = self::injectJavascriptExpressions($encodedResult, $javascriptExpressions);
return $encodedResult;
}
/**
* Discover and replace javascript expressions with temporary placeholders.
*
* Check each value to determine if it is a Laminas\Json\Expr; if so, replace the value with
* a magic key and add the javascript expression to the queue.
*
* NOTE this method is recursive.
*
* NOTE: This method is used internally by the encode method.
*
* @see encode
*
* @param mixed $value a string - object property to be encoded
* @param null|string|int $currentKey
* @return mixed
*/
protected static function recursiveJsonExprFinder(
mixed $value,
SplQueue $javascriptExpressions,
$currentKey = null
) {
if ($value instanceof Expr) {
// TODO: Optimize with ascii keys, if performance is bad
$magicKey = "____" . $currentKey . "_" . count($javascriptExpressions);
$javascriptExpressions->enqueue([
// If currentKey is integer, encodeUnicodeString call is not required.
'magicKey' => is_int($currentKey) ? $magicKey : Encoder::encodeUnicodeString($magicKey),
'value' => $value,
]);
return $magicKey;
}
if (is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = static::recursiveJsonExprFinder($value[$k], $javascriptExpressions, $k);
}
return $value;
}
if (is_object($value)) {
foreach ($value as $k => $v) {
$value->$k = static::recursiveJsonExprFinder($value->$k, $javascriptExpressions, $k);
}
return $value;
}
return $value;
}
/**
* Pretty-print JSON string
*
* Use 'indent' option to select indentation string; by default, four
* spaces are used.
*
* @param string $json Original JSON string
* @param array $options Encoding options
* @return string
*/
public static function prettyPrint($json, array $options = [])
{
$indentString = $options['indent'] ?? ' ';
$json = trim($json);
$length = strlen($json);
$stack = [];
$result = '';
$inLiteral = false;
for ($i = 0; $i < $length; ++$i) {
switch ($json[$i]) {
case '{':
case '[':
if ($inLiteral) {
break;
}
$stack[] = $json[$i];
$result .= $json[$i];
while (isset($json[$i + 1]) && preg_match('/\s/', $json[$i + 1])) {
++$i;
}
if (isset($json[$i + 1]) && $json[$i + 1] !== '}' && $json[$i + 1] !== ']') {
$result .= "\n" . str_repeat($indentString, count($stack));
}
continue 2;
case '}':
case ']':
if ($inLiteral) {
break;
}
$last = end($stack);
if (
($last === '{' && $json[$i] === '}')
|| ($last === '[' && $json[$i] === ']')
) {
array_pop($stack);
}
$result .= $json[$i];
while (isset($json[$i + 1]) && preg_match('/\s/', $json[$i + 1])) {
++$i;
}
if (isset($json[$i + 1]) && ($json[$i + 1] === '}' || $json[$i + 1] === ']')) {
$result .= "\n" . str_repeat($indentString, count($stack) - 1);
}
continue 2;
case '"':
$result .= '"';
if (! $inLiteral) {
$inLiteral = true;
} else {
$backslashes = 0;
$n = $i;
while ($json[--$n] === '\\') {
++$backslashes;
}
if (($backslashes % 2) === 0) {
$inLiteral = false;
while (isset($json[$i + 1]) && preg_match('/\s/', $json[$i + 1])) {
++$i;
}
if (isset($json[$i + 1]) && ($json[$i + 1] === '}' || $json[$i + 1] === ']')) {
$result .= "\n" . str_repeat($indentString, count($stack) - 1);
}
}
}
continue 2;
case ':':
if (! $inLiteral) {
$result .= ': ';
continue 2;
}
break;
case ',':
if (! $inLiteral) {
$result .= ',' . "\n" . str_repeat($indentString, count($stack));
continue 2;
}
break;
default:
if (! $inLiteral && preg_match('/\s/', $json[$i])) {
continue 2;
}
break;
}
$result .= $json[$i];
if ($inLiteral) {
continue;
}
while (isset($json[$i + 1]) && preg_match('/\s/', $json[$i + 1])) {
++$i;
}
if (isset($json[$i + 1]) && ($json[$i + 1] === '}' || $json[$i + 1] === ']')) {
$result .= "\n" . str_repeat($indentString, count($stack) - 1);
}
}
return $result;
}
/**
* Decode a value using the PHP built-in json_decode function.
*
* @param string $encodedValue
* @param int $objectDecodeType
* @return mixed
* @throws RuntimeException
*/
private static function decodeViaPhpBuiltIn($encodedValue, $objectDecodeType)
{
$decoded = json_decode($encodedValue, (bool) $objectDecodeType);
switch (json_last_error()) {
case JSON_ERROR_NONE:
return $decoded;
case JSON_ERROR_DEPTH:
throw new RuntimeException('Decoding failed: Maximum stack depth exceeded');
case JSON_ERROR_CTRL_CHAR:
throw new RuntimeException('Decoding failed: Unexpected control character found');
case JSON_ERROR_SYNTAX:
throw new RuntimeException('Decoding failed: Syntax error');
default:
throw new RuntimeException('Decoding failed');
}
}
/**
* Encode a value to JSON.
*
* Intermediary step between injecting JavaScript expressions.
*
* Delegates to either the PHP built-in json_encode operation, or the
* Encoder component, based on availability of the built-in and/or whether
* or not the component encoder is requested.
*
* @param bool $cycleCheck
* @param array $options
* @param bool $prettyPrint
* @return string
*/
private static function encodeValue(mixed $valueToEncode, $cycleCheck, array $options, $prettyPrint)
{
if (function_exists('json_encode') && static::$useBuiltinEncoderDecoder !== true) {
return self::encodeViaPhpBuiltIn($valueToEncode, $prettyPrint);
}
return self::encodeViaEncoder($valueToEncode, $cycleCheck, $options, $prettyPrint);
}
/**
* Encode a value to JSON using the PHP built-in json_encode function.
*
* Uses the encoding options:
*
* - JSON_HEX_TAG
* - JSON_HEX_APOS
* - JSON_HEX_QUOT
* - JSON_HEX_AMP
*
* If $prettyPrint is boolean true, also uses JSON_PRETTY_PRINT.
*
* @param bool $prettyPrint
* @return string|false Boolean false return value if json_encode is not
* available, or the $useBuiltinEncoderDecoder flag is enabled.
*/
private static function encodeViaPhpBuiltIn(mixed $valueToEncode, $prettyPrint = false)
{
if (! function_exists('json_encode') || static::$useBuiltinEncoderDecoder === true) {
return false;
}
$encodeOptions = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP;
if ($prettyPrint) {
$encodeOptions |= JSON_PRETTY_PRINT;
}
return json_encode($valueToEncode, $encodeOptions);
}
/**
* Encode a value to JSON using the Encoder class.
*
* Passes the value, cycle check flag, and options to Encoder::encode().
*
* Once the result is returned, determines if pretty printing is required,
* and, if so, returns the result of that operation, otherwise returning
* the encoded value.
*
* @param bool $cycleCheck
* @param array $options
* @param bool $prettyPrint
* @return string
*/
private static function encodeViaEncoder(mixed $valueToEncode, $cycleCheck, array $options, $prettyPrint)
{
$encodedResult = Encoder::encode($valueToEncode, $cycleCheck, $options);
if ($prettyPrint) {
return self::prettyPrint($encodedResult, ['indent' => ' ']);
}
return $encodedResult;
}
/**
* Inject javascript expressions into the encoded value.
*
* Loops through each, substituting the "magicKey" of each with its
* associated value.
*
* @param string $encodedValue
* @return string
*/
private static function injectJavascriptExpressions($encodedValue, SplQueue $javascriptExpressions)
{
foreach ($javascriptExpressions as $expression) {
$encodedValue = str_replace(
sprintf('"%s"', $expression['magicKey']),
$expression['value'],
(string) $encodedValue
);
}
return $encodedValue;
}
}