1: <?php
2: /*
3: * Copyright (c) 2015 @trashtoy
4: * https://github.com/trashtoy/
5: *
6: * Permission is hereby granted, free of charge, to any person obtaining a copy of
7: * this software and associated documentation files (the "Software"), to deal in
8: * the Software without restriction, including without limitation the rights to use,
9: * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
10: * Software, and to permit persons to whom the Software is furnished to do so,
11: * subject to the following conditions:
12: *
13: * The above copyright notice and this permission notice shall be included in all
14: * copies or substantial portions of the Software.
15: *
16: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18: * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19: * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20: * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21: * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22: */
23: /**
24: * PHP class file.
25: * @auhtor trashtoy
26: * @since 2.1.0
27: */
28: namespace Peach\DF;
29:
30: use InvalidArgumentException;
31: use Peach\DF\JsonCodec\Context;
32: use Peach\DF\JsonCodec\DecodeException;
33: use Peach\DF\JsonCodec\Root;
34: use Peach\Util\ArrayMap;
35: use Peach\Util\Strings;
36: use Peach\Util\Values;
37:
38: /**
39: * JSON 形式の文字列を扱う Codec です.
40: * このクラスは {@link http://tools.ietf.org/html/rfc7159 RFC 7159}
41: * の仕様に基いて JSON のデコード (JSON を値に変換) とエンコード (値を JSON に変換)
42: * を行います.
43: *
44: * RFC 7159 によると JSON 文字列のエンコーディングは UTF-8, UTF-16, UTF-32
45: * のいずれかであると定義されていますが, この実装は UTF-8 でエンコーディングされていることを前提とします.
46: * UTF-8 以外の文字列をデコードした場合はエラーとなります.
47: */
48: class JsonCodec implements Codec
49: {
50: /**
51: * 定数 JSON_HEX_TAG に相当するオプションです.
52: * 文字 %x3c (LESS-THAN SIGN) および %x3e (GREATER-THAN SIGN)
53: * をそれぞれ "\u003C" および "\u003E" にエンコードします
54: *
55: * @var int
56: */
57: const HEX_TAG = 1;
58:
59: /**
60: * 定数 JSON_HEX_AMP に相当するオプションです.
61: * 文字 "&" を "\u0026" にエンコードします.
62: *
63: * @var int
64: */
65: const HEX_AMP = 2;
66:
67: /**
68: * 定数 JSON_HEX_APOS に相当するオプションです.
69: * 文字 "'" を "\u0027" にエンコードします.
70: *
71: * @var int
72: */
73: const HEX_APOS = 4;
74:
75: /**
76: * 定数 JSON_HEX_QUOT に相当するオプションです.
77: * 文字 '"' を "\u0022" にエンコードします.
78: *
79: * @var int
80: */
81: const HEX_QUOT = 8;
82:
83: /**
84: * 定数 JSON_FORCE_OBJECT に相当するオプションです.
85: * 通常は array 形式でエンコードされる配列を object 形式でエンコードします.
86: * 具体的には以下の配列の出力に影響します.
87: *
88: * - 空の配列
89: * - 添字が 0 から始まる整数の連続 (0, 1, 2, ...) となっている配列
90: *
91: * @var int
92: */
93: const FORCE_OBJECT = 16;
94:
95: /**
96: * 定数 JSON_NUMERIC_CHECK に相当するオプションです.
97: * 数値表現の文字列を数値としてエンコードします.
98: */
99: const NUMERIC_CHECK = 32;
100:
101: /**
102: * 定数 JSON_UNESCAPED_SLASHES に相当するオプションです.
103: * エンコードの際に "/" をエスケープしないようにします.
104: *
105: * @var int
106: */
107: const UNESCAPED_SLASHES = 64;
108:
109: /**
110: * 定数 JSON_PRETTY_PRINT に相当するオプションです.
111: * object, array 形式の書式でエンコードする際に,
112: * 半角スペース 4 個でインデントして整形します.
113: *
114: * @var int
115: */
116: const PRETTY_PRINT = 128;
117:
118: /**
119: * 定数 JSON_UNESCAPED_UNICODE に相当するオプションです.
120: * エンコードの際にマルチバイト文字を UTF-8 文字として表現します.
121: *
122: * @var int
123: */
124: const UNESCAPED_UNICODE = 256;
125:
126: /**
127: * 定数 JSON_PRESERVE_ZERO_FRACTION に相当するオプションです.
128: * float 型の値を常に float 値としてエンコードします.
129: * このオプションが OFF の場合, 小数部が 0 の数値 (2.0 など) は
130: * 整数としてエンコードされます.
131: *
132: * @var int
133: */
134: const PRESERVE_ZERO_FRACTION = 1024;
135:
136: /**
137: * {@link http://php.net/manual/function.json-decode.php json_decode()}
138: * の第 2 引数に相当する, このクラス独自のオプションです.
139: * このオプションが ON の場合, object 形式の値をデコードする際に配列に変換します.
140: * (デフォルトでは stdClass オブジェクトとなります)
141: *
142: * @var int
143: */
144: const OBJECT_AS_ARRAY = 1;
145:
146: /**
147: * 定数 JSON_BIGINT_AS_STRING に相当するオプションです.
148: * 巨大整数をデコードする際に, int の範囲に収まらない値を文字列に変換します.
149: * (デフォルトでは float 型となります)
150: *
151: * @var int
152: */
153: const BIGINT_AS_STRING = 2;
154:
155: /**
156: * encode() の出力内容をカスタマイズするオプションです.
157: *
158: * @var ArrayMap
159: */
160: private $encodeOptions;
161:
162: /**
163: * decode() の出力内容をカスタマイズするオプションです.
164: *
165: * @var ArrayMap
166: */
167: private $decodeOptions;
168:
169: /**
170: * 文字列をエンコードする際に使用する Utf8Codec です.
171: *
172: * @var Utf8Codec
173: */
174: private $utf8Codec;
175:
176: /**
177: * 新しい JsonCodec を構築します.
178: * 引数に encode() および decode() の出力のカスタマイズオプションを指定することが出来ます.
179: * 引数は配列または整数を指定することが出来ます.
180: *
181: * - 配列の場合: キーにオプション定数, 値に true または false を指定してください.
182: * - 整数の場合: 各オプションのビットマスクを指定してください. 例えば JsonCodec::HEX_TAG | JsonCodec::HEX_AMP のような形式となります.
183: *
184: * @param array|int $encodeOptions encode() のカスタマイズオプション
185: * @param array|int $decodeOptions decode() のカスタマイズオプション
186: */
187: public function __construct($encodeOptions = null, $decodeOptions = null)
188: {
189: $this->encodeOptions = $this->initOptions($encodeOptions);
190: $this->decodeOptions = $this->initOptions($decodeOptions);
191: $this->utf8Codec = new Utf8Codec();
192: }
193:
194: /**
195: * コンストラクタに指定された $encodeOptions および $decodeOptions
196: * を初期化します.
197: *
198: * @param array|int $options コンストラクタに指定されたオプション
199: * @return ArrayMap 各オプションの ON/OFF をあらわす ArrayMap
200: */
201: private function initOptions($options)
202: {
203: $result = new ArrayMap();
204: if (is_scalar($options)) {
205: return $this->initOptionsByBitMask(Values::intValue($options, 0));
206: }
207: if (!is_array($options)) {
208: return $result;
209: }
210:
211: foreach ($options as $key => $value) {
212: $result->put($key, \Peach\Util\Values::boolValue($value));
213: }
214: return $result;
215: }
216:
217: /**
218: * ビットマスクを配列に変換します.
219: *
220: * @param int $options オプションをあらわす整数
221: * @return ArrayMap 変換後のオプション
222: */
223: private function initOptionsByBitMask($options)
224: {
225: $opt = 1;
226: $result = new ArrayMap();
227: while ($options) {
228: $result->put($opt, (bool) ($options % 2));
229: $options >>= 1;
230: $opt <<= 1;
231: }
232: return $result;
233: }
234:
235: /**
236: * 指定されたエンコード用オプションが ON かどうかを調べます.
237: *
238: * @param int $code オプション (定義されている定数)
239: * @return bool 指定されたオプションが ON の場合は true, それ以外は false
240: */
241: public function getEncodeOption($code)
242: {
243: return $this->encodeOptions->get($code, false);
244: }
245:
246: /**
247: * 指定されたデコード用オプションが ON かどうかを調べます.
248: *
249: * @param int $code オプション (定義されている定数)
250: * @return bool 指定されたオプションが ON の場合は true, それ以外は false
251: */
252: public function getDecodeOption($code)
253: {
254: return $this->decodeOptions->get($code, false);
255: }
256:
257: /**
258: * 指定された JSON 文字列を値に変換します.
259: *
260: * 引数が空白文字列 (または null, false) の場合は null を返します.
261: *
262: * @param string $text 変換対象の JSON 文字列
263: * @return mixed 変換結果
264: */
265: public function decode($text)
266: {
267: if (Strings::isWhitespace($text)) {
268: return null;
269: }
270:
271: try {
272: $root = new Root();
273: $root->handle(new Context($text, $this->decodeOptions));
274: return $root->getResult();
275: } catch (DecodeException $e) {
276: throw new InvalidArgumentException($e->getMessage());
277: }
278: }
279:
280: /**
281: * 指定された値を JSON 文字列に変換します.
282: *
283: * @param mixed $var 変換対象の値
284: * @return string JSON 文字列
285: */
286: public function encode($var)
287: {
288: return $this->encodeValue($var);
289: }
290:
291: /**
292: * encode() の本体の処理です.
293: * 指定された値がスカラー型 (null, 真偽値, 数値, 文字列), 配列, オブジェクトのいずれかにも該当しない場合,
294: * 文字列にキャストした結果をエンコードします.
295: *
296: * @param mixed $var 変換対象の値
297: * @return string JSON 文字列
298: * @ignore
299: */
300: public function encodeValue($var)
301: {
302: if ($var === null) {
303: return "null";
304: }
305: if ($var === true) {
306: return "true";
307: }
308: if ($var === false) {
309: return "false";
310: }
311: if (is_float($var)) {
312: return $this->encodeFloat($var);
313: }
314: if (is_integer($var)) {
315: return strval($var);
316: }
317: if (is_string($var)) {
318: return is_numeric($var) ? $this->encodeNumeric($var) : $this->encodeString($var);
319: }
320: if (is_array($var)) {
321: return $this->checkKeySequence($var) ? $this->encodeArray($var) : $this->encodeObject($var);
322: }
323: if (is_object($var)) {
324: $arr = (array) $var;
325: return $this->encodeValue($arr);
326: }
327:
328: return $this->encodeValue(Values::stringValue($var));
329: }
330:
331: /**
332: * 数値形式の文字列を数値としてエンコードします.
333: *
334: * @param string $var
335: */
336: private function encodeNumeric($var)
337: {
338: if (!$this->getEncodeOption(self::NUMERIC_CHECK)) {
339: return $this->encodeString($var);
340: }
341:
342: $num = preg_match("/^-?[0-9]+$/", $var) ? intval($var) : floatval($var);
343: return $this->encodeValue($num);
344: }
345:
346: /**
347: * float 値を文字列に変換します.
348: *
349: * @param float $var 変換対象の float 値
350: * @return string 変換結果
351: */
352: private function encodeFloat($var)
353: {
354: $str = strval($var);
355: if (!$this->getEncodeOption(self::PRESERVE_ZERO_FRACTION)) {
356: return $str;
357: }
358: if (false !== strpos($str, "E")) {
359: return $str;
360: }
361: return (floor($var) === $var) ? "{$str}.0" : $str;
362: }
363:
364: /**
365: * 配列のキーが 0, 1, 2, ……という具合に 0 から始まる整数の連続になっていた場合のみ true,
366: * それ以外は false を返します.
367: * ただし, オプション FORCE_OBJECT が ON の場合は常に false を返します.
368: *
369: * @param array $arr 変換対象の配列
370: * @return bool 配列のキーが整数の連続になっていた場合のみ true
371: */
372: private function checkKeySequence(array $arr) {
373: if ($this->getEncodeOption(self::FORCE_OBJECT)) {
374: return false;
375: }
376:
377: $i = 0;
378: foreach (array_keys($arr) as $key) {
379: if ($i !== $key) {
380: return false;
381: }
382: $i++;
383: }
384: return true;
385: }
386:
387: /**
388: * 文字列を JSON 文字列に変換します.
389: *
390: * @param string $str 変換対象の文字列
391: * @return string JSON 文字列
392: * @ignore
393: */
394: public function encodeString($str)
395: {
396: $self = $this;
397: $callback = function ($num) use ($self) {
398: return $self->encodeCodePoint($num);
399: };
400: $unicodeList = $this->utf8Codec->decode($str);
401: return '"' . implode("", array_map($callback, $unicodeList)) . '"';
402: }
403:
404: /**
405: * 指定された Unicode 符号点を JSON 文字に変換します.
406: *
407: * @param int $num Unicode 符号点
408: * @return string 指定された Unicode 符号点に対応する文字列
409: * @ignore
410: */
411: public function encodeCodePoint($num)
412: {
413: // @codeCoverageIgnoreStart
414: static $hexList = array(
415: 0x3C => self::HEX_TAG,
416: 0x3E => self::HEX_TAG,
417: 0x26 => self::HEX_AMP,
418: 0x27 => self::HEX_APOS,
419: 0x22 => self::HEX_QUOT,
420: );
421: static $encodeList = array(
422: 0x22 => "\\\"",
423: 0x5C => "\\\\",
424: 0x08 => "\\b",
425: 0x0C => "\\f",
426: 0x0A => "\\n",
427: 0x0D => "\\r",
428: 0x09 => "\\t",
429: );
430: // @codeCoverageIgnoreEnd
431:
432: if (array_key_exists($num, $hexList) && $this->getEncodeOption($hexList[$num])) {
433: return "\\u00" . strtoupper(dechex($num));
434: }
435: if (array_key_exists($num, $encodeList)) {
436: return $encodeList[$num];
437: }
438: if ($num === 0x2F) {
439: return $this->getEncodeOption(self::UNESCAPED_SLASHES) ? "/" : "\\/";
440: }
441: if (0x20 <= $num && $num < 0x80) {
442: return chr($num);
443: }
444: if (0x80 <= $num && $this->getEncodeOption(self::UNESCAPED_UNICODE)) {
445: return $this->utf8Codec->encode($num);
446: }
447: return "\\u" . str_pad(dechex($num), 4, "0", STR_PAD_LEFT);
448: }
449:
450: /**
451: * 指定された配列を JSON の array 表記に変換します.
452: * オプション PRETTY_PRINT が有効化されている場合,
453: * json_encode の JSON_PRETTY_PRINT と同様に半角スペース 4 個と改行文字で整形します.
454: *
455: * @param array $arr 変換対象
456: * @return string JSON 文字列
457: */
458: private function encodeArray(array $arr)
459: {
460: $prettyPrintEnabled = $this->getEncodeOption(self::PRETTY_PRINT);
461:
462: $indent = $prettyPrintEnabled ? PHP_EOL . " " : "";
463: $start = "[" . $indent;
464: $end = $prettyPrintEnabled ? PHP_EOL . "]" : "]";
465: $self = $this;
466: $callback = function ($value) use ($self, $prettyPrintEnabled, $indent) {
467: $valueResult = $self->encodeValue($value);
468: return $prettyPrintEnabled ? str_replace(PHP_EOL, $indent, $valueResult) : $valueResult;
469: };
470: return $start . implode("," . $indent, array_map($callback, $arr)) . $end;
471: }
472:
473: /**
474: * 指定された配列を JSON の object 表記に変換します.
475: * オプション PRETTY_PRINT が有効化されている場合,
476: * json_encode の JSON_PRETTY_PRINT と同様に半角スペース 4 個と改行文字で整形します.
477: *
478: * @param array $arr 変換対象
479: * @return string JSON 文字列
480: */
481: private function encodeObject(array $arr)
482: {
483: $prettyPrintEnabled = $this->getEncodeOption(self::PRETTY_PRINT);
484:
485: $indent = $prettyPrintEnabled ? PHP_EOL . " " : "";
486: $start = "{" . $indent;
487: $end = $prettyPrintEnabled ? PHP_EOL . "}" : "}";
488: $self = $this;
489: $callback = function ($key, $value) use ($self, $prettyPrintEnabled, $indent) {
490: $coron = $prettyPrintEnabled ? ": " : ":";
491: $valueResult = $self->encodeValue($value);
492: $valueJson = $prettyPrintEnabled ? str_replace(PHP_EOL, $indent, $valueResult) : $valueResult;
493: return $self->encodeString($key) . $coron . $valueJson;
494: };
495: return $start . implode("," . $indent, array_map($callback, array_keys($arr), array_values($arr))) . $end;
496: }
497: }
498: