Overview

Namespaces

  • Peach
    • DF
    • DT
    • Http
      • Body
      • Header
    • Markup
    • Util

Classes

  • Peach\DF\Base64Codec
  • Peach\DF\CodecChain
  • Peach\DF\JsonCodec
  • Peach\DF\SerializationCodec
  • Peach\DF\Utf8Codec
  • Peach\DT\AbstractTime
  • Peach\DT\Clock
  • Peach\DT\Date
  • Peach\DT\Datetime
  • Peach\DT\DefaultClock
  • Peach\DT\FixedClock
  • Peach\DT\FormatWrapper
  • Peach\DT\HttpDateFormat
  • Peach\DT\OffsetClock
  • Peach\DT\ShiftFormat
  • Peach\DT\SimpleFormat
  • Peach\DT\TimeEquator
  • Peach\DT\Timestamp
  • Peach\DT\TimeWrapper
  • Peach\DT\UnixTimeFormat
  • Peach\DT\Util
  • Peach\DT\W3cDatetimeFormat
  • Peach\Http\Body
  • Peach\Http\Body\CodecRenderer
  • Peach\Http\Body\StringRenderer
  • Peach\Http\DefaultEndpoint
  • Peach\Http\Header\CookieItem
  • Peach\Http\Header\CookieOptions
  • Peach\Http\Header\HttpDate
  • Peach\Http\Header\NoField
  • Peach\Http\Header\QualityValues
  • Peach\Http\Header\Raw
  • Peach\Http\Header\SetCookie
  • Peach\Http\Header\Status
  • Peach\Http\Request
  • Peach\Http\Response
  • Peach\Http\Util
  • Peach\Markup\AbstractHelper
  • Peach\Markup\AbstractRenderer
  • Peach\Markup\BaseHelper
  • Peach\Markup\BreakControlWrapper
  • Peach\Markup\Builder
  • Peach\Markup\Code
  • Peach\Markup\Comment
  • Peach\Markup\ContainerElement
  • Peach\Markup\Context
  • Peach\Markup\DebugBuilder
  • Peach\Markup\DebugContext
  • Peach\Markup\DefaultBreakControl
  • Peach\Markup\DefaultBuilder
  • Peach\Markup\DefaultContext
  • Peach\Markup\Element
  • Peach\Markup\EmptyElement
  • Peach\Markup\HelperObject
  • Peach\Markup\HtmlHelper
  • Peach\Markup\Indent
  • Peach\Markup\MinimalBreakControl
  • Peach\Markup\NameBreakControl
  • Peach\Markup\NameValidator
  • Peach\Markup\NodeList
  • Peach\Markup\None
  • Peach\Markup\SgmlRenderer
  • Peach\Markup\Text
  • Peach\Markup\XmlRenderer
  • Peach\Util\AbstractMapEntry
  • Peach\Util\ArrayMap
  • Peach\Util\ArrayMapEntry
  • Peach\Util\Arrays
  • Peach\Util\DefaultComparator
  • Peach\Util\DefaultEquator
  • Peach\Util\HashMap
  • Peach\Util\HashMapEntry
  • Peach\Util\Strings
  • Peach\Util\Values

Interfaces

  • Peach\DF\Codec
  • Peach\DT\Format
  • Peach\DT\Time
  • Peach\Http\BodyRenderer
  • Peach\Http\Endpoint
  • Peach\Http\HeaderField
  • Peach\Http\MultiHeaderField
  • Peach\Http\SingleHeaderField
  • Peach\Markup\BreakControl
  • Peach\Markup\Component
  • Peach\Markup\Container
  • Peach\Markup\Helper
  • Peach\Markup\Node
  • Peach\Markup\Renderer
  • Peach\Util\Comparable
  • Peach\Util\Comparator
  • Peach\Util\Equator
  • Peach\Util\Map
  • Peach\Util\MapEntry
  • Overview
  • Namespace
  • Class
  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: 
PEACH2 API documentation generated by ApiGen