├── .github └── FUNDING.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Decoder.php └── Encoder.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clue 2 | custom: https://clue.engineering/support 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.3.0 (2022-12-23) 4 | 5 | * Feature: Add support for PHP 8.1 and PHP 8.2. 6 | (#31 by @clue and #30 by @SimonFring) 7 | 8 | * Feature: Check type of incoming `data` before trying to decode NDJSON. 9 | (#29 by @SimonFrings) 10 | 11 | * Improve documentation and examples and update to new [default loop](https://reactphp.org/event-loop/#loop). 12 | (#26 by @clue, #27 by @SimonFrings and #25 by @PaulRotmann) 13 | 14 | * Improve test suite, report failed assertions and ensure 100% code coverage. 15 | (#32 and #33 by @clue and #28 by @SimonFrings) 16 | 17 | ## 1.2.0 (2020-12-09) 18 | 19 | * Improve test suite and add `.gitattributes` to exclude dev files from exports. 20 | Add PHP 8 support, update to PHPUnit 9 and simplify test setup. 21 | (#18 by @clue and #19, #22 and #23 by @SimonFrings) 22 | 23 | ## 1.1.0 (2020-02-04) 24 | 25 | * Feature: Improve error reporting and add parsing error message to Exception and 26 | ignore `JSON_THROW_ON_ERROR` option (available as of PHP 7.3). 27 | (#14 by @clue) 28 | 29 | * Feature: Add benchmarking script and import all global function references. 30 | (#16 by @clue) 31 | 32 | * Improve documentation and add NDJSON format description and 33 | add support / sponsorship info. 34 | (#12 and #17 by @clue) 35 | 36 | * Improve test suite to run tests on PHP 7.4 and simplify test matrix and 37 | apply minor code style adjustments to make phpstan happy. 38 | (#13 and #15 by @clue) 39 | 40 | ## 1.0.0 (2018-05-17) 41 | 42 | * First stable release, now following SemVer 43 | 44 | * Improve documentation and usage examples 45 | 46 | > Contains no other changes, so it's actually fully compatible with the v0.1.2 release. 47 | 48 | ## 0.1.2 (2018-05-11) 49 | 50 | * Feature: Limit buffer size to 64 KiB by default. 51 | (#10 by @clue) 52 | 53 | * Feature: Forward compatibility with EventLoop v0.5 and upcoming v1.0. 54 | (#8 by @clue) 55 | 56 | * Fix: Return bool `false` if encoding fails due to invalid value to pause source. 57 | (#9 by @clue) 58 | 59 | * Improve test suite by supporting PHPUnit v6 and test against legacy PHP 5.3 through PHP 7.2. 60 | (#7 by @clue) 61 | 62 | * Update project homepage. 63 | (#11 by @clue) 64 | 65 | ## 0.1.1 (2017-05-22) 66 | 67 | * Feature: Forward compatibility with Stream v0.7, v0.6, v0.5 and upcoming v1.0 (while keeping BC) 68 | (#6 by @thklein) 69 | 70 | * Improved test suite by adding PHPUnit to `require-dev` 71 | (#5 by @thklein) 72 | 73 | ## 0.1.0 (2016-11-24) 74 | 75 | * First tagged release 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Christian Lück 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 furnished 10 | 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clue/reactphp-ndjson 2 | 3 | [![CI status](https://github.com/clue/reactphp-ndjson/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-ndjson/actions) 4 | [![installs on Packagist](https://img.shields.io/packagist/dt/clue/ndjson-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/ndjson-react) 5 | [![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests) 6 | 7 | Streaming newline-delimited JSON ([NDJSON](http://ndjson.org/)) parser and encoder for [ReactPHP](https://reactphp.org/). 8 | 9 | [NDJSON](http://ndjson.org/) can be used to store multiple JSON records in a 10 | file to store any kind of (uniform) structured data, such as a list of user 11 | objects or log entries. It uses a simple newline character between each 12 | individual record and as such can be both used for efficient persistence and 13 | simple append-style operations. This also allows it to be used in a streaming 14 | context, such as a simple inter-process communication (IPC) protocol or for a 15 | remote procedure call (RPC) mechanism. This library provides a simple 16 | streaming API to process very large NDJSON files with thousands or even millions 17 | of rows efficiently without having to load the whole file into memory at once. 18 | 19 | * **Standard interfaces** - 20 | Allows easy integration with existing higher-level components by implementing 21 | ReactPHP's standard streaming interfaces. 22 | * **Lightweight, SOLID design** - 23 | Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough) 24 | and does not get in your way. 25 | Builds on top of well-tested components and well-established concepts instead of reinventing the wheel. 26 | * **Good test coverage** - 27 | Comes with an [automated tests suite](#tests) and is regularly tested in the *real world*. 28 | 29 | **Table of contents** 30 | 31 | * [Support us](#support-us) 32 | * [NDJSON format](#ndjson-format) 33 | * [Usage](#usage) 34 | * [Decoder](#decoder) 35 | * [Encoder](#encoder) 36 | * [Install](#install) 37 | * [Tests](#tests) 38 | * [License](#license) 39 | * [More](#more) 40 | 41 | ## Support us 42 | 43 | We invest a lot of time developing, maintaining, and updating our awesome 44 | open-source projects. You can help us sustain this high-quality of our work by 45 | [becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get 46 | numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) 47 | for details. 48 | 49 | Let's take these projects to the next level together! 🚀 50 | 51 | ## NDJSON format 52 | 53 | NDJSON ("Newline-Delimited JSON" or sometimes referred to as "JSON lines") is a 54 | very simple text-based format for storing a large number of records, such as a 55 | list of user records or log entries. 56 | 57 | ```JSON 58 | {"name":"Alice","age":30,"comment":"Yes, I like cheese"} 59 | {"name":"Bob","age":50,"comment":"Hello\nWorld!"} 60 | ``` 61 | 62 | If you understand JSON and you're now looking at this newline-delimited JSON for 63 | the first time, you should already know everything you need to know to 64 | understand NDJSON: As the name implies, this format essentially consists of 65 | individual lines where each individual line is any valid JSON text and each line 66 | is delimited with a newline character. 67 | 68 | This example uses a list of user objects where each user has some arbitrary 69 | properties. This can easily be adjusted for many different use cases, such as 70 | storing for example products instead of users, assigning additional properties 71 | or having a significantly larger number of records. You can edit NDJSON files in 72 | any text editor or use them in a streaming context where individual records 73 | should be processed. Unlike normal JSON files, adding a new log entry to this 74 | NDJSON file does not require modification of this file's structure (note there's 75 | no "outer array" to be modified). This makes it a perfect fit for a streaming 76 | context, for line-oriented CLI tools (such as `grep` and others) or for a logging 77 | context where you want to append records at a later time. Additionally, this 78 | also allows it to be used in a streaming context, such as a simple inter-process 79 | communication (IPC) protocol or for a remote procedure call (RPC) mechanism. 80 | 81 | The newline character at the end of each line allows for some really simple 82 | *framing* (detecting individual records). While each individual line is valid 83 | JSON, the complete file as a whole is technically no longer valid JSON, because 84 | it contains multiple JSON texts. This implies that for example calling PHP's 85 | `json_decode()` on this complete input would fail because it would try to parse 86 | multiple records at once. Likewise, using "pretty printing" JSON 87 | (`JSON_PRETTY_PRINT`) is not allowed because each JSON text is limited to exactly 88 | one line. On the other hand, values containing newline characters (such as the 89 | `comment` property in the above example) do not cause issues because each newline 90 | within a JSON string will be represented by a `\n` instead. 91 | 92 | One common alternative to NDJSON would be Comma-Separated Values (CSV). 93 | If you want to process CSV files, you may want to take a look at the related 94 | project [clue/reactphp-csv](https://github.com/clue/reactphp-csv) instead: 95 | 96 | ``` 97 | name,age,comment 98 | Alice,30,"Yes, I like cheese" 99 | Bob,50,"Hello 100 | World!" 101 | ``` 102 | 103 | CSV may look slightly simpler, but this simplicity comes at a price. CSV is 104 | limited to untyped, two-dimensional data, so there's no standard way of storing 105 | any nested structures or to differentiate a boolean value from a string or 106 | integer. Field names are sometimes used, sometimes they're not 107 | (application-dependant). Inconsistent handling for fields that contain 108 | separators such as `,` or spaces or line breaks (see the `comment` field above) 109 | introduce additional complexity and its text encoding is usually undefined, 110 | Unicode (or UTF-8) is unlikely to be supported and CSV files often use ISO 111 | 8859-1 encoding or some variant (again application-dependant). 112 | 113 | While NDJSON helps avoiding many of CSV's shortcomings, it is still a 114 | (relatively) young format while CSV files have been used in production systems 115 | for decades. This means that if you want to interface with an existing system, 116 | you may have to rely on the format that's already supported. If you're building 117 | a new system, using NDJSON is an excellent choice as it provides a flexible way 118 | to process individual records using a common text-based format that can include 119 | any kind of structured data. 120 | 121 | ## Usage 122 | 123 | ### Decoder 124 | 125 | The `Decoder` (parser) class can be used to make sure you only get back 126 | complete, valid JSON elements when reading from a stream. 127 | It wraps a given 128 | [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) 129 | and exposes its data through the same interface, but emits the JSON elements 130 | as parsed values instead of just chunks of strings: 131 | 132 | ``` 133 | {"name":"test","active":true} 134 | {"name":"hello w\u00f6rld","active":true} 135 | ``` 136 | 137 | ```php 138 | $stdin = new React\Stream\ReadableResourceStream(STDIN); 139 | 140 | $ndjson = new Clue\React\NDJson\Decoder($stdin); 141 | 142 | $ndjson->on('data', function ($data) { 143 | // $data is a parsed element from the JSON stream 144 | // line 1: $data = (object)array('name' => 'test', 'active' => true); 145 | // line 2: $data = (object)array('name' => 'hello wörld', 'active' => true); 146 | var_dump($data); 147 | }); 148 | ``` 149 | 150 | ReactPHP's streams emit chunks of data strings and make no assumption about their lengths. 151 | These chunks do not necessarily represent complete JSON elements, as an 152 | element may be broken up into multiple chunks. 153 | This class reassembles these elements by buffering incomplete ones. 154 | 155 | The `Decoder` supports the same optional parameters as the underlying 156 | [`json_decode()`](https://www.php.net/manual/en/function.json-decode.php) function. 157 | This means that, by default, JSON objects will be emitted as a `stdClass`. 158 | This behavior can be controlled through the optional constructor parameters: 159 | 160 | ```php 161 | $ndjson = new Clue\React\NDJson\Decoder($stdin, true); 162 | 163 | $ndjson->on('data', function ($data) { 164 | // JSON objects will be emitted as assoc arrays now 165 | }); 166 | ``` 167 | 168 | Additionally, the `Decoder` limits the maximum buffer size (maximum line 169 | length) to avoid buffer overflows due to malformed user input. Usually, there 170 | should be no need to change this value, unless you know you're dealing with some 171 | unreasonably long lines. It accepts an additional argument if you want to change 172 | this from the default of 64 KiB: 173 | 174 | ```php 175 | $ndjson = new Clue\React\NDJson\Decoder($stdin, false, 512, 0, 64 * 1024); 176 | ``` 177 | 178 | If the underlying stream emits an `error` event or the plain stream contains 179 | any data that does not represent a valid NDJson stream, 180 | it will emit an `error` event and then `close` the input stream: 181 | 182 | ```php 183 | $ndjson->on('error', function (Exception $error) { 184 | // an error occurred, stream will close next 185 | }); 186 | ``` 187 | 188 | If the underlying stream emits an `end` event, it will flush any incomplete 189 | data from the buffer, thus either possibly emitting a final `data` event 190 | followed by an `end` event on success or an `error` event for 191 | incomplete/invalid JSON data as above: 192 | 193 | ```php 194 | $ndjson->on('end', function () { 195 | // stream successfully ended, stream will close next 196 | }); 197 | ``` 198 | 199 | If either the underlying stream or the `Decoder` is closed, it will forward 200 | the `close` event: 201 | 202 | ```php 203 | $ndjson->on('close', function () { 204 | // stream closed 205 | // possibly after an "end" event or due to an "error" event 206 | }); 207 | ``` 208 | 209 | The `close(): void` method can be used to explicitly close the `Decoder` and 210 | its underlying stream: 211 | 212 | ```php 213 | $ndjson->close(); 214 | ``` 215 | 216 | The `pipe(WritableStreamInterface $dest, array $options = array(): WritableStreamInterface` 217 | method can be used to forward all data to the given destination stream. 218 | Please note that the `Decoder` emits decoded/parsed data events, while many 219 | (most?) writable streams expect only data chunks: 220 | 221 | ```php 222 | $ndjson->pipe($logger); 223 | ``` 224 | 225 | For more details, see ReactPHP's 226 | [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface). 227 | 228 | ### Encoder 229 | 230 | The `Encoder` (serializer) class can be used to make sure anything you write to 231 | a stream ends up as valid JSON elements in the resulting NDJSON stream. 232 | It wraps a given 233 | [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface) 234 | and accepts its data through the same interface, but handles any data as complete 235 | JSON elements instead of just chunks of strings: 236 | 237 | ```php 238 | $stdout = new React\Stream\WritableResourceStream(STDOUT); 239 | 240 | $ndjson = new Clue\React\NDJson\Encoder($stdout); 241 | 242 | $ndjson->write(array('name' => 'test', 'active' => true)); 243 | $ndjson->write(array('name' => 'hello wörld', 'active' => true)); 244 | ``` 245 | ``` 246 | {"name":"test","active":true} 247 | {"name":"hello w\u00f6rld","active":true} 248 | ``` 249 | 250 | The `Encoder` supports the same parameters as the underlying 251 | [`json_encode()`](https://www.php.net/manual/en/function.json-encode.php) function. 252 | This means that, by default, Unicode characters will be escaped in the output. 253 | This behavior can be controlled through the optional constructor parameters: 254 | 255 | ```php 256 | $ndjson = new Clue\React\NDJson\Encoder($stdout, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 257 | 258 | $ndjson->write('hello wörld'); 259 | ``` 260 | ``` 261 | "hello wörld" 262 | ``` 263 | 264 | Note that trying to pass the `JSON_PRETTY_PRINT` option will yield an 265 | `InvalidArgumentException` because it is not compatible with NDJSON. 266 | 267 | If the underlying stream emits an `error` event or the given data contains 268 | any data that can not be represented as a valid NDJSON stream, 269 | it will emit an `error` event and then `close` the input stream: 270 | 271 | ```php 272 | $ndjson->on('error', function (Exception $error) { 273 | // an error occurred, stream will close next 274 | }); 275 | ``` 276 | 277 | If either the underlying stream or the `Encoder` is closed, it will forward 278 | the `close` event: 279 | 280 | ```php 281 | $ndjson->on('close', function () { 282 | // stream closed 283 | // possibly after an "end" event or due to an "error" event 284 | }); 285 | ``` 286 | 287 | The `end(mixed $data = null): void` method can be used to optionally emit 288 | any final data and then soft-close the `Encoder` and its underlying stream: 289 | 290 | ```php 291 | $ndjson->end(); 292 | ``` 293 | 294 | The `close(): void` method can be used to explicitly close the `Encoder` and 295 | its underlying stream: 296 | 297 | ```php 298 | $ndjson->close(); 299 | ``` 300 | 301 | For more details, see ReactPHP's 302 | [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface). 303 | 304 | ## Install 305 | 306 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 307 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 308 | 309 | This project follows [SemVer](https://semver.org/). 310 | This will install the latest supported version: 311 | 312 | ```bash 313 | composer require clue/ndjson-react:^1.3 314 | ``` 315 | 316 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 317 | 318 | This project aims to run on any platform and thus does not require any PHP 319 | extensions and supports running on legacy PHP 5.3 through current PHP 8+ and 320 | HHVM. 321 | It's *highly recommended to use the latest supported PHP version* for this project. 322 | 323 | ## Tests 324 | 325 | To run the test suite, you first need to clone this repo and then install all 326 | dependencies [through Composer](https://getcomposer.org/): 327 | 328 | ```bash 329 | composer install 330 | ``` 331 | 332 | To run the test suite, go to the project root and run: 333 | 334 | ```bash 335 | vendor/bin/phpunit 336 | ``` 337 | 338 | ## License 339 | 340 | This project is released under the permissive [MIT license](LICENSE). 341 | 342 | > Did you know that I offer custom development services and issuing invoices for 343 | sponsorships of releases and for contributions? Contact me (@clue) for details. 344 | 345 | ## More 346 | 347 | * If you want to learn more about processing streams of data, refer to the documentation of 348 | the underlying [react/stream](https://github.com/reactphp/stream) component. 349 | 350 | * If you want to process compressed NDJSON files (`.ndjson.gz` file extension), 351 | you may want to use [clue/reactphp-zlib](https://github.com/clue/reactphp-zlib) 352 | on the compressed input stream before passing the decompressed stream to the NDJSON decoder. 353 | 354 | * If you want to create compressed NDJSON files (`.ndjson.gz` file extension), 355 | you may want to use [clue/reactphp-zlib](https://github.com/clue/reactphp-zlib) 356 | on the resulting NDJSON encoder output stream before passing the compressed 357 | stream to the file output stream. 358 | 359 | * If you want to concurrently process the records from your NDJSON stream, 360 | you may want to use [clue/reactphp-flux](https://github.com/clue/reactphp-flux) 361 | to concurrently process many (but not too many) records at once. 362 | 363 | * If you want to process structured data in the more common text-based format, 364 | you may want to use [clue/reactphp-csv](https://github.com/clue/reactphp-csv) 365 | to process Comma-Separated-Values (CSV) files (`.csv` file extension). 366 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/ndjson-react", 3 | "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", 4 | "keywords": ["NDJSON", "newline", "JSON", "jsonlines", "streaming", "ReactPHP"], 5 | "homepage": "https://github.com/clue/reactphp-ndjson", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@clue.engineering" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3", 15 | "react/stream": "^1.2" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", 19 | "react/event-loop": "^1.2" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Clue\\React\\NDJson\\": "src/" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "Clue\\Tests\\React\\NDJson\\": "tests/" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Decoder.php: -------------------------------------------------------------------------------- 1 | input = $input; 45 | 46 | if (!$input->isReadable()) { 47 | $this->close(); 48 | return; 49 | } 50 | 51 | $this->assoc = $assoc; 52 | $this->depth = $depth; 53 | $this->options = $options; 54 | $this->maxlength = $maxlength; 55 | 56 | $this->input->on('data', array($this, 'handleData')); 57 | $this->input->on('end', array($this, 'handleEnd')); 58 | $this->input->on('error', array($this, 'handleError')); 59 | $this->input->on('close', array($this, 'close')); 60 | } 61 | 62 | public function isReadable() 63 | { 64 | return !$this->closed; 65 | } 66 | 67 | public function close() 68 | { 69 | if ($this->closed) { 70 | return; 71 | } 72 | 73 | $this->closed = true; 74 | $this->buffer = ''; 75 | 76 | $this->input->close(); 77 | 78 | $this->emit('close'); 79 | $this->removeAllListeners(); 80 | } 81 | 82 | public function pause() 83 | { 84 | $this->input->pause(); 85 | } 86 | 87 | public function resume() 88 | { 89 | $this->input->resume(); 90 | } 91 | 92 | public function pipe(WritableStreamInterface $dest, array $options = array()) 93 | { 94 | Util::pipe($this, $dest, $options); 95 | 96 | return $dest; 97 | } 98 | 99 | /** @internal */ 100 | public function handleData($data) 101 | { 102 | if (!\is_string($data)) { 103 | $this->handleError(new \UnexpectedValueException('Expected stream to emit string, but got ' . \gettype($data))); 104 | return; 105 | } 106 | 107 | $this->buffer .= $data; 108 | 109 | // keep parsing while a newline has been found 110 | while (($newline = \strpos($this->buffer, "\n")) !== false && $newline <= $this->maxlength) { 111 | // read data up until newline and remove from buffer 112 | $data = (string)\substr($this->buffer, 0, $newline); 113 | $this->buffer = (string)\substr($this->buffer, $newline + 1); 114 | 115 | // decode data with options given in ctor 116 | // @codeCoverageIgnoreStart 117 | if ($this->options === 0) { 118 | $data = \json_decode($data, $this->assoc, $this->depth); 119 | } else { 120 | assert(\PHP_VERSION_ID >= 50400); 121 | $data = \json_decode($data, $this->assoc, $this->depth, $this->options); 122 | } 123 | // @codeCoverageIgnoreEnd 124 | 125 | // abort stream if decoding failed 126 | if ($data === null && \json_last_error() !== \JSON_ERROR_NONE) { 127 | // @codeCoverageIgnoreStart 128 | if (\PHP_VERSION_ID > 50500) { 129 | $errstr = \json_last_error_msg(); 130 | } elseif (\json_last_error() === \JSON_ERROR_SYNTAX) { 131 | $errstr = 'Syntax error'; 132 | } else { 133 | $errstr = 'Unknown error'; 134 | } 135 | // @codeCoverageIgnoreEnd 136 | return $this->handleError(new \RuntimeException('Unable to decode JSON: ' . $errstr, \json_last_error())); 137 | } 138 | 139 | $this->emit('data', array($data)); 140 | } 141 | 142 | if (isset($this->buffer[$this->maxlength])) { 143 | $this->handleError(new \OverflowException('Buffer size exceeded')); 144 | } 145 | } 146 | 147 | /** @internal */ 148 | public function handleEnd() 149 | { 150 | if ($this->buffer !== '') { 151 | $this->handleData("\n"); 152 | } 153 | 154 | if (!$this->closed) { 155 | $this->emit('end'); 156 | $this->close(); 157 | } 158 | } 159 | 160 | /** @internal */ 161 | public function handleError(\Exception $error) 162 | { 163 | $this->emit('error', array($error)); 164 | $this->close(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Encoder.php: -------------------------------------------------------------------------------- 1 | output = $output; 41 | 42 | if (!$output->isWritable()) { 43 | $this->close(); 44 | return; 45 | } 46 | 47 | $this->options = $options; 48 | $this->depth = $depth; 49 | 50 | $this->output->on('drain', array($this, 'handleDrain')); 51 | $this->output->on('error', array($this, 'handleError')); 52 | $this->output->on('close', array($this, 'close')); 53 | } 54 | 55 | public function write($data) 56 | { 57 | if ($this->closed) { 58 | return false; 59 | } 60 | 61 | // we have to handle PHP warnings for legacy PHP < 5.5 62 | // certain values (such as INF etc.) emit a warning, but still encode successfully 63 | // @codeCoverageIgnoreStart 64 | if (\PHP_VERSION_ID < 50500) { 65 | $errstr = null; 66 | \set_error_handler(function ($_, $error) use (&$errstr) { 67 | $errstr = $error; 68 | }); 69 | 70 | // encode data with options given in ctor (depth not supported) 71 | $data = \json_encode($data, $this->options); 72 | 73 | // always check error code and match missing error messages 74 | \restore_error_handler(); 75 | $errno = \json_last_error(); 76 | if (\defined('JSON_ERROR_UTF8') && $errno === \JSON_ERROR_UTF8) { 77 | // const JSON_ERROR_UTF8 added in PHP 5.3.3, but no error message assigned in legacy PHP < 5.5 78 | // this overrides PHP 5.3.14 only: https://3v4l.org/IGP8Z#v5314 79 | $errstr = 'Malformed UTF-8 characters, possibly incorrectly encoded'; 80 | } elseif ($errno !== \JSON_ERROR_NONE && $errstr === null) { 81 | // error number present, but no error message applicable 82 | $errstr = 'Unknown error'; 83 | } 84 | 85 | // abort stream if encoding fails 86 | if ($errno !== \JSON_ERROR_NONE || $errstr !== null) { 87 | $this->handleError(new \RuntimeException('Unable to encode JSON: ' . $errstr, $errno)); 88 | return false; 89 | } 90 | } else { 91 | // encode data with options given in ctor 92 | $data = \json_encode($data, $this->options, $this->depth); 93 | 94 | // abort stream if encoding fails 95 | if ($data === false && \json_last_error() !== \JSON_ERROR_NONE) { 96 | $this->handleError(new \RuntimeException('Unable to encode JSON: ' . \json_last_error_msg(), \json_last_error())); 97 | return false; 98 | } 99 | } 100 | // @codeCoverageIgnoreEnd 101 | 102 | return $this->output->write($data . "\n"); 103 | } 104 | 105 | public function end($data = null) 106 | { 107 | if ($data !== null) { 108 | $this->write($data); 109 | } 110 | 111 | $this->output->end(); 112 | } 113 | 114 | public function isWritable() 115 | { 116 | return !$this->closed; 117 | } 118 | 119 | public function close() 120 | { 121 | if ($this->closed) { 122 | return; 123 | } 124 | 125 | $this->closed = true; 126 | $this->output->close(); 127 | 128 | $this->emit('close'); 129 | $this->removeAllListeners(); 130 | } 131 | 132 | /** @internal */ 133 | public function handleDrain() 134 | { 135 | $this->emit('drain'); 136 | } 137 | 138 | /** @internal */ 139 | public function handleError(\Exception $error) 140 | { 141 | $this->emit('error', array($error)); 142 | $this->close(); 143 | } 144 | } 145 | --------------------------------------------------------------------------------