├── examples ├── stream_simple.php ├── buffer_simple.php ├── buffer_foreach.php ├── buffer_generator.php ├── stream_file.php ├── json_stream.php └── benchmark.php ├── src ├── EncodingException.php ├── autoload.php ├── JsonToken.php ├── BufferJsonEncoder.php ├── StreamJsonEncoder.php ├── JsonStream.php └── AbstractJsonEncoder.php ├── LICENSE ├── composer.json ├── CHANGES.md ├── composer.lock └── README.md /examples/stream_simple.php: -------------------------------------------------------------------------------- 1 | encode(); 7 | -------------------------------------------------------------------------------- /examples/buffer_simple.php: -------------------------------------------------------------------------------- 1 | encode(); 7 | -------------------------------------------------------------------------------- /examples/buffer_foreach.php: -------------------------------------------------------------------------------- 1 | encode(); 14 | fclose($fp); 15 | -------------------------------------------------------------------------------- /src/EncodingException.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2016-2020 Riikka Kalliomäki 10 | * @license http://opensource.org/licenses/mit-license.php MIT License 11 | */ 12 | class EncodingException extends \Exception 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/autoload.php: -------------------------------------------------------------------------------- 1 | DIRECTORY_SEPARATOR]) . '.php'; 9 | 10 | if (file_exists($path)) { 11 | require $path; 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /examples/json_stream.php: -------------------------------------------------------------------------------- 1 | getFilename(); 8 | } 9 | }; 10 | 11 | $encoder = (new \Violet\StreamingJsonEncoder\BufferJsonEncoder($iterator)) 12 | ->setOptions(JSON_PRETTY_PRINT); 13 | 14 | $stream = new \Violet\StreamingJsonEncoder\JsonStream($encoder); 15 | 16 | while (!$stream->eof()) { 17 | echo $stream->read(1024 * 8); 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2022 Riikka Kalliomäki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/JsonToken.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2017-2020 Riikka Kalliomäki 10 | * @license http://opensource.org/licenses/mit-license.php MIT License 11 | */ 12 | final class JsonToken 13 | { 14 | /** Represents the [ character that begins an array */ 15 | const T_LEFT_BRACKET = 1; 16 | 17 | /** Represents the ] character the ends an array */ 18 | const T_RIGHT_BRACKET = 2; 19 | 20 | /** Represents the { character that begins an object */ 21 | const T_LEFT_BRACE = 3; 22 | 23 | /** Represents the } character that ends an object */ 24 | const T_RIGHT_BRACE = 4; 25 | 26 | /** Represents a name in an object name/value pair */ 27 | const T_NAME = 5; 28 | 29 | /** Represent the : character that separates a name and a value */ 30 | const T_COLON = 6; 31 | 32 | /** Represents all values */ 33 | const T_VALUE = 7; 34 | 35 | /** Represents the , character that separates array values and object name/value pairs */ 36 | const T_COMMA = 8; 37 | 38 | /** Represents all whitespace */ 39 | const T_WHITESPACE = 9; 40 | } 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "violet/streaming-json-encoder", 3 | "description": "Library for iteratively encoding large JSON documents piece by piece", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "streaming", 8 | "json", 9 | "encoder", 10 | "psr-7" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Riikka Kalliomäki", 15 | "email": "riikka.kalliomaki@gmail.com", 16 | "homepage": "http://riimu.net" 17 | } 18 | ], 19 | "homepage": "http://violet.riimu.net", 20 | "require": { 21 | "php": ">=5.6.0", 22 | "psr/http-message": "^1.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Violet\\StreamingJsonEncoder\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Violet\\StreamingJsonEncoder\\": "tests/tests/", 32 | "Violet\\StreamingJsonEncoder\\Test\\": "tests/classes/" 33 | } 34 | }, 35 | "scripts": { 36 | "ci-all": [ 37 | "composer test -- --do-not-cache-result", 38 | "composer phpcs -- --no-cache", 39 | "composer php-cs-fixer -- --dry-run --diff --using-cache=no", 40 | "composer normalize --dry-run" 41 | ], 42 | "php-cs-fixer": "php-cs-fixer fix -v", 43 | "phpcs": "phpcs -p", 44 | "test": "phpunit" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/benchmark.php: -------------------------------------------------------------------------------- 1 | true, 11 | 'integer' => 1879234, 12 | 'float' => 1.234900, 13 | 'string' => 'stringy thingy', 14 | 'array' => [ 15 | 'value 1', 16 | 'value 2', 17 | 'value 3', 18 | ], 19 | 'very_long' => $longString, 20 | ]; 21 | 22 | for ($i = 0; $i < 10000; $i++) { 23 | $payload[] = $node; 24 | } 25 | 26 | function benchmark(Closure $callback) 27 | { 28 | $timer = microtime(true); 29 | 30 | ob_start(function () { 31 | return ''; 32 | }, 1024 * 8); 33 | 34 | $bytes = $callback(); 35 | 36 | ob_end_flush(); 37 | printf( 38 | 'Output: %s kb, %d ms, Mem %d mb', 39 | number_format($bytes / 1024), 40 | (microtime(true) - $timer) * 1000, 41 | memory_get_peak_usage(true) / 1024 / 1024 42 | ); 43 | } 44 | 45 | echo 'Streaming: '; 46 | benchmark(function () use ($payload) { 47 | $encoder = new \Violet\StreamingJsonEncoder\StreamJsonEncoder($payload); 48 | $encoder->setOptions(JSON_PRETTY_PRINT); 49 | return $encoder->encode(); 50 | }); 51 | 52 | echo "\nDirect: "; 53 | benchmark(function () use ($payload) { 54 | $output = json_encode($payload, JSON_PRETTY_PRINT); 55 | echo $output; 56 | return strlen($output); 57 | }); 58 | -------------------------------------------------------------------------------- /src/BufferJsonEncoder.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2016-2020 Riikka Kalliomäki 10 | * @license http://opensource.org/licenses/mit-license.php MIT License 11 | */ 12 | class BufferJsonEncoder extends AbstractJsonEncoder 13 | { 14 | /** @var string The encoded JSON in the current step */ 15 | private $buffer; 16 | 17 | /** 18 | * Encodes the entire value as JSON and returns the value as a string. 19 | * @return string The encoded JSON 20 | */ 21 | public function encode() 22 | { 23 | $json = []; 24 | 25 | foreach ($this as $string) { 26 | $json[] = $string; 27 | } 28 | 29 | return implode($json); 30 | } 31 | 32 | /** {@inheritdoc} */ 33 | public function rewind() 34 | { 35 | $this->buffer = ''; 36 | 37 | parent::rewind(); 38 | } 39 | 40 | /** {@inheritdoc} */ 41 | public function next() 42 | { 43 | $this->buffer = ''; 44 | 45 | parent::next(); 46 | } 47 | 48 | /** 49 | * Returns the JSON encoded in the current step. 50 | * @return string|null The currently encoded JSON or null if the state is not valid 51 | */ 52 | public function current() 53 | { 54 | return $this->valid() ? $this->buffer : null; 55 | } 56 | 57 | /** 58 | * Writes the JSON output to the step buffer. 59 | * @param string $string The JSON string to write 60 | * @param int $token The type of the token 61 | */ 62 | protected function write($string, $token) 63 | { 64 | $this->buffer .= $string; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog # 2 | 3 | ## v1.1.5 (2022-12-09) ## 4 | 5 | * Add support for PHP 8.2 6 | 7 | ## v1.1.4 (2022-01-02) ## 8 | 9 | * Improve Github action workflow 10 | * Fix additional PHP 8.1 deprecations 11 | * Fix issue with `JsonStream::getContents()` no returning buffer contents 12 | 13 | ## v1.1.3 (2021-12-27) ## 14 | 15 | * Add support for PHP 8.1 16 | 17 | ## v1.1.2 (2020-11-29) ## 18 | 19 | * HHVM support has been dropped due to HHVM's renewed focus on Hack 20 | * CI build has been migrated to github actions 21 | * The CI build now tests for PHP version from 5.6 to 8.0 22 | 23 | ## v1.1.1 (2017-07-09) ## 24 | 25 | * Return `UNKNOWN_ERROR` as error code if valid error constant is not found 26 | * Minor improvements to the travis build process 27 | * Slightly improve the bundled autoloader 28 | 29 | ## v1.1.0 (2017-06-28) ## 30 | 31 | * For `json_encode()` compatibility, all objects are encoded as JSON objects 32 | unless they implement `Traversable` and either return `0` as the first key or 33 | return no values at all. 34 | * Testing for associative arrays is now more memory efficient and fails faster. 35 | * JSON encoding errors now contain the error constant. 36 | * The visibility for `StreamJsonEncoder::write()` and `BufferJsonEncoder::write()` 37 | has been changed to protected (as was originally intended). 38 | * A new protected method `AbstractJsonEncoder::getValueStack()` has been added 39 | that returns the current unresolved value stack (for special write method 40 | implementations). 41 | * An overridable protected method `AbstractJsonEncoder::resolveValue()` has 42 | been added which is used to resolve objects (i.e. resolving the value of 43 | closures and objects implementing `JsonSerializable`). 44 | 45 | ## v1.0.0 (2017-02-26) ## 46 | 47 | * Initial release 48 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "fbf93b9ed8b9f3514a19b0ae374cc577", 8 | "packages": [ 9 | { 10 | "name": "psr/http-message", 11 | "version": "1.0.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/php-fig/http-message.git", 15 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", 20 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.0" 25 | }, 26 | "type": "library", 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.0.x-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Psr\\Http\\Message\\": "src/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "PHP-FIG", 44 | "homepage": "http://www.php-fig.org/" 45 | } 46 | ], 47 | "description": "Common interface for HTTP messages", 48 | "homepage": "https://github.com/php-fig/http-message", 49 | "keywords": [ 50 | "http", 51 | "http-message", 52 | "psr", 53 | "psr-7", 54 | "request", 55 | "response" 56 | ], 57 | "support": { 58 | "source": "https://github.com/php-fig/http-message/tree/master" 59 | }, 60 | "time": "2016-08-06T14:39:51+00:00" 61 | } 62 | ], 63 | "packages-dev": [], 64 | "aliases": [], 65 | "minimum-stability": "stable", 66 | "stability-flags": [], 67 | "prefer-stable": false, 68 | "prefer-lowest": false, 69 | "platform": { 70 | "php": ">=5.6.0" 71 | }, 72 | "platform-dev": [], 73 | "plugin-api-version": "2.3.0" 74 | } 75 | -------------------------------------------------------------------------------- /src/StreamJsonEncoder.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2016-2020 Riikka Kalliomäki 10 | * @license http://opensource.org/licenses/mit-license.php MIT License 11 | */ 12 | class StreamJsonEncoder extends AbstractJsonEncoder 13 | { 14 | /** @var callable|null The stream callable */ 15 | private $stream; 16 | 17 | /** @var int Number of bytes written in the current step */ 18 | private $bytes; 19 | 20 | /** 21 | * StreamJsonEncoder constructor. 22 | * 23 | * If a callable is given as the second argument, the callable will be 24 | * called with two arguments. The first argument is the JSON string to 25 | * output and the second argument is the type of the token being outputted. 26 | * 27 | * If no second parameter is passed to the constructor, then the encoder 28 | * will simply output the json using an echo statement. 29 | * 30 | * @param mixed $value The value to encode as JSON 31 | * @param callable|null $stream An optional stream to pass the output or null to echo it 32 | */ 33 | public function __construct($value, callable $stream = null) 34 | { 35 | parent::__construct($value); 36 | 37 | $this->stream = $stream; 38 | } 39 | 40 | /** 41 | * Encodes the entire value into JSON and returns the number bytes. 42 | * @return int Returned the number of bytes outputted 43 | */ 44 | public function encode() 45 | { 46 | $total = 0; 47 | 48 | foreach ($this as $bytes) { 49 | $total += $bytes; 50 | } 51 | 52 | return $total; 53 | } 54 | 55 | /** {@inheritdoc} */ 56 | public function rewind() 57 | { 58 | $this->bytes = 0; 59 | 60 | parent::rewind(); 61 | } 62 | 63 | /** {@inheritdoc} */ 64 | public function next() 65 | { 66 | $this->bytes = 0; 67 | 68 | parent::next(); 69 | } 70 | 71 | /** 72 | * Returns the bytes written in the last step or null if the encoder is not in valid state. 73 | * @return int|null The number of bytes written or null when invalid 74 | */ 75 | public function current() 76 | { 77 | return $this->valid() ? $this->bytes : null; 78 | } 79 | 80 | /** 81 | * Echoes to given string or passes it to the stream callback. 82 | * @param string $string The string to output 83 | * @param int $token The type of the string 84 | */ 85 | protected function write($string, $token) 86 | { 87 | if ($this->stream === null) { 88 | echo $string; 89 | } else { 90 | $callback = $this->stream; 91 | $callback($string, $token); 92 | } 93 | 94 | $this->bytes += strlen($string); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/JsonStream.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright Copyright (c) 2016-2020 Riikka Kalliomäki 12 | * @license http://opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | class JsonStream implements StreamInterface 15 | { 16 | /** @var BufferJsonEncoder|null The encoder used to produce the JSON stream or null once closed */ 17 | private $encoder; 18 | 19 | /** @var int The current position of the cursor in the JSON stream */ 20 | private $cursor; 21 | 22 | /** @var string|null Buffered output from encoding the value as JSON or null when at EOF */ 23 | private $buffer; 24 | 25 | /** 26 | * JsonStream constructor. 27 | * @param BufferJsonEncoder|mixed $value A JSON encoder to use or a value to encode 28 | */ 29 | public function __construct($value) 30 | { 31 | if (!$value instanceof BufferJsonEncoder) { 32 | $value = (new BufferJsonEncoder($value)) 33 | ->setOptions(JSON_PARTIAL_OUTPUT_ON_ERROR) 34 | ->setIndent(0); 35 | } 36 | 37 | $this->encoder = $value; 38 | $this->rewind(); 39 | } 40 | 41 | /** 42 | * Returns the JSON encoder used for the JSON stream. 43 | * @return BufferJsonEncoder The currently used JSON encoder 44 | * @throws \RuntimeException If the stream has been closed 45 | */ 46 | private function getEncoder() 47 | { 48 | if (!$this->encoder instanceof BufferJsonEncoder) { 49 | throw new \RuntimeException('Cannot operate on a closed JSON stream'); 50 | } 51 | 52 | return $this->encoder; 53 | } 54 | 55 | /** 56 | * Returns the entire JSON stream as a string. 57 | * 58 | * Note that this operation performs rewind operation on the JSON encoder. Whether 59 | * this works or not is dependant on the underlying value being encoded. An empty 60 | * string is returned if the value cannot be encoded. 61 | * 62 | * @return string The entire JSON stream as a string 63 | */ 64 | public function __toString() 65 | { 66 | try { 67 | $this->rewind(); 68 | return $this->getContents(); 69 | } catch (\Exception $exception) { 70 | return ''; 71 | } 72 | } 73 | 74 | /** 75 | * Frees the JSON encoder from memory and prevents further reading from the JSON stream. 76 | */ 77 | public function close() 78 | { 79 | $this->encoder = null; 80 | } 81 | 82 | /** 83 | * Detaches the underlying PHP resource from the stream and returns it. 84 | * @return null Always returns null as no underlying PHP resource exists 85 | */ 86 | public function detach() 87 | { 88 | return null; 89 | } 90 | 91 | /** 92 | * Returns the total size of the JSON stream. 93 | * @return null Always returns null as the total size cannot be determined 94 | */ 95 | public function getSize() 96 | { 97 | return null; 98 | } 99 | 100 | /** 101 | * Returns the current position of the cursor in the JSON stream. 102 | * @return int Current position of the cursor 103 | */ 104 | public function tell() 105 | { 106 | $this->getEncoder(); 107 | return $this->cursor; 108 | } 109 | 110 | /** 111 | * Tells if there are no more bytes to read from the JSON stream. 112 | * @return bool True if there are no more bytes to read, false if there are 113 | */ 114 | public function eof() 115 | { 116 | return $this->buffer === null; 117 | } 118 | 119 | /** 120 | * Tells if the JSON stream is seekable or not. 121 | * @return bool Always returns true as JSON streams as always seekable 122 | */ 123 | public function isSeekable() 124 | { 125 | return true; 126 | } 127 | 128 | /** 129 | * Seeks the given cursor position in the JSON stream. 130 | * 131 | * If the provided seek position is less than the current cursor position, a rewind 132 | * operation is performed on the underlying JSON encoder. Whether this works or not 133 | * depends on whether the encoded value supports rewinding. 134 | * 135 | * Note that since it's not possible to determine the end of the JSON stream without 136 | * encoding the entire value, it's not possible to set the cursor using SEEK_END 137 | * constant and doing so will result in an exception. 138 | * 139 | * @param int $offset The offset for the cursor 140 | * @param int $whence Either SEEK_CUR or SEEK_SET to determine new cursor position 141 | * @throws \RuntimeException If SEEK_END is used to determine the cursor position 142 | */ 143 | public function seek($offset, $whence = SEEK_SET) 144 | { 145 | $position = $this->calculatePosition($offset, $whence); 146 | 147 | if (!isset($this->cursor) || $position < $this->cursor) { 148 | $this->getEncoder()->rewind(); 149 | $this->buffer = ''; 150 | $this->cursor = 0; 151 | } 152 | 153 | $this->forward($position); 154 | } 155 | 156 | /** 157 | * Calculates new position for the cursor based on offset and whence. 158 | * @param int $offset The cursor offset 159 | * @param int $whence One of the SEEK_* constants 160 | * @return int The new cursor position 161 | * @throws \RuntimeException If SEEK_END is used to determine the cursor position 162 | */ 163 | private function calculatePosition($offset, $whence) 164 | { 165 | if ($whence === SEEK_CUR) { 166 | return max(0, $this->cursor + (int) $offset); 167 | } elseif ($whence === SEEK_SET) { 168 | return max(0, (int) $offset); 169 | } elseif ($whence === SEEK_END) { 170 | throw new \RuntimeException('Cannot set cursor position from the end of a JSON stream'); 171 | } 172 | 173 | throw new \InvalidArgumentException("Invalid cursor relative position '$whence'"); 174 | } 175 | 176 | /** 177 | * Forwards the JSON stream reading cursor to the given position or to the end of stream. 178 | * @param int $position The new position of the cursor 179 | */ 180 | private function forward($position) 181 | { 182 | $encoder = $this->getEncoder(); 183 | 184 | while ($this->cursor < $position) { 185 | $length = strlen($this->buffer); 186 | 187 | if ($this->cursor + $length > $position) { 188 | $this->buffer = substr($this->buffer, $position - $this->cursor); 189 | $this->cursor = $position; 190 | break; 191 | } 192 | 193 | $this->cursor += $length; 194 | $this->buffer = ''; 195 | 196 | if (!$encoder->valid()) { 197 | $this->buffer = null; 198 | break; 199 | } 200 | 201 | $this->buffer = $encoder->current(); 202 | $encoder->next(); 203 | } 204 | } 205 | 206 | /** 207 | * Seeks the beginning of the JSON stream. 208 | * 209 | * If the encoding has already been started, rewinding the encoder may not work, 210 | * if the underlying value being encoded does not support rewinding. 211 | */ 212 | public function rewind() 213 | { 214 | $this->seek(0); 215 | } 216 | 217 | /** 218 | * Tells if the JSON stream is writable or not. 219 | * @return bool Always returns false as JSON streams are never writable 220 | */ 221 | public function isWritable() 222 | { 223 | return false; 224 | } 225 | 226 | /** 227 | * Writes the given bytes to the JSON stream. 228 | * 229 | * As the JSON stream does not represent a writable stream, this method will 230 | * always throw a runtime exception. 231 | * 232 | * @param string $string The bytes to write 233 | * @return int The number of bytes written 234 | * @throws \RuntimeException Always throws a runtime exception 235 | */ 236 | public function write($string) 237 | { 238 | throw new \RuntimeException('Cannot write to a JSON stream'); 239 | } 240 | 241 | /** 242 | * Tells if the JSON stream is readable or not. 243 | * @return bool Always returns true as JSON streams are always readable 244 | */ 245 | public function isReadable() 246 | { 247 | return true; 248 | } 249 | 250 | /** 251 | * Returns the given number of bytes from the JSON stream. 252 | * 253 | * The underlying value is encoded into JSON until enough bytes have been 254 | * generated to fulfill the requested number of bytes. The extraneous bytes are 255 | * then buffered for the next read from the JSON stream. The stream may return 256 | * fewer number of bytes if the entire value has been encoded and there are no 257 | * more bytes to return. 258 | * 259 | * @param int $length The number of bytes to return 260 | * @return string The bytes read from the JSON stream 261 | */ 262 | public function read($length) 263 | { 264 | if ($this->eof()) { 265 | return ''; 266 | } 267 | 268 | $length = max(0, (int) $length); 269 | $encoder = $this->getEncoder(); 270 | 271 | while (strlen($this->buffer) < $length && $encoder->valid()) { 272 | $this->buffer .= $encoder->current(); 273 | $encoder->next(); 274 | } 275 | 276 | if (strlen($this->buffer) > $length || $encoder->valid()) { 277 | $output = substr($this->buffer, 0, $length); 278 | $this->buffer = substr($this->buffer, $length); 279 | } else { 280 | $output = (string) $this->buffer; 281 | $this->buffer = null; 282 | } 283 | 284 | $this->cursor += strlen($output); 285 | 286 | return $output; 287 | } 288 | 289 | /** 290 | * Returns the remaining bytes from the JSON stream. 291 | * @return string The remaining bytes from JSON stream 292 | */ 293 | public function getContents() 294 | { 295 | if ($this->eof()) { 296 | return ''; 297 | } 298 | 299 | $encoder = $this->getEncoder(); 300 | $output = $this->buffer; 301 | 302 | while ($encoder->valid()) { 303 | $output .= $encoder->current(); 304 | $encoder->next(); 305 | } 306 | 307 | $this->cursor += strlen($output); 308 | $this->buffer = null; 309 | 310 | return $output; 311 | } 312 | 313 | /** 314 | * Returns the metadata related to the JSON stream. 315 | * 316 | * No underlying PHP resource exists for the stream, but this method will 317 | * still return relevant information regarding the stream that is similar 318 | * to PHP stream meta data. 319 | * 320 | * @param string|null $key The key of the value to return 321 | * @return array|mixed|null The meta data array, a specific value or null if not defined 322 | */ 323 | public function getMetadata($key = null) 324 | { 325 | $meta = [ 326 | 'timed_out' => false, 327 | 'blocked' => true, 328 | 'eof' => $this->eof(), 329 | 'unread_bytes' => strlen($this->buffer), 330 | 'stream_type' => get_class($this->encoder), 331 | 'wrapper_type' => 'OBJECT', 332 | 'wrapper_data' => [ 333 | 'step' => $this->encoder->key(), 334 | 'errors' => $this->encoder->getErrors(), 335 | ], 336 | 'mode' => 'r', 337 | 'seekable' => true, 338 | 'uri' => '', 339 | ]; 340 | 341 | return $key === null ? $meta : (isset($meta[$key]) ? $meta[$key] : null); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streaming JSON Encoder # 2 | 3 | *Streaming JSON Encoder* is a PHP library that provides a set of classes to help 4 | with encoding JSON in a streaming manner, i.e. allowing you to encode the JSON 5 | document bit by bit rather than encoding the whole document at once. Compared to 6 | the built in `json_encode` function, there are two main advantages: 7 | 8 | * You will not need to load the entire data set into memory, as the encoder 9 | supports iterating over both arrays and any kind of iterators, like 10 | generators, for example. 11 | * You will not need to load the entire resulting JSON document into the 12 | memory, since the JSON document will be encoded value by value and it's 13 | possible to output the encoded document piece by piece. 14 | 15 | In other words, the Streaming JSON Encoder can provide the greatest benefit 16 | when you need to handle large data sets that may otherwise take up too much 17 | memory to process. 18 | 19 | In order to increase interoperability, the library also provides a PSR-7 20 | compatible stream to use with frameworks and HTTP requests. 21 | 22 | The API documentation is available at: http://violet.riimu.net/api/streaming-json-encoder/ 23 | 24 | [![CI](https://img.shields.io/github/workflow/status/violet-php/streaming-json-encoder/CI/main?style=flat-square)](https://github.com/violet-php/streaming-json-encoder/actions) 25 | [![Scrutinizer](https://img.shields.io/scrutinizer/quality/g/violet-php/streaming-json-encoder/main?style=flat-square)](https://scrutinizer-ci.com/g/violet-php/streaming-json-encoder/) 26 | [![codecov](https://img.shields.io/codecov/c/github/violet-php/streaming-json-encoder/main?style=flat-square)](https://codecov.io/gh/violet-php/streaming-json-encoder) 27 | [![Packagist](https://img.shields.io/packagist/v/violet/streaming-json-encoder.svg?style=flat-square)](https://packagist.org/packages/violet/streaming-json-encoder) 28 | 29 | ## Requirements ## 30 | 31 | * The minimum supported PHP version is 5.6 32 | * The library depends on the following external PHP libraries: 33 | * [psr/http-message](https://packagist.org/packages/psr/http-message) (`^1.0`) 34 | 35 | ## Installation ## 36 | 37 | ### Installation with Composer ### 38 | 39 | The easiest way to install this library is to use Composer to handle your 40 | dependencies. In order to install this library via Composer, simply follow 41 | these two steps: 42 | 43 | 1. Acquire the `composer.phar` by running the Composer 44 | [Command-line installation](https://getcomposer.org/download/) 45 | in your project root. 46 | 47 | 2. Once you have run the installation script, you should have the `composer.phar` 48 | file in you project root and you can run the following command: 49 | 50 | ``` 51 | php composer.phar require "violet/streaming-json-encoder:^1.1" 52 | ``` 53 | 54 | After installing this library via Composer, you can load the library by 55 | including the `vendor/autoload.php` file that was generated by Composer during 56 | the installation. 57 | 58 | ### Adding the library as a dependency ### 59 | 60 | If you are already familiar with how to use Composer, you may alternatively add 61 | the library as a dependency by adding the following `composer.json` file to your 62 | project and running the `composer install` command: 63 | 64 | ```json 65 | { 66 | "require": { 67 | "violet/streaming-json-encoder": "^1.1" 68 | } 69 | } 70 | ``` 71 | 72 | ### Manual installation ### 73 | 74 | If you do not wish to use Composer to load the library, you may also download 75 | the library manually by downloading the [latest release](https://github.com/violet-php/streaming-json-encoder/releases/latest) 76 | and extracting the `src` folder to your project. You may then include the 77 | provided `src/autoload.php` file to load the library classes. 78 | 79 | Please note that using Composer will also automatically download the other 80 | required PHP libraries. If you install this library manually, you will also need 81 | to make those other required libraries available. 82 | 83 | ## Usage ## 84 | 85 | This library offers 3 main different ways to use the library via the classes 86 | `BufferJsonEncoder`, `StreamJsonEncoder` and the PSR-7 compatible stream 87 | `JsonStream`. 88 | 89 | ### Using BufferJsonEncoder ### 90 | 91 | The buffer encoder is most useful when you need to generate the JSON document 92 | in a way that does not involve passing callbacks to handle the generated JSON. 93 | 94 | The easiest way to use the `BufferJsonEncoder` is to instantiate it with the 95 | JSON value to encode and call the `encode()` method to return the entire output 96 | as a string: 97 | 98 | ```php 99 | encode(); 105 | ``` 106 | 107 | The most useful way to use this encoder, however, is to use it as an iterator. 108 | As the encoder implements the `Iterator` interface, you can simply loop over the 109 | generated JSON with a foreach loop: 110 | 111 | ```php 112 | encode(); 167 | ``` 168 | 169 | The `encode()` method in `StreamJsonEncoder` returns the total number of bytes 170 | it passed to the output. This encoder makes it convenient, for example, to 171 | write the JSON to file in a streaming manner. For example: 172 | 173 | ```php 174 | encode(); 187 | fclose($fp); 188 | ``` 189 | 190 | ### Using JsonStream ### 191 | 192 | The stream class provides a PSR-7 compatible `StreamInterface` for streaming 193 | JSON content. It actually uses the `BufferJsonEncoder` to do the hard work and 194 | simply wraps the calls in a stream like fashion. 195 | 196 | The constructor of `JsonStream` either accepts a value to encode as JSON or an 197 | instance of `BufferJsonEncoder` (which allows you to set the encoding options). 198 | You can then operate on the stream using the methods provided by the PSR-7 199 | interface. For example: 200 | 201 | ```php 202 | getFilename(); 209 | } 210 | }; 211 | 212 | $encoder = (new \Violet\StreamingJsonEncoder\BufferJsonEncoder($iterator)) 213 | ->setOptions(JSON_PRETTY_PRINT); 214 | 215 | $stream = new \Violet\StreamingJsonEncoder\JsonStream($encoder); 216 | 217 | while (!$stream->eof()) { 218 | echo $stream->read(1024 * 8); 219 | } 220 | ``` 221 | 222 | For more information about PSR-7 streams, please refer to the [PSR-7 223 | documentation](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface). 224 | 225 | ### How the encoder resolves values ### 226 | 227 | In many ways, the Streaming JSON encoder is intended to work mostly as a drop in 228 | replacement for `json_encode()`. However, since the encoder is intended to deal 229 | with large data sets, there are some notable differences in how it handles 230 | objects and arrays. 231 | 232 | First, to determine how to encode an object, the encoder will attempt to resolve 233 | the object values in following ways: 234 | 235 | * For any object that implements `JsonSerializable` the implemented method 236 | `jsonSerialize()` is called and return value is used instead. 237 | * Any `Closure` will be invoked and the return value will be used instead. 238 | However, no other invokables are called in this manner. 239 | 240 | The returned value is looped until it cannot be resolved further. After that, 241 | a decision is made on whether the array or object is encoded as an array or as 242 | an object. The following logic is used: 243 | 244 | * Any empty array or array that has keys from 0 to n-1 in that order are 245 | encoded as JSON arrays. All other arrays are encoded as JSON objects. 246 | * If an object implements `Traversable` and it either returns an **interger** 247 | `0` as the first key or returns no values at all, it will be encoded as a 248 | JSON array (regardless of other keys). All other objects implementing 249 | `Traversable` are encoded as JSON objects. 250 | * Any other object, whether it's empty or whatever keys it mey have, is encoded 251 | as a JSON object. 252 | 253 | Note, however, that if the JSON encoding option `JSON_FORCE_OBJECT` is used, all 254 | objects and arrays are encoded as JSON objects. 255 | 256 | Note that all objects are traversed via a `foreach` statement. This means that 257 | all `Traversable` objects are encoded using the values returned by the iterator. 258 | For other objects, this means that the public properties are used (as per 259 | default iteration behavior). 260 | 261 | All other values (i.e. nulls, booleans, numbers and strings) are treated exactly 262 | the same way as `json_encode()` does (and in fact, it's used to encode those 263 | values). 264 | 265 | ### JSON encoding options ### 266 | 267 | Both `BufferJsonEncoder` and `StreamJsonEncoder` have a method `setOptions()` to 268 | change the JSON encoding options. The accepted options are the same as those 269 | accepted by `json_encode()` function. The encoder still internally uses the 270 | `json_encode()` method to encode values other than arrays or object. A few 271 | options also have additional effects on the encoders: 272 | 273 | * Using `JSON_FORCE_OBJECT` will force all arrays and objects to be encoded 274 | as JSON objects similar to `json_encode()`. 275 | * Using `JSON_PRETTY_PRINT` causes the encoder to output whitespace to in 276 | order to make the output more readable. The used indentation can be changed 277 | using the method `setIndent()` which accepts either a string argument to use 278 | as the indent or an integer to indicate the number of spaces. 279 | * Using `JSON_PARTIAL_OUTPUT_ON_ERROR` will cause the encoder to continue the 280 | output despite encoding errors. Otherwise the encoding will halt and the 281 | encoder will throw an `EncodingException`. 282 | 283 | ## Credits ## 284 | 285 | This library is Copyright (c) 2017-2022 Riikka Kalliomäki. 286 | 287 | See LICENSE for license and copying information. 288 | -------------------------------------------------------------------------------- /src/AbstractJsonEncoder.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2016-2020 Riikka Kalliomäki 10 | * @license http://opensource.org/licenses/mit-license.php MIT License 11 | */ 12 | abstract class AbstractJsonEncoder implements \Iterator 13 | { 14 | /** @var \Iterator[] Current value stack in encoding */ 15 | private $stack; 16 | 17 | /** @var bool[] True for every object in the stack, false for an array */ 18 | private $stackType; 19 | 20 | /** @var array Stack of values being encoded */ 21 | private $valueStack; 22 | 23 | /** @var bool Whether the next value is the first value in an array or an object */ 24 | private $first; 25 | 26 | /** @var int The JSON encoding options */ 27 | private $options; 28 | 29 | /** @var bool Whether next token should be preceded by new line or not */ 30 | private $newLine; 31 | 32 | /** @var string Indent to use for indenting JSON output */ 33 | private $indent; 34 | 35 | /** @var string[] Errors that occurred in encoding */ 36 | private $errors; 37 | 38 | /** @var int Number of the current line in output */ 39 | private $line; 40 | 41 | /** @var int Number of the current column in output */ 42 | private $column; 43 | 44 | /** @var mixed The initial value to encode as JSON */ 45 | private $initialValue; 46 | 47 | /** @var int|null The current step of the encoder */ 48 | private $step; 49 | 50 | /** 51 | * AbstractJsonEncoder constructor. 52 | * @param mixed $value The value to encode as JSON 53 | */ 54 | public function __construct($value) 55 | { 56 | $this->initialValue = $value; 57 | $this->options = 0; 58 | $this->errors = []; 59 | $this->indent = ' '; 60 | } 61 | 62 | /** 63 | * Sets the JSON encoding options. 64 | * @param int $options The JSON encoding options that are used by json_encode 65 | * @return $this Returns self for call chaining 66 | * @throws \RuntimeException If changing encoding options during encoding operation 67 | */ 68 | public function setOptions($options) 69 | { 70 | if ($this->step !== null) { 71 | throw new \RuntimeException('Cannot change encoding options during encoding'); 72 | } 73 | 74 | $this->options = (int) $options; 75 | return $this; 76 | } 77 | 78 | /** 79 | * Sets the indent for the JSON output. 80 | * @param string|int $indent A string to use as indent or the number of spaces 81 | * @return $this Returns self for call chaining 82 | * @throws \RuntimeException If changing indent during encoding operation 83 | */ 84 | public function setIndent($indent) 85 | { 86 | if ($this->step !== null) { 87 | throw new \RuntimeException('Cannot change indent during encoding'); 88 | } 89 | 90 | $this->indent = is_int($indent) ? str_repeat(' ', $indent) : (string) $indent; 91 | return $this; 92 | } 93 | 94 | /** 95 | * Returns the list of errors that occurred during the last encoding process. 96 | * @return string[] List of errors that occurred during encoding 97 | */ 98 | public function getErrors() 99 | { 100 | return $this->errors; 101 | } 102 | 103 | /** 104 | * Returns the current encoding value stack. 105 | * @return array The current encoding value stack 106 | */ 107 | protected function getValueStack() 108 | { 109 | return $this->valueStack; 110 | } 111 | 112 | /** 113 | * Initializes the iterator if it has not been initialized yet. 114 | */ 115 | private function initialize() 116 | { 117 | if (!isset($this->stack)) { 118 | $this->rewind(); 119 | } 120 | } 121 | 122 | /** 123 | * Returns the current number of step in the encoder. 124 | * @return int|null The current step number as integer or null if the current state is not valid 125 | */ 126 | #[\ReturnTypeWillChange] 127 | public function key() 128 | { 129 | $this->initialize(); 130 | 131 | return $this->step; 132 | } 133 | 134 | /** 135 | * Tells if the encoder has a valid current state. 136 | * @return bool True if the iterator has a valid state, false if not 137 | */ 138 | #[\ReturnTypeWillChange] 139 | public function valid() 140 | { 141 | $this->initialize(); 142 | 143 | return $this->step !== null; 144 | } 145 | 146 | /** 147 | * Returns the current value or state from the encoder. 148 | * @return mixed The current value or state from the encoder 149 | */ 150 | #[\ReturnTypeWillChange] 151 | abstract public function current(); 152 | 153 | /** 154 | * Returns the JSON encoding to the beginning. 155 | */ 156 | #[\ReturnTypeWillChange] 157 | public function rewind() 158 | { 159 | if ($this->step === 0) { 160 | return; 161 | } 162 | 163 | $this->stack = []; 164 | $this->stackType = []; 165 | $this->valueStack = []; 166 | $this->errors = []; 167 | $this->newLine = false; 168 | $this->first = true; 169 | $this->line = 1; 170 | $this->column = 1; 171 | $this->step = 0; 172 | 173 | $this->processValue($this->initialValue); 174 | } 175 | 176 | /** 177 | * Iterates the next token or tokens to the output stream. 178 | */ 179 | #[\ReturnTypeWillChange] 180 | public function next() 181 | { 182 | $this->initialize(); 183 | 184 | if (!empty($this->stack)) { 185 | $this->step++; 186 | $iterator = end($this->stack); 187 | 188 | if ($iterator->valid()) { 189 | $this->processStack($iterator, end($this->stackType)); 190 | $iterator->next(); 191 | } else { 192 | $this->popStack(); 193 | } 194 | } else { 195 | $this->step = null; 196 | } 197 | } 198 | 199 | /** 200 | * Handles the next value from the iterator to be encoded as JSON. 201 | * @param \Iterator $iterator The iterator used to generate the next value 202 | * @param bool $isObject True if the iterator is being handled as an object, false if not 203 | */ 204 | private function processStack(\Iterator $iterator, $isObject) 205 | { 206 | if ($isObject) { 207 | if (!$this->processKey($iterator->key())) { 208 | return; 209 | } 210 | } elseif (!$this->first) { 211 | $this->outputLine(',', JsonToken::T_COMMA); 212 | } 213 | 214 | $this->first = false; 215 | $this->processValue($iterator->current()); 216 | } 217 | 218 | /** 219 | * Handles the given value key into JSON. 220 | * @param mixed $key The key to process 221 | * @return bool True if the key is valid, false if not 222 | */ 223 | private function processKey($key) 224 | { 225 | if (!is_int($key) && !is_string($key)) { 226 | $this->addError('Only string or integer keys are supported'); 227 | return false; 228 | } 229 | 230 | if (!$this->first) { 231 | $this->outputLine(',', JsonToken::T_COMMA); 232 | } 233 | 234 | $this->outputJson((string) $key, JsonToken::T_NAME); 235 | $this->output(':', JsonToken::T_COLON); 236 | 237 | if ($this->options & JSON_PRETTY_PRINT) { 238 | $this->output(' ', JsonToken::T_WHITESPACE); 239 | } 240 | 241 | return true; 242 | } 243 | 244 | /** 245 | * Handles the given JSON value appropriately depending on it's type. 246 | * @param mixed $value The value that should be encoded as JSON 247 | */ 248 | private function processValue($value) 249 | { 250 | $this->valueStack[] = $value; 251 | $value = $this->resolveValue($value); 252 | 253 | if (is_array($value) || is_object($value)) { 254 | $this->pushStack($value); 255 | } else { 256 | $this->outputJson($value, JsonToken::T_VALUE); 257 | array_pop($this->valueStack); 258 | } 259 | } 260 | 261 | /** 262 | * Resolves the actual value of any given value that is about to be processed. 263 | * @param mixed $value The value to resolve 264 | * @return mixed The resolved value 265 | */ 266 | protected function resolveValue($value) 267 | { 268 | do { 269 | if ($value instanceof \JsonSerializable) { 270 | $value = $value->jsonSerialize(); 271 | } elseif ($value instanceof \Closure) { 272 | $value = $value(); 273 | } else { 274 | break; 275 | } 276 | } while (true); 277 | 278 | return $value; 279 | } 280 | 281 | /** 282 | * Adds an JSON encoding error to the list of errors. 283 | * @param string $message The error message to add 284 | * @throws EncodingException If the encoding should not continue due to the error 285 | */ 286 | private function addError($message) 287 | { 288 | $errorMessage = sprintf('Line %d, column %d: %s', $this->line, $this->column, $message); 289 | $this->errors[] = $errorMessage; 290 | 291 | if ($this->options & JSON_PARTIAL_OUTPUT_ON_ERROR) { 292 | return; 293 | } 294 | 295 | $this->stack = []; 296 | $this->step = null; 297 | 298 | throw new EncodingException($errorMessage); 299 | } 300 | 301 | /** 302 | * Pushes the given iterable to the value stack. 303 | * @param object|array $iterable The iterable value to push to the stack 304 | */ 305 | private function pushStack($iterable) 306 | { 307 | $iterator = $this->getIterator($iterable); 308 | $isObject = $this->isObject($iterable, $iterator); 309 | 310 | if ($isObject) { 311 | $this->outputLine('{', JsonToken::T_LEFT_BRACE); 312 | } else { 313 | $this->outputLine('[', JsonToken::T_LEFT_BRACKET); 314 | } 315 | 316 | $this->first = true; 317 | $this->stack[] = $iterator; 318 | $this->stackType[] = $isObject; 319 | } 320 | 321 | /** 322 | * Creates a generator from the given iterable using a foreach loop. 323 | * @param object|array $iterable The iterable value to iterate 324 | * @return \Generator The generator using the given iterable 325 | */ 326 | private function getIterator($iterable) 327 | { 328 | foreach ($iterable as $key => $value) { 329 | yield $key => $value; 330 | } 331 | } 332 | 333 | /** 334 | * Tells if the given iterable should be handled as a JSON object or not. 335 | * @param object|array $iterable The iterable value to test 336 | * @param \Iterator $iterator An Iterator created from the iterable value 337 | * @return bool True if the given iterable should be treated as object, false if not 338 | */ 339 | private function isObject($iterable, \Iterator $iterator) 340 | { 341 | if ($this->options & JSON_FORCE_OBJECT) { 342 | return true; 343 | } 344 | 345 | if ($iterable instanceof \Traversable) { 346 | return $iterator->valid() && $iterator->key() !== 0; 347 | } 348 | 349 | return is_object($iterable) || $this->isAssociative($iterable); 350 | } 351 | 352 | /** 353 | * Tells if the given array is an associative array. 354 | * @param array $array The array to test 355 | * @return bool True if the array is associative, false if not 356 | */ 357 | private function isAssociative(array $array) 358 | { 359 | if ($array === []) { 360 | return false; 361 | } 362 | 363 | $expected = 0; 364 | 365 | foreach ($array as $key => $_) { 366 | if ($key !== $expected++) { 367 | return true; 368 | } 369 | } 370 | 371 | return false; 372 | } 373 | 374 | /** 375 | * Removes the top element of the value stack. 376 | */ 377 | private function popStack() 378 | { 379 | if (!$this->first) { 380 | $this->newLine = true; 381 | } 382 | 383 | $this->first = false; 384 | array_pop($this->stack); 385 | 386 | if (array_pop($this->stackType)) { 387 | $this->output('}', JsonToken::T_RIGHT_BRACE); 388 | } else { 389 | $this->output(']', JsonToken::T_RIGHT_BRACKET); 390 | } 391 | 392 | array_pop($this->valueStack); 393 | } 394 | 395 | /** 396 | * Encodes the given value as JSON and passes it to output stream. 397 | * @param mixed $value The value to output as JSON 398 | * @param int $token The token type of the value 399 | */ 400 | private function outputJson($value, $token) 401 | { 402 | $encoded = json_encode($value, $this->options); 403 | $error = json_last_error(); 404 | 405 | if ($error !== JSON_ERROR_NONE) { 406 | $this->addError(sprintf('%s (%s)', json_last_error_msg(), $this->getJsonErrorName($error))); 407 | } 408 | 409 | $this->output($encoded, $token); 410 | } 411 | 412 | /** 413 | * Returns the name of the JSON error constant. 414 | * @param int $error The error code to find 415 | * @return string The name for the error code 416 | */ 417 | private function getJsonErrorName($error) 418 | { 419 | $matches = array_keys(get_defined_constants(), $error, true); 420 | $prefix = 'JSON_ERROR_'; 421 | $prefixLength = strlen($prefix); 422 | $name = 'UNKNOWN_ERROR'; 423 | 424 | foreach ($matches as $match) { 425 | if (is_string($match) && strncmp($match, $prefix, $prefixLength) === 0) { 426 | $name = $match; 427 | break; 428 | } 429 | } 430 | 431 | return $name; 432 | } 433 | 434 | /** 435 | * Passes the given token to the output stream and ensures the next token is preceded by a newline. 436 | * @param string $string The token to write to the output stream 437 | * @param int $token The type of the token 438 | */ 439 | private function outputLine($string, $token) 440 | { 441 | $this->output($string, $token); 442 | $this->newLine = true; 443 | } 444 | 445 | /** 446 | * Passes the given token to the output stream. 447 | * @param string $string The token to write to the output stream 448 | * @param int $token The type of the token 449 | */ 450 | private function output($string, $token) 451 | { 452 | if ($this->newLine && $this->options & JSON_PRETTY_PRINT) { 453 | $indent = str_repeat($this->indent, count($this->stack)); 454 | $this->write("\n", JsonToken::T_WHITESPACE); 455 | 456 | if ($indent !== '') { 457 | $this->write($indent, JsonToken::T_WHITESPACE); 458 | } 459 | 460 | $this->line++; 461 | $this->column = strlen($indent) + 1; 462 | } 463 | 464 | $this->newLine = false; 465 | $this->write($string, $token); 466 | $this->column += strlen($string); 467 | } 468 | 469 | /** 470 | * Actually handles the writing of the given token to the output stream. 471 | * @param string $string The given token to write 472 | * @param int $token The type of the token 473 | * @return void 474 | */ 475 | abstract protected function write($string, $token); 476 | } 477 | --------------------------------------------------------------------------------