├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Decoder.php ├── Encoder.php ├── Exception ├── BadMethodCallException.php ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── RecursionException.php └── RuntimeException.php ├── Expr.php └── Json.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 3.1.3 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 3.1.2 - 2019-10-09 28 | 29 | ### Added 30 | 31 | - Nothing. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - [#46](https://github.com/zendframework/zend-json/pull/46) changes 48 | curly braces in array and string offset access to square brackets 49 | in order to prevent issues under the upcoming PHP 7.4 release. 50 | 51 | - [#37](https://github.com/zendframework/zend-json/pull/37) fixes 52 | output of `\Zend\Json::prettyPrint` to not remove spaces after 53 | commas in value. 54 | 55 | ## 3.1.1 - 2019-06-18 56 | 57 | ### Added 58 | 59 | - [#44](https://github.com/zendframework/zend-json/pull/44) adds support for PHP 7.3. 60 | 61 | ### Changed 62 | 63 | - Nothing. 64 | 65 | ### Deprecated 66 | 67 | - Nothing. 68 | 69 | ### Removed 70 | 71 | - Nothing. 72 | 73 | ### Fixed 74 | 75 | - Nothing. 76 | 77 | ## 3.1.0 - 2018-01-04 78 | 79 | ### Added 80 | 81 | - [#35](https://github.com/zendframework/zend-json/pull/35) and 82 | [#39](https://github.com/zendframework/zend-json/pull/39) add support for PHP 83 | 7.1 and PHP 7.2. 84 | 85 | ### Deprecated 86 | 87 | - Nothing. 88 | 89 | ### Removed 90 | 91 | - [#35](https://github.com/zendframework/zend-json/pull/35) removes support for 92 | PHP 5.5. 93 | 94 | - [#35](https://github.com/zendframework/zend-json/pull/35) removes support for 95 | HHVM. 96 | 97 | ### Fixed 98 | 99 | - [#38](https://github.com/zendframework/zend-json/pull/38) provides a fix to 100 | `Json::prettyPrint()` to ensure that empty arrays and objects are printed 101 | without newlines. 102 | 103 | - [#38](https://github.com/zendframework/zend-json/pull/38) provides a fix to 104 | `Json::prettyPrint()` to remove additional newlines preceding a closing 105 | bracket. 106 | 107 | ## 3.0.0 - 2016-03-31 108 | 109 | ### Added 110 | 111 | - [#21](https://github.com/zendframework/zend-json/pull/21) adds documentation 112 | and publishes it to https://zendframework.github.io/zend-json/ 113 | 114 | ### Deprecated 115 | 116 | - Nothing. 117 | 118 | ### Removed 119 | 120 | - [#20](https://github.com/zendframework/zend-json/pull/20) removes the 121 | `Zend\Json\Server` subcomponent, which has been extracted to 122 | [zend-json-server](https://zendframework.github.io/zend-json-server/). 123 | If you use that functionality, install the new component. 124 | - [#21](https://github.com/zendframework/zend-json/pull/21) removes the 125 | `Zend\Json\Json::fromXml()` functionality, which has been extracted to 126 | [zend-xml2json](https://zendframework.github.io/zend-xml2json/). If you used 127 | this functionality, you will need to install the new package, and rewrite 128 | calls to `Zend\Json\Json::fromXml()` to `Zend\Xml2Json\Xml2Json::fromXml()`. 129 | - [#20](https://github.com/zendframework/zend-json/pull/20) and 130 | [#21](https://github.com/zendframework/zend-json/pull/21) removes dependencies 131 | on zendframework/zendxml, zendframework/zend-stdlib, 132 | zendframework/zend-server, and zendframework-zend-http, due to the above 133 | listed component extractions. 134 | 135 | ### Fixed 136 | 137 | - Nothing. 138 | 139 | ## 2.6.1 - 2016-02-04 140 | 141 | ### Added 142 | 143 | - Nothing. 144 | 145 | ### Deprecated 146 | 147 | - Nothing. 148 | 149 | ### Removed 150 | 151 | - Nothing. 152 | 153 | ### Fixed 154 | 155 | - [#18](https://github.com/zendframework/zend-json/pull/18) updates dependencies 156 | to allow usage on PHP 7, as well as with zend-stdlib v3. 157 | 158 | ## 2.6.0 - 2015-11-18 159 | 160 | ### Added 161 | 162 | - Nothing. 163 | 164 | ### Deprecated 165 | 166 | - Nothing. 167 | 168 | ### Removed 169 | 170 | - [#5](https://github.com/zendframework/zend-json/pull/5) removes 171 | zendframework/zend-stdlib as a required dependency, marking it instead 172 | optional, as it is only used for the `Server` subcomponent. 173 | 174 | ### Fixed 175 | 176 | - Nothing. 177 | 178 | ## 2.5.2 - 2015-08-05 179 | 180 | ### Added 181 | 182 | - Nothing. 183 | 184 | ### Deprecated 185 | 186 | - Nothing. 187 | 188 | ### Removed 189 | 190 | - Nothing. 191 | 192 | ### Fixed 193 | 194 | - [#3](https://github.com/zendframework/zend-json/pull/3) fixes an array key 195 | name from `intent` to `indent` to ensure indentation works correctly during 196 | pretty printing. 197 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2019, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-json 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-json](https://github.com/laminas/laminas-json). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-json.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-json) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-json/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-json?branch=master) 9 | 10 | `Zend\Json` provides convenience methods for serializing native PHP to JSON and 11 | decoding JSON to native PHP. For more information on JSON, visit the JSON 12 | [project site](http://www.json.org/). 13 | 14 | 15 | ## Installation 16 | 17 | Run the following to install this library: 18 | 19 | ```bash 20 | $ composer require zendframework/zend-json 21 | ``` 22 | 23 | ## Documentation 24 | 25 | Browse the documentation online at https://docs.zendframework.com/zend-json/ 26 | 27 | ## Support 28 | 29 | * [Issues](https://github.com/zendframework/zend-json/issues/) 30 | * [Chat](https://zendframework-slack.herokuapp.com/) 31 | * [Forum](https://discourse.zendframework.com/) 32 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-json", 3 | "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zf", 7 | "zendframework", 8 | "json" 9 | ], 10 | "support": { 11 | "docs": "https://docs.zendframework.com/zend-json/", 12 | "issues": "https://github.com/zendframework/zend-json/issues", 13 | "source": "https://github.com/zendframework/zend-json", 14 | "rss": "https://github.com/zendframework/zend-json/releases.atom", 15 | "chat": "https://zendframework-slack.herokuapp.com", 16 | "forum": "https://discourse.zendframework.com/c/questions/components" 17 | }, 18 | "require": { 19 | "php": "^5.6 || ^7.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^5.7.23 || ^6.4.3", 23 | "zendframework/zend-coding-standard": "~1.0.0", 24 | "zendframework/zend-stdlib": "^2.7.7 || ^3.1" 25 | }, 26 | "suggest": { 27 | "zendframework/zend-json-server": "For implementing JSON-RPC servers", 28 | "zendframework/zend-xml2json": "For converting XML documents to JSON" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Zend\\Json\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "ZendTest\\Json\\": "test/" 38 | } 39 | }, 40 | "config": { 41 | "sort-packages": true 42 | }, 43 | "extra": { 44 | "branch-alias": { 45 | "dev-master": "3.1.x-dev", 46 | "dev-develop": "3.2.x-dev" 47 | } 48 | }, 49 | "scripts": { 50 | "check": [ 51 | "@cs-check", 52 | "@test" 53 | ], 54 | "cs-check": "phpcs", 55 | "cs-fix": "phpcbf", 56 | "test": "phpunit --colors=always", 57 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Decoder.php: -------------------------------------------------------------------------------- 1 | = 0x20) && ($ordChrsC <= 0x7F): 110 | $utf8 .= $chrs[$i]; 111 | break; 112 | case ($ordChrsC & 0xE0) == 0xC0: 113 | // characters U-00000080 - U-000007FF, mask 110XXXXX 114 | //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 115 | $utf8 .= substr($chrs, $i, 2); 116 | ++$i; 117 | break; 118 | case ($ordChrsC & 0xF0) == 0xE0: 119 | // characters U-00000800 - U-0000FFFF, mask 1110XXXX 120 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 121 | $utf8 .= substr($chrs, $i, 3); 122 | $i += 2; 123 | break; 124 | case ($ordChrsC & 0xF8) == 0xF0: 125 | // characters U-00010000 - U-001FFFFF, mask 11110XXX 126 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 127 | $utf8 .= substr($chrs, $i, 4); 128 | $i += 3; 129 | break; 130 | case ($ordChrsC & 0xFC) == 0xF8: 131 | // characters U-00200000 - U-03FFFFFF, mask 111110XX 132 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 133 | $utf8 .= substr($chrs, $i, 5); 134 | $i += 4; 135 | break; 136 | case ($ordChrsC & 0xFE) == 0xFC: 137 | // characters U-04000000 - U-7FFFFFFF, mask 1111110X 138 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 139 | $utf8 .= substr($chrs, $i, 6); 140 | $i += 5; 141 | break; 142 | } 143 | } 144 | 145 | return $utf8; 146 | } 147 | 148 | /** 149 | * Constructor 150 | * 151 | * @param string $source String source to decode 152 | * @param int $decodeType How objects should be decoded -- see 153 | * {@link Json::TYPE_ARRAY} and {@link Json::TYPE_OBJECT} for * valid 154 | * values 155 | * @throws InvalidArgumentException 156 | */ 157 | protected function __construct($source, $decodeType) 158 | { 159 | // Set defaults 160 | $this->source = self::decodeUnicodeString($source); 161 | $this->sourceLength = strlen($this->source); 162 | $this->token = self::EOF; 163 | $this->offset = 0; 164 | 165 | switch ($decodeType) { 166 | case Json::TYPE_ARRAY: 167 | case Json::TYPE_OBJECT: 168 | $this->decodeType = $decodeType; 169 | break; 170 | default: 171 | throw new InvalidArgumentException(sprintf( 172 | 'Unknown decode type "%s", please use one of the Json::TYPE_* constants', 173 | $decodeType 174 | )); 175 | } 176 | 177 | // Set pointer at first token 178 | $this->getNextToken(); 179 | } 180 | 181 | /** 182 | * Decode a JSON source string. 183 | * 184 | * Decodes a JSON encoded string; the value returned will be one of the 185 | * following: 186 | * 187 | * - integer 188 | * - float 189 | * - boolean 190 | * - null 191 | * - stdClass 192 | * - array 193 | * - array of one or more of the above types 194 | * 195 | * By default, decoded objects will be returned as a stdClass object; 196 | * to return associative arrays instead, pass {@link Json::TYPE_ARRAY} 197 | * to the $objectDecodeType parameter. 198 | * 199 | * @param string $source String to be decoded. 200 | * @param int $objectDecodeType How objects should be decoded; should be 201 | * either or {@link Json::TYPE_ARRAY} or {@link Json::TYPE_OBJECT}; 202 | * defaults to Json::TYPE_OBJECT. 203 | * @return mixed 204 | */ 205 | public static function decode($source, $objectDecodeType = Json::TYPE_OBJECT) 206 | { 207 | $decoder = new static($source, $objectDecodeType); 208 | return $decoder->decodeValue(); 209 | } 210 | 211 | /** 212 | * Recursive routine for supported toplevel types. 213 | * 214 | * @return mixed 215 | */ 216 | protected function decodeValue() 217 | { 218 | switch ($this->token) { 219 | case self::DATUM: 220 | $result = $this->tokenValue; 221 | $this->getNextToken(); 222 | return($result); 223 | case self::LBRACE: 224 | return($this->decodeObject()); 225 | case self::LBRACKET: 226 | return($this->decodeArray()); 227 | default: 228 | return; 229 | } 230 | } 231 | 232 | /** 233 | * Decodes an object of the form { "attribute: value, "attribute2" : value, ... } 234 | * 235 | * If Zend\Json\Encoder was used to encode the original object, then 236 | * a special attribute called __className will specify a class 237 | * name with which to wrap the data contained within the encoded source. 238 | * 239 | * Decodes to either an array or stdClass object, based on the value of 240 | * {@link $decodeType}. If invalid $decodeType present, returns as an 241 | * array. 242 | * 243 | * @return array|stdClass 244 | * @throws RuntimeException 245 | */ 246 | protected function decodeObject() 247 | { 248 | $members = []; 249 | $tok = $this->getNextToken(); 250 | 251 | while ($tok && $tok !== self::RBRACE) { 252 | if ($tok !== self::DATUM || ! is_string($this->tokenValue)) { 253 | throw new RuntimeException(sprintf('Missing key in object encoding: %s', $this->source)); 254 | } 255 | 256 | $key = $this->tokenValue; 257 | $tok = $this->getNextToken(); 258 | 259 | if ($tok !== self::COLON) { 260 | throw new RuntimeException(sprintf('Missing ":" in object encoding: %s', $this->source)); 261 | } 262 | 263 | $this->getNextToken(); 264 | $members[$key] = $this->decodeValue(); 265 | $tok = $this->token; 266 | 267 | if ($tok === self::RBRACE) { 268 | break; 269 | } 270 | 271 | if ($tok !== self::COMMA) { 272 | throw new RuntimeException(sprintf('Missing "," in object encoding: %s', $this->source)); 273 | } 274 | 275 | $tok = $this->getNextToken(); 276 | } 277 | 278 | switch ($this->decodeType) { 279 | case Json::TYPE_OBJECT: 280 | // Create new stdClass and populate with $members 281 | $result = new stdClass(); 282 | foreach ($members as $key => $value) { 283 | if ($key === '') { 284 | $key = '_empty_'; 285 | } 286 | $result->$key = $value; 287 | } 288 | break; 289 | case Json::TYPE_ARRAY: 290 | // intentionally fall-through 291 | default: 292 | $result = $members; 293 | break; 294 | } 295 | 296 | $this->getNextToken(); 297 | return $result; 298 | } 299 | 300 | /** 301 | * Decodes the JSON array format [element, element2, ..., elementN] 302 | * 303 | * @return array 304 | * @throws RuntimeException 305 | */ 306 | protected function decodeArray() 307 | { 308 | $result = []; 309 | $tok = $this->getNextToken(); // Move past the '[' 310 | $index = 0; 311 | 312 | while ($tok && $tok !== self::RBRACKET) { 313 | $result[$index++] = $this->decodeValue(); 314 | 315 | $tok = $this->token; 316 | 317 | if ($tok == self::RBRACKET || ! $tok) { 318 | break; 319 | } 320 | 321 | if ($tok !== self::COMMA) { 322 | throw new RuntimeException(sprintf('Missing "," in array encoding: %s', $this->source)); 323 | } 324 | 325 | $tok = $this->getNextToken(); 326 | } 327 | 328 | $this->getNextToken(); 329 | return $result; 330 | } 331 | 332 | /** 333 | * Removes whitespace characters from the source input. 334 | */ 335 | protected function eatWhitespace() 336 | { 337 | if (preg_match('/([\t\b\f\n\r ])*/s', $this->source, $matches, PREG_OFFSET_CAPTURE, $this->offset) 338 | && $matches[0][1] == $this->offset 339 | ) { 340 | $this->offset += strlen($matches[0][0]); 341 | } 342 | } 343 | 344 | /** 345 | * Retrieves the next token from the source stream. 346 | * 347 | * @return int Token constant value specified in class definition. 348 | * @throws RuntimeException 349 | */ 350 | protected function getNextToken() 351 | { 352 | $this->token = self::EOF; 353 | $this->tokenValue = null; 354 | $this->eatWhitespace(); 355 | 356 | if ($this->offset >= $this->sourceLength) { 357 | return(self::EOF); 358 | } 359 | 360 | $str = $this->source; 361 | $strLength = $this->sourceLength; 362 | $i = $this->offset; 363 | $start = $i; 364 | 365 | switch ($str[$i]) { 366 | case '{': 367 | $this->token = self::LBRACE; 368 | break; 369 | case '}': 370 | $this->token = self::RBRACE; 371 | break; 372 | case '[': 373 | $this->token = self::LBRACKET; 374 | break; 375 | case ']': 376 | $this->token = self::RBRACKET; 377 | break; 378 | case ',': 379 | $this->token = self::COMMA; 380 | break; 381 | case ':': 382 | $this->token = self::COLON; 383 | break; 384 | case '"': 385 | $result = ''; 386 | do { 387 | $i++; 388 | if ($i >= $strLength) { 389 | break; 390 | } 391 | 392 | $chr = $str[$i]; 393 | 394 | if ($chr === '"') { 395 | break; 396 | } 397 | 398 | if ($chr !== '\\') { 399 | $result .= $chr; 400 | continue; 401 | } 402 | 403 | $i++; 404 | 405 | if ($i >= $strLength) { 406 | break; 407 | } 408 | 409 | $chr = $str[$i]; 410 | switch ($chr) { 411 | case '"': 412 | $result .= '"'; 413 | break; 414 | case '\\': 415 | $result .= '\\'; 416 | break; 417 | case '/': 418 | $result .= '/'; 419 | break; 420 | case 'b': 421 | $result .= "\x08"; 422 | break; 423 | case 'f': 424 | $result .= "\x0c"; 425 | break; 426 | case 'n': 427 | $result .= "\x0a"; 428 | break; 429 | case 'r': 430 | $result .= "\x0d"; 431 | break; 432 | case 't': 433 | $result .= "\x09"; 434 | break; 435 | case '\'': 436 | $result .= '\''; 437 | break; 438 | default: 439 | throw new RuntimeException(sprintf('Illegal escape sequence "%s"', $chr)); 440 | } 441 | } while ($i < $strLength); 442 | 443 | $this->token = self::DATUM; 444 | $this->tokenValue = $result; 445 | break; 446 | case 't': 447 | if (($i + 3) < $strLength && $start === strpos($str, "true", $start)) { 448 | $this->token = self::DATUM; 449 | } 450 | $this->tokenValue = true; 451 | $i += 3; 452 | break; 453 | case 'f': 454 | if (($i + 4) < $strLength && $start === strpos($str, "false", $start)) { 455 | $this->token = self::DATUM; 456 | } 457 | $this->tokenValue = false; 458 | $i += 4; 459 | break; 460 | case 'n': 461 | if (($i + 3) < $strLength && $start === strpos($str, "null", $start)) { 462 | $this->token = self::DATUM; 463 | } 464 | $this->tokenValue = null; 465 | $i += 3; 466 | break; 467 | } 468 | 469 | if ($this->token !== self::EOF) { 470 | $this->offset = $i + 1; // Consume the last token character 471 | return ($this->token); 472 | } 473 | 474 | $chr = $str[$i]; 475 | 476 | if ($chr !== '-' && $chr !== '.' && ($chr < '0' || $chr > '9')) { 477 | throw new RuntimeException('Illegal Token'); 478 | } 479 | 480 | if (preg_match('/-?([0-9])*(\.[0-9]*)?((e|E)((-|\+)?)[0-9]+)?/s', $str, $matches, PREG_OFFSET_CAPTURE, $start) 481 | && $matches[0][1] == $start 482 | ) { 483 | $datum = $matches[0][0]; 484 | 485 | if (! is_numeric($datum)) { 486 | throw new RuntimeException(sprintf('Illegal number format: %s', $datum)); 487 | } 488 | 489 | if (preg_match('/^0\d+$/', $datum)) { 490 | throw new RuntimeException(sprintf('Octal notation not supported by JSON (value: %o)', $datum)); 491 | } 492 | 493 | $val = intval($datum); 494 | $fVal = floatval($datum); 495 | $this->tokenValue = ($val == $fVal ? $val : $fVal); 496 | 497 | $this->token = self::DATUM; 498 | $this->offset = $start + strlen($datum); 499 | } 500 | 501 | return $this->token; 502 | } 503 | 504 | /** 505 | * Convert a string from one UTF-16 char to one UTF-8 char. 506 | * 507 | * Normally should be handled by mb_convert_encoding, but provides a slower 508 | * PHP-only method for installations that lack the multibyte string 509 | * extension. 510 | * 511 | * This method is from the Solar Framework by Paul M. Jones. 512 | * 513 | * @link http://solarphp.com 514 | * @param string $utf16 UTF-16 character 515 | * @return string UTF-8 character 516 | */ 517 | protected static function utf162utf8($utf16) 518 | { 519 | // Check for mb extension otherwise do by hand. 520 | if (function_exists('mb_convert_encoding')) { 521 | return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); 522 | } 523 | 524 | $bytes = (ord($utf16[0]) << 8) | ord($utf16[1]); 525 | 526 | switch (true) { 527 | case ((0x7F & $bytes) == $bytes): 528 | // This case should never be reached, because we are in ASCII range; 529 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 530 | return chr(0x7F & $bytes); 531 | 532 | case (0x07FF & $bytes) == $bytes: 533 | // Return a 2-byte UTF-8 character; 534 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 535 | return chr(0xC0 | (($bytes >> 6) & 0x1F)) 536 | . chr(0x80 | ($bytes & 0x3F)); 537 | 538 | case (0xFFFF & $bytes) == $bytes: 539 | // Return a 3-byte UTF-8 character; 540 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 541 | return chr(0xE0 | (($bytes >> 12) & 0x0F)) 542 | . chr(0x80 | (($bytes >> 6) & 0x3F)) 543 | . chr(0x80 | ($bytes & 0x3F)); 544 | } 545 | 546 | // ignoring UTF-32 for now, sorry 547 | return ''; 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /src/Encoder.php: -------------------------------------------------------------------------------- 1 | cycleCheck = $cycleCheck; 50 | $this->options = $options; 51 | } 52 | 53 | /** 54 | * Use the JSON encoding scheme for the value specified. 55 | * 56 | * @param mixed $value The value to be encoded. 57 | * @param bool $cycleCheck Whether or not to check for possible object recursion when encoding. 58 | * @param array $options Additional options used during encoding. 59 | * @return string The encoded value. 60 | */ 61 | public static function encode($value, $cycleCheck = false, array $options = []) 62 | { 63 | $encoder = new static($cycleCheck, $options); 64 | 65 | if ($value instanceof JsonSerializable) { 66 | $value = $value->jsonSerialize(); 67 | } 68 | 69 | return $encoder->encodeValue($value); 70 | } 71 | 72 | /** 73 | * Encode a value to JSON. 74 | * 75 | * Recursive method which determines the type of value to be encoded 76 | * and then dispatches to the appropriate method. 77 | * 78 | * $values are either 79 | * - objects (returns from {@link encodeObject()}) 80 | * - arrays (returns from {@link encodeArray()}) 81 | * - scalars (returns from {@link encodeDatum()}) 82 | * 83 | * @param $value mixed The value to be encoded. 84 | * @return string Encoded value. 85 | */ 86 | protected function encodeValue(&$value) 87 | { 88 | if (is_object($value)) { 89 | return $this->encodeObject($value); 90 | } 91 | 92 | if (is_array($value)) { 93 | return $this->encodeArray($value); 94 | } 95 | 96 | return $this->encodeDatum($value); 97 | } 98 | 99 | /** 100 | * Encode an object to JSON by encoding each of the public properties. 101 | * 102 | * A special property is added to the JSON object called '__className' that 103 | * contains the classname of $value; this can be used by consumers of the 104 | * resulting JSON to cast to the specific class. 105 | * 106 | * @param $value object 107 | * @return string 108 | * @throws RecursionException If recursive checks are enabled and the 109 | * object has been serialized previously. 110 | */ 111 | protected function encodeObject(&$value) 112 | { 113 | if ($this->cycleCheck) { 114 | if ($this->wasVisited($value)) { 115 | if (! isset($this->options['silenceCyclicalExceptions']) 116 | || $this->options['silenceCyclicalExceptions'] !== true 117 | ) { 118 | throw new RecursionException(sprintf( 119 | 'Cycles not supported in JSON encoding; cycle introduced by class "%s"', 120 | get_class($value) 121 | )); 122 | } 123 | 124 | return '"* RECURSION (' . str_replace('\\', '\\\\', get_class($value)) . ') *"'; 125 | } 126 | 127 | $this->visited[] = $value; 128 | } 129 | 130 | $props = ''; 131 | 132 | if (method_exists($value, 'toJson')) { 133 | $props = ',' . preg_replace("/^\{(.*)\}$/", "\\1", $value->toJson()); 134 | } else { 135 | if ($value instanceof IteratorAggregate) { 136 | $propCollection = $value->getIterator(); 137 | } elseif ($value instanceof Iterator) { 138 | $propCollection = $value; 139 | } else { 140 | $propCollection = get_object_vars($value); 141 | } 142 | 143 | foreach ($propCollection as $name => $propValue) { 144 | if (! isset($propValue)) { 145 | continue; 146 | } 147 | 148 | $props .= ',' 149 | . $this->encodeValue($name) 150 | . ':' 151 | . $this->encodeValue($propValue); 152 | } 153 | } 154 | 155 | $className = get_class($value); 156 | return '{"__className":' 157 | . $this->encodeString($className) 158 | . $props . '}'; 159 | } 160 | 161 | /** 162 | * Determine if an object has been serialized already. 163 | * 164 | * @param mixed $value 165 | * @return bool 166 | */ 167 | protected function wasVisited(&$value) 168 | { 169 | if (in_array($value, $this->visited, true)) { 170 | return true; 171 | } 172 | 173 | return false; 174 | } 175 | 176 | /** 177 | * JSON encode an array value. 178 | * 179 | * Recursively encodes each value of an array and returns a JSON encoded 180 | * array string. 181 | * 182 | * Arrays are defined as integer-indexed arrays starting at index 0, where 183 | * the last index is (count($array) -1); any deviation from that is 184 | * considered an associative array, and will be passed to 185 | * {@link encodeAssociativeArray()}. 186 | * 187 | * @param $array array 188 | * @return string 189 | */ 190 | protected function encodeArray($array) 191 | { 192 | // Check for associative array 193 | if (! empty($array) && (array_keys($array) !== range(0, count($array) - 1))) { 194 | // Associative array 195 | return $this->encodeAssociativeArray($array); 196 | } 197 | 198 | // Indexed array 199 | $tmpArray = []; 200 | $result = '['; 201 | $length = count($array); 202 | 203 | for ($i = 0; $i < $length; $i++) { 204 | $tmpArray[] = $this->encodeValue($array[$i]); 205 | } 206 | 207 | $result .= implode(',', $tmpArray); 208 | $result .= ']'; 209 | 210 | return $result; 211 | } 212 | 213 | /** 214 | * Encode an associative array to JSON. 215 | * 216 | * JSON does not have a concept of associative arrays; as such, we encode 217 | * them to objects. 218 | * 219 | * @param array $array Array to encode. 220 | * @return string 221 | */ 222 | protected function encodeAssociativeArray($array) 223 | { 224 | $tmpArray = []; 225 | $result = '{'; 226 | 227 | foreach ($array as $key => $value) { 228 | $tmpArray[] = sprintf( 229 | '%s:%s', 230 | $this->encodeString((string) $key), 231 | $this->encodeValue($value) 232 | ); 233 | } 234 | 235 | $result .= implode(',', $tmpArray); 236 | $result .= '}'; 237 | return $result; 238 | } 239 | 240 | /** 241 | * JSON encode a scalar data type (string, number, boolean, null). 242 | * 243 | * If value type is not a string, number, boolean, or null, the string 244 | * 'null' is returned. 245 | * 246 | * @param mixed $value 247 | * @return string 248 | */ 249 | protected function encodeDatum($value) 250 | { 251 | if (is_int($value) || is_float($value)) { 252 | return str_replace(',', '.', (string) $value); 253 | } 254 | 255 | if (is_string($value)) { 256 | return $this->encodeString($value); 257 | } 258 | 259 | if (is_bool($value)) { 260 | return $value ? 'true' : 'false'; 261 | } 262 | 263 | return 'null'; 264 | } 265 | 266 | /** 267 | * JSON encode a string value by escaping characters as necessary. 268 | * 269 | * @param string $string 270 | * @return string 271 | */ 272 | protected function encodeString($string) 273 | { 274 | // @codingStandardsIgnoreStart 275 | // Escape these characters with a backslash or unicode escape: 276 | // " \ / \n \r \t \b \f 277 | $search = ['\\', "\n", "\t", "\r", "\b", "\f", '"', '\'', '&', '<', '>', '/']; 278 | $replace = ['\\\\', '\\n', '\\t', '\\r', '\\b', '\\f', '\\u0022', '\\u0027', '\\u0026', '\\u003C', '\\u003E', '\\/']; 279 | $string = str_replace($search, $replace, $string); 280 | // @codingStandardsIgnoreEnd 281 | 282 | // Escape certain ASCII characters: 283 | // 0x08 => \b 284 | // 0x0c => \f 285 | $string = str_replace([chr(0x08), chr(0x0C)], ['\b', '\f'], $string); 286 | $string = self::encodeUnicodeString($string); 287 | 288 | return '"' . $string . '"'; 289 | } 290 | 291 | /** 292 | * Encode the constants associated with the ReflectionClass parameter. 293 | * 294 | * The encoding format is based on the class2 format. 295 | * 296 | * @param ReflectionClass $class 297 | * @return string Encoded constant block in class2 format 298 | */ 299 | private static function encodeConstants(ReflectionClass $class) 300 | { 301 | $result = "constants:{"; 302 | $constants = $class->getConstants(); 303 | 304 | if (empty($constants)) { 305 | return $result . '}'; 306 | } 307 | 308 | $tmpArray = []; 309 | foreach ($constants as $key => $value) { 310 | $tmpArray[] = sprintf('%s: %s', $key, self::encode($value)); 311 | } 312 | 313 | $result .= implode(', ', $tmpArray); 314 | 315 | return $result . "}"; 316 | } 317 | 318 | /** 319 | * Encode the public methods of the ReflectionClass in the class2 format 320 | * 321 | * @param ReflectionClass $class 322 | * @return string Encoded method fragment. 323 | */ 324 | private static function encodeMethods(ReflectionClass $class) 325 | { 326 | $result = 'methods:{'; 327 | $started = false; 328 | 329 | foreach ($class->getMethods() as $method) { 330 | if (! $method->isPublic() || ! $method->isUserDefined()) { 331 | continue; 332 | } 333 | 334 | if ($started) { 335 | $result .= ','; 336 | } 337 | $started = true; 338 | 339 | $result .= sprintf('%s:function(', $method->getName()); 340 | 341 | if ('__construct' === $method->getName()) { 342 | $result .= '){}'; 343 | continue; 344 | } 345 | 346 | $argsStarted = false; 347 | $argNames = "var argNames=["; 348 | 349 | foreach ($method->getParameters() as $param) { 350 | if ($argsStarted) { 351 | $result .= ','; 352 | } 353 | 354 | $result .= $param->getName(); 355 | 356 | if ($argsStarted) { 357 | $argNames .= ','; 358 | } 359 | 360 | $argNames .= sprintf('"%s"', $param->getName()); 361 | $argsStarted = true; 362 | } 363 | $argNames .= "];"; 364 | 365 | $result .= "){" 366 | . $argNames 367 | . 'var result = ZAjaxEngine.invokeRemoteMethod(' 368 | . "this, '" 369 | . $method->getName() 370 | . "',argNames,arguments);" 371 | . 'return(result);}'; 372 | } 373 | 374 | return $result . "}"; 375 | } 376 | 377 | /** 378 | * Encode the public properties of the ReflectionClass in the class2 format. 379 | * 380 | * @param ReflectionClass $class 381 | * @return string Encode properties list 382 | * 383 | */ 384 | private static function encodeVariables(ReflectionClass $class) 385 | { 386 | $propValues = get_class_vars($class->getName()); 387 | $result = "variables:{"; 388 | $tmpArray = []; 389 | 390 | foreach ($class->getProperties() as $prop) { 391 | if (! $prop->isPublic()) { 392 | continue; 393 | } 394 | 395 | $name = $prop->getName(); 396 | $tmpArray[] = sprintf('%s:%s', $name, self::encode($propValues[$name])); 397 | } 398 | 399 | $result .= implode(',', $tmpArray); 400 | 401 | return $result . "}"; 402 | } 403 | 404 | /** 405 | * Encodes the given $className into the class2 model of encoding PHP classes into JavaScript class2 classes. 406 | * 407 | * NOTE: Currently only public methods and variables are proxied onto the 408 | * client machine 409 | * 410 | * @param $className string The name of the class, the class must be 411 | * instantiable using a null constructor. 412 | * @param $package string Optional package name appended to JavaScript 413 | * proxy class name. 414 | * @return string The class2 (JavaScript) encoding of the class. 415 | * @throws InvalidArgumentException 416 | */ 417 | public static function encodeClass($className, $package = '') 418 | { 419 | $class = new ReflectionClass($className); 420 | if (! $class->isInstantiable()) { 421 | throw new InvalidArgumentException(sprintf( 422 | '"%s" must be instantiable', 423 | $className 424 | )); 425 | } 426 | 427 | return sprintf( 428 | 'Class.create(\'%s%s\',{%s,%s,%s});', 429 | $package, 430 | $className, 431 | self::encodeConstants($class), 432 | self::encodeMethods($class), 433 | self::encodeVariables($class) 434 | ); 435 | } 436 | 437 | /** 438 | * Encode several classes at once. 439 | * 440 | * Returns JSON encoded classes, using {@link encodeClass()}. 441 | * 442 | * @param string[] $classNames 443 | * @param string $package 444 | * @return string 445 | */ 446 | public static function encodeClasses(array $classNames, $package = '') 447 | { 448 | $result = ''; 449 | foreach ($classNames as $className) { 450 | $result .= static::encodeClass($className, $package); 451 | } 452 | 453 | return $result; 454 | } 455 | 456 | /** 457 | * Encode Unicode Characters to \u0000 ASCII syntax. 458 | * 459 | * This algorithm was originally developed for the Solar Framework by Paul 460 | * M. Jones. 461 | * 462 | * @link http://solarphp.com/ 463 | * @link https://github.com/solarphp/core/blob/master/Solar/Json.php 464 | * @param string $value 465 | * @return string 466 | */ 467 | public static function encodeUnicodeString($value) 468 | { 469 | $strlenVar = strlen($value); 470 | $ascii = ""; 471 | 472 | // Iterate over every character in the string, escaping with a slash or 473 | // encoding to UTF-8 where necessary. 474 | for ($i = 0; $i < $strlenVar; $i++) { 475 | $ordVarC = ord($value[$i]); 476 | 477 | switch (true) { 478 | case (($ordVarC >= 0x20) && ($ordVarC <= 0x7F)): 479 | // characters U-00000000 - U-0000007F (same as ASCII) 480 | $ascii .= $value[$i]; 481 | break; 482 | 483 | case (($ordVarC & 0xE0) == 0xC0): 484 | // characters U-00000080 - U-000007FF, mask 110XXXXX 485 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 486 | $char = pack('C*', $ordVarC, ord($value[$i + 1])); 487 | $i += 1; 488 | $utf16 = self::utf82utf16($char); 489 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 490 | break; 491 | 492 | case (($ordVarC & 0xF0) == 0xE0): 493 | // characters U-00000800 - U-0000FFFF, mask 1110XXXX 494 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 495 | $char = pack( 496 | 'C*', 497 | $ordVarC, 498 | ord($value[$i + 1]), 499 | ord($value[$i + 2]) 500 | ); 501 | $i += 2; 502 | $utf16 = self::utf82utf16($char); 503 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 504 | break; 505 | 506 | case (($ordVarC & 0xF8) == 0xF0): 507 | // characters U-00010000 - U-001FFFFF, mask 11110XXX 508 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 509 | $char = pack( 510 | 'C*', 511 | $ordVarC, 512 | ord($value[$i + 1]), 513 | ord($value[$i + 2]), 514 | ord($value[$i + 3]) 515 | ); 516 | $i += 3; 517 | $utf16 = self::utf82utf16($char); 518 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 519 | break; 520 | 521 | case (($ordVarC & 0xFC) == 0xF8): 522 | // characters U-00200000 - U-03FFFFFF, mask 111110XX 523 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 524 | $char = pack( 525 | 'C*', 526 | $ordVarC, 527 | ord($value[$i + 1]), 528 | ord($value[$i + 2]), 529 | ord($value[$i + 3]), 530 | ord($value[$i + 4]) 531 | ); 532 | $i += 4; 533 | $utf16 = self::utf82utf16($char); 534 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 535 | break; 536 | 537 | case (($ordVarC & 0xFE) == 0xFC): 538 | // characters U-04000000 - U-7FFFFFFF, mask 1111110X 539 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 540 | $char = pack( 541 | 'C*', 542 | $ordVarC, 543 | ord($value[$i + 1]), 544 | ord($value[$i + 2]), 545 | ord($value[$i + 3]), 546 | ord($value[$i + 4]), 547 | ord($value[$i + 5]) 548 | ); 549 | $i += 5; 550 | $utf16 = self::utf82utf16($char); 551 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 552 | break; 553 | } 554 | } 555 | 556 | return $ascii; 557 | } 558 | 559 | /** 560 | * Convert a string from one UTF-8 char to one UTF-16 char. 561 | * 562 | * Normally should be handled by mb_convert_encoding, but provides a slower 563 | * PHP-only method for installations that lack the multibyte string 564 | * extension. 565 | * 566 | * This method is from the Solar Framework by Paul M. Jones. 567 | * 568 | * @link http://solarphp.com 569 | * @param string $utf8 UTF-8 character 570 | * @return string UTF-16 character 571 | */ 572 | protected static function utf82utf16($utf8) 573 | { 574 | // Check for mb extension otherwise do by hand. 575 | if (function_exists('mb_convert_encoding')) { 576 | return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); 577 | } 578 | 579 | switch (strlen($utf8)) { 580 | case 1: 581 | // This case should never be reached, because we are in ASCII range; 582 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 583 | return $utf8; 584 | 585 | case 2: 586 | // Return a UTF-16 character from a 2-byte UTF-8 char; 587 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 588 | return chr(0x07 & (ord($utf8[0]) >> 2)) . chr((0xC0 & (ord($utf8[0]) << 6)) | (0x3F & ord($utf8[1]))); 589 | 590 | case 3: 591 | // Return a UTF-16 character from a 3-byte UTF-8 char; 592 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 593 | return chr((0xF0 & (ord($utf8[0]) << 4)) 594 | | (0x0F & (ord($utf8[1]) >> 2))) . chr((0xC0 & (ord($utf8[1]) << 6)) 595 | | (0x7F & ord($utf8[2]))); 596 | } 597 | 598 | // ignoring UTF-32 for now, sorry 599 | return ''; 600 | } 601 | } 602 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | 20 | * $foo = array( 21 | * 'integer' => 9, 22 | * 'string' => 'test string', 23 | * 'function' => Zend\Json\Expr( 24 | * 'function () { window.alert("javascript function encoded by Zend\Json\Json") }' 25 | * ), 26 | * ); 27 | * 28 | * echo Zend\Json\Json::encode($foo, false, ['enableJsonExprFinder' => true]); 29 | * 30 | * 31 | * The above returns the following JSON (formatted for readability): 32 | * 33 | * 34 | * { 35 | * "integer": 9, 36 | * "string": "test string", 37 | * "function": function () {window.alert("javascript function encoded by Zend\Json\Json")} 38 | * } 39 | * 40 | */ 41 | class Expr 42 | { 43 | /** 44 | * Storage for javascript expression. 45 | * 46 | * @var string 47 | */ 48 | protected $expression; 49 | 50 | /** 51 | * @param string $expression The expression to represent. 52 | */ 53 | public function __construct($expression) 54 | { 55 | $this->expression = (string) $expression; 56 | } 57 | 58 | /** 59 | * Cast to string 60 | * 61 | * @return string holded javascript expression. 62 | */ 63 | public function __toString() 64 | { 65 | return $this->expression; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Json.php: -------------------------------------------------------------------------------- 1 | toJson(); 80 | } 81 | 82 | if (method_exists($valueToEncode, 'toArray')) { 83 | return static::encode($valueToEncode->toArray(), $cycleCheck, $options); 84 | } 85 | } 86 | 87 | // Pre-process and replace javascript expressions with placeholders 88 | $javascriptExpressions = new SplQueue(); 89 | if (isset($options['enableJsonExprFinder']) 90 | && $options['enableJsonExprFinder'] == true 91 | ) { 92 | $valueToEncode = static::recursiveJsonExprFinder($valueToEncode, $javascriptExpressions); 93 | } 94 | 95 | // Encoding 96 | $prettyPrint = (isset($options['prettyPrint']) && ($options['prettyPrint'] === true)); 97 | $encodedResult = self::encodeValue($valueToEncode, $cycleCheck, $options, $prettyPrint); 98 | 99 | // Post-process to revert back any Zend\Json\Expr instances. 100 | $encodedResult = self::injectJavascriptExpressions($encodedResult, $javascriptExpressions); 101 | 102 | return $encodedResult; 103 | } 104 | 105 | /** 106 | * Discover and replace javascript expressions with temporary placeholders. 107 | * 108 | * Check each value to determine if it is a Zend\Json\Expr; if so, replace the value with 109 | * a magic key and add the javascript expression to the queue. 110 | * 111 | * NOTE this method is recursive. 112 | * 113 | * NOTE: This method is used internally by the encode method. 114 | * 115 | * @see encode 116 | * @param mixed $value a string - object property to be encoded 117 | * @param SplQueue $javascriptExpressions 118 | * @param null|string|int $currentKey 119 | * @return mixed 120 | */ 121 | protected static function recursiveJsonExprFinder( 122 | $value, 123 | SplQueue $javascriptExpressions, 124 | $currentKey = null 125 | ) { 126 | if ($value instanceof Expr) { 127 | // TODO: Optimize with ascii keys, if performance is bad 128 | $magicKey = "____" . $currentKey . "_" . (count($javascriptExpressions)); 129 | 130 | $javascriptExpressions->enqueue([ 131 | // If currentKey is integer, encodeUnicodeString call is not required. 132 | 'magicKey' => (is_int($currentKey)) ? $magicKey : Encoder::encodeUnicodeString($magicKey), 133 | 'value' => $value, 134 | ]); 135 | 136 | return $magicKey; 137 | } 138 | 139 | if (is_array($value)) { 140 | foreach ($value as $k => $v) { 141 | $value[$k] = static::recursiveJsonExprFinder($value[$k], $javascriptExpressions, $k); 142 | } 143 | return $value; 144 | } 145 | 146 | if (is_object($value)) { 147 | foreach ($value as $k => $v) { 148 | $value->$k = static::recursiveJsonExprFinder($value->$k, $javascriptExpressions, $k); 149 | } 150 | return $value; 151 | } 152 | 153 | return $value; 154 | } 155 | 156 | /** 157 | * Pretty-print JSON string 158 | * 159 | * Use 'indent' option to select indentation string; by default, four 160 | * spaces are used. 161 | * 162 | * @param string $json Original JSON string 163 | * @param array $options Encoding options 164 | * @return string 165 | */ 166 | public static function prettyPrint($json, array $options = []) 167 | { 168 | $indentString = isset($options['indent']) ? $options['indent'] : ' '; 169 | 170 | $json = trim($json); 171 | $length = strlen($json); 172 | $stack = []; 173 | 174 | $result = ''; 175 | $inLiteral = false; 176 | 177 | for ($i = 0; $i < $length; ++$i) { 178 | switch ($json[$i]) { 179 | case '{': 180 | case '[': 181 | if ($inLiteral) { 182 | break; 183 | } 184 | 185 | $stack[] = $json[$i]; 186 | 187 | $result .= $json[$i]; 188 | while (isset($json[$i + 1]) && preg_match('/\s/', $json[$i + 1])) { 189 | ++$i; 190 | } 191 | if (isset($json[$i + 1]) && $json[$i + 1] !== '}' && $json[$i + 1] !== ']') { 192 | $result .= "\n" . str_repeat($indentString, count($stack)); 193 | } 194 | 195 | continue 2; 196 | case '}': 197 | case ']': 198 | if ($inLiteral) { 199 | break; 200 | } 201 | 202 | $last = end($stack); 203 | if (($last === '{' && $json[$i] === '}') 204 | || ($last === '[' && $json[$i] === ']') 205 | ) { 206 | array_pop($stack); 207 | } 208 | 209 | $result .= $json[$i]; 210 | while (isset($json[$i + 1]) && preg_match('/\s/', $json[$i + 1])) { 211 | ++$i; 212 | } 213 | if (isset($json[$i + 1]) && ($json[$i + 1] === '}' || $json[$i + 1] === ']')) { 214 | $result .= "\n" . str_repeat($indentString, count($stack) - 1); 215 | } 216 | 217 | continue 2; 218 | case '"': 219 | $result .= '"'; 220 | 221 | if (! $inLiteral) { 222 | $inLiteral = true; 223 | } else { 224 | $backslashes = 0; 225 | $n = $i; 226 | while ($json[--$n] === '\\') { 227 | ++$backslashes; 228 | } 229 | 230 | if (($backslashes % 2) === 0) { 231 | $inLiteral = false; 232 | 233 | while (isset($json[$i + 1]) && preg_match('/\s/', $json[$i + 1])) { 234 | ++$i; 235 | } 236 | 237 | if (isset($json[$i + 1]) && ($json[$i + 1] === '}' || $json[$i + 1] === ']')) { 238 | $result .= "\n" . str_repeat($indentString, count($stack) - 1); 239 | } 240 | } 241 | } 242 | continue 2; 243 | case ':': 244 | if (! $inLiteral) { 245 | $result .= ': '; 246 | continue 2; 247 | } 248 | break; 249 | case ',': 250 | if (! $inLiteral) { 251 | $result .= ',' . "\n" . str_repeat($indentString, count($stack)); 252 | continue 2; 253 | } 254 | break; 255 | default: 256 | if (! $inLiteral && preg_match('/\s/', $json[$i])) { 257 | continue 2; 258 | } 259 | break; 260 | } 261 | 262 | $result .= $json[$i]; 263 | 264 | if ($inLiteral) { 265 | continue; 266 | } 267 | 268 | while (isset($json[$i + 1]) && preg_match('/\s/', $json[$i + 1])) { 269 | ++$i; 270 | } 271 | 272 | if (isset($json[$i + 1]) && ($json[$i + 1] === '}' || $json[$i + 1] === ']')) { 273 | $result .= "\n" . str_repeat($indentString, count($stack) - 1); 274 | } 275 | } 276 | 277 | return $result; 278 | } 279 | 280 | /** 281 | * Decode a value using the PHP built-in json_decode function. 282 | * 283 | * @param string $encodedValue 284 | * @param int $objectDecodeType 285 | * @return mixed 286 | * @throws RuntimeException 287 | */ 288 | private static function decodeViaPhpBuiltIn($encodedValue, $objectDecodeType) 289 | { 290 | $decoded = json_decode($encodedValue, (bool) $objectDecodeType); 291 | 292 | switch (json_last_error()) { 293 | case JSON_ERROR_NONE: 294 | return $decoded; 295 | case JSON_ERROR_DEPTH: 296 | throw new RuntimeException('Decoding failed: Maximum stack depth exceeded'); 297 | case JSON_ERROR_CTRL_CHAR: 298 | throw new RuntimeException('Decoding failed: Unexpected control character found'); 299 | case JSON_ERROR_SYNTAX: 300 | throw new RuntimeException('Decoding failed: Syntax error'); 301 | default: 302 | throw new RuntimeException('Decoding failed'); 303 | } 304 | } 305 | 306 | /** 307 | * Encode a value to JSON. 308 | * 309 | * Intermediary step between injecting JavaScript expressions. 310 | * 311 | * Delegates to either the PHP built-in json_encode operation, or the 312 | * Encoder component, based on availability of the built-in and/or whether 313 | * or not the component encoder is requested. 314 | * 315 | * @param mixed $valueToEncode 316 | * @param bool $cycleCheck 317 | * @param array $options 318 | * @param bool $prettyPrint 319 | * @return string 320 | */ 321 | private static function encodeValue($valueToEncode, $cycleCheck, array $options, $prettyPrint) 322 | { 323 | if (function_exists('json_encode') && static::$useBuiltinEncoderDecoder !== true) { 324 | return self::encodeViaPhpBuiltIn($valueToEncode, $prettyPrint); 325 | } 326 | 327 | return self::encodeViaEncoder($valueToEncode, $cycleCheck, $options, $prettyPrint); 328 | } 329 | 330 | /** 331 | * Encode a value to JSON using the PHP built-in json_encode function. 332 | * 333 | * Uses the encoding options: 334 | * 335 | * - JSON_HEX_TAG 336 | * - JSON_HEX_APOS 337 | * - JSON_HEX_QUOT 338 | * - JSON_HEX_AMP 339 | * 340 | * If $prettyPrint is boolean true, also uses JSON_PRETTY_PRINT. 341 | * 342 | * @param mixed $valueToEncode 343 | * @param bool $prettyPrint 344 | * @return string|false Boolean false return value if json_encode is not 345 | * available, or the $useBuiltinEncoderDecoder flag is enabled. 346 | */ 347 | private static function encodeViaPhpBuiltIn($valueToEncode, $prettyPrint = false) 348 | { 349 | if (! function_exists('json_encode') || static::$useBuiltinEncoderDecoder === true) { 350 | return false; 351 | } 352 | 353 | $encodeOptions = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP; 354 | 355 | if ($prettyPrint) { 356 | $encodeOptions |= JSON_PRETTY_PRINT; 357 | } 358 | 359 | return json_encode($valueToEncode, $encodeOptions); 360 | } 361 | 362 | /** 363 | * Encode a value to JSON using the Encoder class. 364 | * 365 | * Passes the value, cycle check flag, and options to Encoder::encode(). 366 | * 367 | * Once the result is returned, determines if pretty printing is required, 368 | * and, if so, returns the result of that operation, otherwise returning 369 | * the encoded value. 370 | * 371 | * @param mixed $valueToEncode 372 | * @param bool $cycleCheck 373 | * @param array $options 374 | * @param bool $prettyPrint 375 | * @return string 376 | */ 377 | private static function encodeViaEncoder($valueToEncode, $cycleCheck, array $options, $prettyPrint) 378 | { 379 | $encodedResult = Encoder::encode($valueToEncode, $cycleCheck, $options); 380 | 381 | if ($prettyPrint) { 382 | return self::prettyPrint($encodedResult, ['indent' => ' ']); 383 | } 384 | 385 | return $encodedResult; 386 | } 387 | 388 | /** 389 | * Inject javascript expressions into the encoded value. 390 | * 391 | * Loops through each, substituting the "magicKey" of each with its 392 | * associated value. 393 | * 394 | * @param string $encodedValue 395 | * @param SplQueue $javascriptExpressions 396 | * @return string 397 | */ 398 | private static function injectJavascriptExpressions($encodedValue, SplQueue $javascriptExpressions) 399 | { 400 | foreach ($javascriptExpressions as $expression) { 401 | $encodedValue = str_replace( 402 | sprintf('"%s"', $expression['magicKey']), 403 | $expression['value'], 404 | (string) $encodedValue 405 | ); 406 | } 407 | 408 | return $encodedValue; 409 | } 410 | } 411 | --------------------------------------------------------------------------------