├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── UnwrapReadableStream.php ├── UnwrapWritableStream.php ├── functions.php └── functions_include.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.7.0 (2023-12-13) 4 | 5 | * Feature: Full PHP 8.3 compatibility. 6 | (#39 by @clue) 7 | 8 | * Update test suite and collect all garbage cycles. 9 | (#38 and #39 by @clue) 10 | 11 | ## 1.6.0 (2023-07-07) 12 | 13 | * Feature: Update unwrapped stream to avoid unhandled promise rejections. 14 | (#37 by @clue) 15 | 16 | * Feature: Improve `first()` promise resolution to clean up any garbage references. 17 | (#36 by @lucasnetau) 18 | 19 | * Improve test suite and project setup and report failed assertions. 20 | (#34 by @clue and #35 by @WyriHaximus) 21 | 22 | ## 1.5.0 (2022-09-09) 23 | 24 | * Feature: Full support for PHP 8.2 release. 25 | (#33 by @WyriHaximus) 26 | 27 | * Improve test suite and minor documentation improvements. 28 | (#32 by @clue and #31 by @nhedger) 29 | 30 | ## 1.4.0 (2022-06-20) 31 | 32 | * Feature: Forward compatibility with react/promise 3. 33 | (#20 by @WyriHaximus) 34 | 35 | * Improve test suite, test against PHP 8.1 and fix legacy HHVM build. 36 | (#28, #29 and #30 by @SimonFrings) 37 | 38 | ## 1.3.0 (2021-10-18) 39 | 40 | * Feature: Improve error reporting by appending previous exception messages. 41 | (#26 by @clue) 42 | 43 | For most common use cases this means that simply reporting the `Exception` 44 | message should give the most relevant details for any issues: 45 | 46 | ```php 47 | React\Promise\Stream\buffer($stream)->then(function (string $contents) { 48 | // … 49 | }, function (Exception $e) { 50 | echo 'Error:' . $e->getMessage() . PHP_EOL; 51 | }); 52 | ``` 53 | 54 | * Improve documentation, describe promise and stream data types. 55 | (#27 by @clue and #23 by @WyriHaximus) 56 | 57 | * Improve test suite and add `.gitattributes` to exclude dev files from exports. 58 | Use GitHub actions for continuous integration (CI) and run tests on PHPUnit 9 and PHP 8. 59 | (#21 by @reedy and #22, #24 and #25 by @SimonFrings) 60 | 61 | ## 1.2.0 (2019-07-03) 62 | 63 | * Feature: Support unwrapping object streams by buffering original write chunks in array. 64 | (#15 by @clue) 65 | 66 | * Feature: Clean up unneeded references for unwrapped streams when closing. 67 | (#18 by @clue) 68 | 69 | * Fix: Writing to closed unwrapped stream should return false (backpressure). 70 | (#17 by @clue) 71 | 72 | * Improve test suite to support PHPUnit 7, PHP 7.3 and fix incomplete test 73 | and improve API documentation. 74 | (#16 and #19 by @clue) 75 | 76 | ## 1.1.1 (2017-12-22) 77 | 78 | * Fix: Fix `all()` to assume null values if no event data is passed 79 | (#13 by @clue) 80 | 81 | * Improve test suite by simplifying test bootstrapping logic via Composer and 82 | add forward compatibility with PHPUnit 5 and PHPUnit 6 and 83 | test against PHP 7.1 and 7.2 84 | (#11 and #12 by @clue and #9 by @carusogabriel) 85 | 86 | ## 1.1.0 (2017-11-28) 87 | 88 | * Feature: Reject `first()` when stream emits an error event 89 | (#7 by @clue) 90 | 91 | * Fix: Explicit `close()` of unwrapped stream should not emit `error` event 92 | (#8 by @clue) 93 | 94 | * Internal refactoring to simplify `buffer()` function 95 | (#6 by @kelunik) 96 | 97 | ## 1.0.0 (2017-10-24) 98 | 99 | * First stable release, now following SemVer 100 | 101 | > Contains no other changes, so it's actually fully compatible with the v0.1.2 release. 102 | 103 | ## 0.1.2 (2017-10-18) 104 | 105 | * Feature: Optional maximum buffer length for `buffer()` (#3 by @WyriHaximus) 106 | * Improvement: Readme improvements (#5 by @jsor) 107 | 108 | ## 0.1.1 (2017-05-15) 109 | 110 | * Improvement: Forward compatibility with stream 1.0, 0.7, 0.6, and 0.5 (#2 by @WyriHaximus) 111 | 112 | ## 0.1.0 (2017-05-10) 113 | 114 | * Initial release, adapted from [`clue/promise-stream-react`](https://github.com/clue/php-promise-stream-react) 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden 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 | # PromiseStream 2 | 3 | [![CI status](https://github.com/reactphp/promise-stream/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/promise-stream/actions) 4 | [![installs on Packagist](https://img.shields.io/packagist/dt/react/promise-stream?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/promise-stream) 5 | 6 | The missing link between Promise-land and Stream-land 7 | for [ReactPHP](https://reactphp.org/). 8 | 9 | **Table of Contents** 10 | 11 | * [Usage](#usage) 12 | * [buffer()](#buffer) 13 | * [first()](#first) 14 | * [all()](#all) 15 | * [unwrapReadable()](#unwrapreadable) 16 | * [unwrapWritable()](#unwrapwritable) 17 | * [Install](#install) 18 | * [Tests](#tests) 19 | * [License](#license) 20 | 21 | ## Usage 22 | 23 | This lightweight library consists only of a few simple functions. 24 | All functions reside under the `React\Promise\Stream` namespace. 25 | 26 | The below examples refer to all functions with their fully-qualified names like this: 27 | 28 | ```php 29 | React\Promise\Stream\buffer(…); 30 | ``` 31 | 32 | As of PHP 5.6+ you can also import each required function into your code like this: 33 | 34 | ```php 35 | use function React\Promise\Stream\buffer; 36 | 37 | buffer(…); 38 | ``` 39 | 40 | Alternatively, you can also use an import statement similar to this: 41 | 42 | ```php 43 | use React\Promise\Stream; 44 | 45 | Stream\buffer(…); 46 | ``` 47 | 48 | ### buffer() 49 | 50 | The `buffer(ReadableStreamInterface $stream, ?int $maxLength = null): PromiseInterface` function can be used to 51 | create a `Promise` which will be fulfilled with the stream data buffer. 52 | 53 | ```php 54 | $stream = accessSomeJsonStream(); 55 | 56 | React\Promise\Stream\buffer($stream)->then(function (string $contents) { 57 | var_dump(json_decode($contents)); 58 | }); 59 | ``` 60 | 61 | The promise will be fulfilled with a `string` of all data chunks concatenated once the stream closes. 62 | 63 | The promise will be fulfilled with an empty `string` if the stream is already closed. 64 | 65 | The promise will be rejected with a `RuntimeException` if the stream emits an error. 66 | 67 | The promise will be rejected with a `RuntimeException` if it is cancelled. 68 | 69 | The optional `$maxLength` argument defaults to no limit. In case the maximum 70 | length is given and the stream emits more data before the end, the promise 71 | will be rejected with an `OverflowException`. 72 | 73 | ```php 74 | $stream = accessSomeToLargeStream(); 75 | 76 | React\Promise\Stream\buffer($stream, 1024)->then(function ($contents) { 77 | var_dump(json_decode($contents)); 78 | }, function ($error) { 79 | // Reaching here when the stream buffer goes above the max size, 80 | // in this example that is 1024 bytes, 81 | // or when the stream emits an error. 82 | }); 83 | ``` 84 | 85 | ### first() 86 | 87 | The `first(ReadableStreamInterface|WritableStreamInterface $stream, string $event = 'data'): PromiseInterface` function can be used to 88 | create a `Promise` which will be fulfilled once the given event triggers for the first time. 89 | 90 | ```php 91 | $stream = accessSomeJsonStream(); 92 | 93 | React\Promise\Stream\first($stream)->then(function (string $chunk) { 94 | echo 'The first chunk arrived: ' . $chunk; 95 | }); 96 | ``` 97 | 98 | The promise will be fulfilled with a `mixed` value of whatever the first event 99 | emitted or `null` if the event does not pass any data. 100 | If you do not pass a custom event name, then it will wait for the first "data" 101 | event. 102 | For common streams of type `ReadableStreamInterface`, this means it will be 103 | fulfilled with a `string` containing the first data chunk. 104 | 105 | The promise will be rejected with a `RuntimeException` if the stream emits an error 106 | – unless you're waiting for the "error" event, in which case it will be fulfilled. 107 | 108 | The promise will be rejected with a `RuntimeException` once the stream closes 109 | – unless you're waiting for the "close" event, in which case it will be fulfilled. 110 | 111 | The promise will be rejected with a `RuntimeException` if the stream is already closed. 112 | 113 | The promise will be rejected with a `RuntimeException` if it is cancelled. 114 | 115 | ### all() 116 | 117 | The `all(ReadableStreamInterface|WritableStreamInterface $stream, string $event = 'data'): PromiseInterface` function can be used to 118 | create a `Promise` which will be fulfilled with an array of all the event data. 119 | 120 | ```php 121 | $stream = accessSomeJsonStream(); 122 | 123 | React\Promise\Stream\all($stream)->then(function (array $chunks) { 124 | echo 'The stream consists of ' . count($chunks) . ' chunk(s)'; 125 | }); 126 | ``` 127 | 128 | The promise will be fulfilled with an `array` once the stream closes. The array 129 | will contain whatever all events emitted or `null` values if the events do not pass any data. 130 | If you do not pass a custom event name, then it will wait for all the "data" 131 | events. 132 | For common streams of type `ReadableStreamInterface`, this means it will be 133 | fulfilled with a `string[]` array containing all the data chunk. 134 | 135 | The promise will be fulfilled with an empty `array` if the stream is already closed. 136 | 137 | The promise will be rejected with a `RuntimeException` if the stream emits an error. 138 | 139 | The promise will be rejected with a `RuntimeException` if it is cancelled. 140 | 141 | ### unwrapReadable() 142 | 143 | The `unwrapReadable(PromiseInterface> $promise): ReadableStreamInterface` function can be used to 144 | unwrap a `Promise` which will be fulfilled with a `ReadableStreamInterface`. 145 | 146 | This function returns a readable stream instance (implementing `ReadableStreamInterface`) 147 | right away which acts as a proxy for the future promise resolution. 148 | Once the given Promise will be fulfilled with a `ReadableStreamInterface`, its 149 | data will be piped to the output stream. 150 | 151 | ```php 152 | //$promise = someFunctionWhichResolvesWithAStream(); 153 | $promise = startDownloadStream($uri); 154 | 155 | $stream = React\Promise\Stream\unwrapReadable($promise); 156 | 157 | $stream->on('data', function (string $data) { 158 | echo $data; 159 | }); 160 | 161 | $stream->on('end', function () { 162 | echo 'DONE'; 163 | }); 164 | ``` 165 | 166 | If the given promise is either rejected or fulfilled with anything but an 167 | instance of `ReadableStreamInterface`, then the output stream will emit 168 | an `error` event and close: 169 | 170 | ```php 171 | $promise = startDownloadStream($invalidUri); 172 | 173 | $stream = React\Promise\Stream\unwrapReadable($promise); 174 | 175 | $stream->on('error', function (Exception $error) { 176 | echo 'Error: ' . $error->getMessage(); 177 | }); 178 | ``` 179 | 180 | The given `$promise` SHOULD be pending, i.e. it SHOULD NOT be fulfilled or rejected 181 | at the time of invoking this function. 182 | If the given promise is already settled and does not fulfill with an instance of 183 | `ReadableStreamInterface`, then you will not be able to receive the `error` event. 184 | 185 | You can `close()` the resulting stream at any time, which will either try to 186 | `cancel()` the pending promise or try to `close()` the underlying stream. 187 | 188 | ```php 189 | $promise = startDownloadStream($uri); 190 | 191 | $stream = React\Promise\Stream\unwrapReadable($promise); 192 | 193 | $loop->addTimer(2.0, function () use ($stream) { 194 | $stream->close(); 195 | }); 196 | ``` 197 | 198 | ### unwrapWritable() 199 | 200 | The `unwrapWritable(PromiseInterface> $promise): WritableStreamInterface` function can be used to 201 | unwrap a `Promise` which will be fulfilled with a `WritableStreamInterface`. 202 | 203 | This function returns a writable stream instance (implementing `WritableStreamInterface`) 204 | right away which acts as a proxy for the future promise resolution. 205 | Any writes to this instance will be buffered in memory for when the promise will 206 | be fulfilled. 207 | Once the given Promise will be fulfilled with a `WritableStreamInterface`, any 208 | data you have written to the proxy will be forwarded transparently to the inner 209 | stream. 210 | 211 | ```php 212 | //$promise = someFunctionWhichResolvesWithAStream(); 213 | $promise = startUploadStream($uri); 214 | 215 | $stream = React\Promise\Stream\unwrapWritable($promise); 216 | 217 | $stream->write('hello'); 218 | $stream->end('world'); 219 | 220 | $stream->on('close', function () { 221 | echo 'DONE'; 222 | }); 223 | ``` 224 | 225 | If the given promise is either rejected or fulfilled with anything but an 226 | instance of `WritableStreamInterface`, then the output stream will emit 227 | an `error` event and close: 228 | 229 | ```php 230 | $promise = startUploadStream($invalidUri); 231 | 232 | $stream = React\Promise\Stream\unwrapWritable($promise); 233 | 234 | $stream->on('error', function (Exception $error) { 235 | echo 'Error: ' . $error->getMessage(); 236 | }); 237 | ``` 238 | 239 | The given `$promise` SHOULD be pending, i.e. it SHOULD NOT be fulfilled or rejected 240 | at the time of invoking this function. 241 | If the given promise is already settled and does not fulfill with an instance of 242 | `WritableStreamInterface`, then you will not be able to receive the `error` event. 243 | 244 | You can `close()` the resulting stream at any time, which will either try to 245 | `cancel()` the pending promise or try to `close()` the underlying stream. 246 | 247 | ```php 248 | $promise = startUploadStream($uri); 249 | 250 | $stream = React\Promise\Stream\unwrapWritable($promise); 251 | 252 | $loop->addTimer(2.0, function () use ($stream) { 253 | $stream->close(); 254 | }); 255 | ``` 256 | 257 | ## Install 258 | 259 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 260 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 261 | 262 | This project follows [SemVer](https://semver.org/). 263 | This will install the latest supported version: 264 | 265 | ```bash 266 | composer require react/promise-stream:^1.7 267 | ``` 268 | 269 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 270 | 271 | This project aims to run on any platform and thus does not require any PHP 272 | extensions and supports running on legacy PHP 5.3 through current PHP 8+ and 273 | HHVM. 274 | It's *highly recommended to use the latest supported PHP version* for this project. 275 | 276 | ## Tests 277 | 278 | To run the test suite, you first need to clone this repo and then install all 279 | dependencies [through Composer](https://getcomposer.org/): 280 | 281 | ```bash 282 | composer install 283 | ``` 284 | 285 | To run the test suite, go to the project root and run: 286 | 287 | ```bash 288 | vendor/bin/phpunit 289 | ``` 290 | 291 | ## License 292 | 293 | MIT, see [LICENSE file](LICENSE). 294 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react/promise-stream", 3 | "description": "The missing link between Promise-land and Stream-land for ReactPHP", 4 | "keywords": ["unwrap", "stream", "buffer", "promise", "ReactPHP", "async"], 5 | "homepage": "https://github.com/reactphp/promise-stream", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "homepage": "https://clue.engineering/", 11 | "email": "christian@clue.engineering" 12 | }, 13 | { 14 | "name": "Cees-Jan Kiewiet", 15 | "homepage": "https://wyrihaximus.net/", 16 | "email": "reactphp@ceesjankiewiet.nl" 17 | }, 18 | { 19 | "name": "Jan Sorgalla", 20 | "homepage": "https://sorgalla.com/", 21 | "email": "jsorgalla@gmail.com" 22 | }, 23 | { 24 | "name": "Chris Boden", 25 | "homepage": "https://cboden.dev/", 26 | "email": "cboden@gmail.com" 27 | } 28 | ], 29 | "require": { 30 | "php": ">=5.3", 31 | "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6", 32 | "react/promise": "^3 || ^2.1 || ^1.2" 33 | }, 34 | "require-dev": { 35 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "React\\Promise\\Stream\\": "src/" 40 | }, 41 | "files": [ 42 | "src/functions_include.php" 43 | ] 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "React\\Tests\\Promise\\Stream\\": "tests/" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/UnwrapReadableStream.php: -------------------------------------------------------------------------------- 1 | $promise 26 | */ 27 | public function __construct(PromiseInterface $promise) 28 | { 29 | $out = $this; 30 | $closed =& $this->closed; 31 | 32 | $this->promise = $promise->then( 33 | function ($stream) { 34 | if (!$stream instanceof ReadableStreamInterface) { 35 | throw new InvalidArgumentException('Not a readable stream'); 36 | } 37 | return $stream; 38 | } 39 | )->then( 40 | function (ReadableStreamInterface $stream) use ($out, &$closed) { 41 | // stream is already closed, make sure to close output stream 42 | if (!$stream->isReadable()) { 43 | $out->close(); 44 | return $stream; 45 | } 46 | 47 | // resolves but output is already closed, make sure to close stream silently 48 | if ($closed) { 49 | $stream->close(); 50 | return $stream; 51 | } 52 | 53 | // stream any writes into output stream 54 | $stream->on('data', function ($data) use ($out) { 55 | $out->emit('data', array($data, $out)); 56 | }); 57 | 58 | // forward end events and close 59 | $stream->on('end', function () use ($out, &$closed) { 60 | if (!$closed) { 61 | $out->emit('end', array($out)); 62 | $out->close(); 63 | } 64 | }); 65 | 66 | // error events cancel output stream 67 | $stream->on('error', function ($error) use ($out) { 68 | $out->emit('error', array($error, $out)); 69 | $out->close(); 70 | }); 71 | 72 | // close both streams once either side closes 73 | $stream->on('close', array($out, 'close')); 74 | $out->on('close', array($stream, 'close')); 75 | 76 | return $stream; 77 | }, 78 | function ($e) use ($out, &$closed) { 79 | // Forward exception as error event if not already closed 80 | if (!$closed) { 81 | $out->emit('error', array($e, $out)); 82 | $out->close(); 83 | } 84 | 85 | // Both resume() and pause() may attach to this promise, so 86 | // return a NOOP stream instance here. 87 | $stream = new ThroughStream(); 88 | $stream->close(); 89 | return $stream; 90 | } 91 | ); 92 | } 93 | 94 | public function isReadable() 95 | { 96 | return !$this->closed; 97 | } 98 | 99 | public function pause() 100 | { 101 | if ($this->promise !== null) { 102 | $this->promise->then(function (ReadableStreamInterface $stream) { 103 | $stream->pause(); 104 | }); 105 | } 106 | } 107 | 108 | public function resume() 109 | { 110 | if ($this->promise !== null) { 111 | $this->promise->then(function (ReadableStreamInterface $stream) { 112 | $stream->resume(); 113 | }); 114 | } 115 | } 116 | 117 | public function pipe(WritableStreamInterface $dest, array $options = array()) 118 | { 119 | Util::pipe($this, $dest, $options); 120 | 121 | return $dest; 122 | } 123 | 124 | public function close() 125 | { 126 | if ($this->closed) { 127 | return; 128 | } 129 | 130 | $this->closed = true; 131 | 132 | // try to cancel promise once the stream closes 133 | if ($this->promise !== null && \method_exists($this->promise, 'cancel')) { 134 | $this->promise->cancel(); 135 | } 136 | $this->promise = null; 137 | 138 | $this->emit('close'); 139 | $this->removeAllListeners(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/UnwrapWritableStream.php: -------------------------------------------------------------------------------- 1 | $promise 26 | */ 27 | public function __construct(PromiseInterface $promise) 28 | { 29 | $out = $this; 30 | $store =& $this->stream; 31 | $buffer =& $this->buffer; 32 | $ending =& $this->ending; 33 | $closed =& $this->closed; 34 | 35 | $this->promise = $promise->then( 36 | function ($stream) { 37 | if (!$stream instanceof WritableStreamInterface) { 38 | throw new InvalidArgumentException('Not a writable stream'); 39 | } 40 | return $stream; 41 | } 42 | )->then( 43 | function (WritableStreamInterface $stream) use ($out, &$store, &$buffer, &$ending, &$closed) { 44 | // stream is already closed, make sure to close output stream 45 | if (!$stream->isWritable()) { 46 | $out->close(); 47 | return $stream; 48 | } 49 | 50 | // resolves but output is already closed, make sure to close stream silently 51 | if ($closed) { 52 | $stream->close(); 53 | return $stream; 54 | } 55 | 56 | // forward drain events for back pressure 57 | $stream->on('drain', function () use ($out) { 58 | $out->emit('drain', array($out)); 59 | }); 60 | 61 | // error events cancel output stream 62 | $stream->on('error', function ($error) use ($out) { 63 | $out->emit('error', array($error, $out)); 64 | $out->close(); 65 | }); 66 | 67 | // close both streams once either side closes 68 | $stream->on('close', array($out, 'close')); 69 | $out->on('close', array($stream, 'close')); 70 | 71 | if ($buffer) { 72 | // flush buffer to stream and check if its buffer is not exceeded 73 | $drained = true; 74 | foreach ($buffer as $chunk) { 75 | if (!$stream->write($chunk)) { 76 | $drained = false; 77 | } 78 | } 79 | $buffer = array(); 80 | 81 | if ($drained) { 82 | // signal drain event, because the output stream previous signalled a full buffer 83 | $out->emit('drain', array($out)); 84 | } 85 | } 86 | 87 | if ($ending) { 88 | $stream->end(); 89 | } else { 90 | $store = $stream; 91 | } 92 | 93 | return $stream; 94 | }, 95 | function ($e) use ($out, &$closed) { 96 | if (!$closed) { 97 | $out->emit('error', array($e, $out)); 98 | $out->close(); 99 | } 100 | } 101 | ); 102 | } 103 | 104 | public function write($data) 105 | { 106 | if ($this->ending) { 107 | return false; 108 | } 109 | 110 | // forward to inner stream if possible 111 | if ($this->stream !== null) { 112 | return $this->stream->write($data); 113 | } 114 | 115 | // append to buffer and signal the buffer is full 116 | $this->buffer[] = $data; 117 | return false; 118 | } 119 | 120 | public function end($data = null) 121 | { 122 | if ($this->ending) { 123 | return; 124 | } 125 | 126 | $this->ending = true; 127 | 128 | // forward to inner stream if possible 129 | if ($this->stream !== null) { 130 | return $this->stream->end($data); 131 | } 132 | 133 | // append to buffer 134 | if ($data !== null) { 135 | $this->buffer[] = $data; 136 | } 137 | } 138 | 139 | public function isWritable() 140 | { 141 | return !$this->ending; 142 | } 143 | 144 | public function close() 145 | { 146 | if ($this->closed) { 147 | return; 148 | } 149 | 150 | $this->buffer = array(); 151 | $this->ending = true; 152 | $this->closed = true; 153 | 154 | // try to cancel promise once the stream closes 155 | if ($this->promise !== null && \method_exists($this->promise, 'cancel')) { 156 | $this->promise->cancel(); 157 | } 158 | $this->promise = $this->stream = null; 159 | 160 | $this->emit('close'); 161 | $this->removeAllListeners(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | then(function (string $contents) { 18 | * var_dump(json_decode($contents)); 19 | * }); 20 | * ``` 21 | * 22 | * The promise will be fulfilled with a `string` of all data chunks concatenated once the stream closes. 23 | * 24 | * The promise will be fulfilled with an empty `string` if the stream is already closed. 25 | * 26 | * The promise will be rejected with a `RuntimeException` if the stream emits an error. 27 | * 28 | * The promise will be rejected with a `RuntimeException` if it is cancelled. 29 | * 30 | * The optional `$maxLength` argument defaults to no limit. In case the maximum 31 | * length is given and the stream emits more data before the end, the promise 32 | * will be rejected with an `OverflowException`. 33 | * 34 | * ```php 35 | * $stream = accessSomeToLargeStream(); 36 | * 37 | * React\Promise\Stream\buffer($stream, 1024)->then(function ($contents) { 38 | * var_dump(json_decode($contents)); 39 | * }, function ($error) { 40 | * // Reaching here when the stream buffer goes above the max size, 41 | * // in this example that is 1024 bytes, 42 | * // or when the stream emits an error. 43 | * }); 44 | * ``` 45 | * 46 | * @param ReadableStreamInterface $stream 47 | * @param ?int $maxLength Maximum number of bytes to buffer or null for unlimited. 48 | * @return PromiseInterface 49 | */ 50 | function buffer(ReadableStreamInterface $stream, $maxLength = null) 51 | { 52 | // stream already ended => resolve with empty buffer 53 | if (!$stream->isReadable()) { 54 | return Promise\resolve(''); 55 | } 56 | 57 | $buffer = ''; 58 | 59 | $promise = new Promise\Promise(function ($resolve, $reject) use ($stream, $maxLength, &$buffer, &$bufferer) { 60 | $bufferer = function ($data) use (&$buffer, $reject, $maxLength) { 61 | $buffer .= $data; 62 | 63 | if ($maxLength !== null && isset($buffer[$maxLength])) { 64 | $reject(new \OverflowException('Buffer exceeded maximum length')); 65 | } 66 | }; 67 | 68 | $stream->on('data', $bufferer); 69 | 70 | $stream->on('error', function (\Exception $e) use ($reject) { 71 | $reject(new \RuntimeException( 72 | 'An error occured on the underlying stream while buffering: ' . $e->getMessage(), 73 | $e->getCode(), 74 | $e 75 | )); 76 | }); 77 | 78 | $stream->on('close', function () use ($resolve, &$buffer) { 79 | $resolve($buffer); 80 | }); 81 | }, function ($_, $reject) { 82 | $reject(new \RuntimeException('Cancelled buffering')); 83 | }); 84 | 85 | return $promise->then(null, function (\Exception $error) use (&$buffer, $bufferer, $stream) { 86 | // promise rejected => clear buffer and buffering 87 | $buffer = ''; 88 | $stream->removeListener('data', $bufferer); 89 | 90 | throw $error; 91 | }); 92 | } 93 | 94 | /** 95 | * Create a `Promise` which will be fulfilled once the given event triggers for the first time. 96 | * 97 | * ```php 98 | * $stream = accessSomeJsonStream(); 99 | * 100 | * React\Promise\Stream\first($stream)->then(function (string $chunk) { 101 | * echo 'The first chunk arrived: ' . $chunk; 102 | * }); 103 | * ``` 104 | * 105 | * The promise will be fulfilled with a `mixed` value of whatever the first event 106 | * emitted or `null` if the event does not pass any data. 107 | * If you do not pass a custom event name, then it will wait for the first "data" 108 | * event. 109 | * For common streams of type `ReadableStreamInterface`, this means it will be 110 | * fulfilled with a `string` containing the first data chunk. 111 | * 112 | * The promise will be rejected with a `RuntimeException` if the stream emits an error 113 | * – unless you're waiting for the "error" event, in which case it will be fulfilled. 114 | * 115 | * The promise will be rejected with a `RuntimeException` once the stream closes 116 | * – unless you're waiting for the "close" event, in which case it will be fulfilled. 117 | * 118 | * The promise will be rejected with a `RuntimeException` if the stream is already closed. 119 | * 120 | * The promise will be rejected with a `RuntimeException` if it is cancelled. 121 | * 122 | * @param ReadableStreamInterface|WritableStreamInterface $stream 123 | * @param string $event 124 | * @return PromiseInterface 125 | */ 126 | function first(EventEmitterInterface $stream, $event = 'data') 127 | { 128 | if ($stream instanceof ReadableStreamInterface) { 129 | // readable or duplex stream not readable => already closed 130 | // a half-open duplex stream is considered closed if its readable side is closed 131 | if (!$stream->isReadable()) { 132 | return Promise\reject(new \RuntimeException('Stream already closed')); 133 | } 134 | } elseif ($stream instanceof WritableStreamInterface) { 135 | // writable-only stream (not duplex) not writable => already closed 136 | if (!$stream->isWritable()) { 137 | return Promise\reject(new \RuntimeException('Stream already closed')); 138 | } 139 | } 140 | 141 | return new Promise\Promise(function ($resolve, $reject) use ($stream, $event, &$listener) { 142 | $listener = function ($data = null) use ($stream, $event, &$listener, $resolve) { 143 | $stream->removeListener($event, $listener); 144 | $listener = null; 145 | $resolve($data); 146 | }; 147 | $stream->on($event, $listener); 148 | 149 | if ($event !== 'error') { 150 | $stream->on('error', function (\Exception $e) use ($stream, $event, $listener, $reject) { 151 | $stream->removeListener($event, $listener); 152 | $reject(new \RuntimeException( 153 | 'An error occured on the underlying stream while waiting for event: ' . $e->getMessage(), 154 | $e->getCode(), 155 | $e 156 | )); 157 | }); 158 | } 159 | 160 | $stream->on('close', function () use ($stream, $event, &$listener, $reject) { 161 | if ($listener !== null) { 162 | $stream->removeListener($event, $listener); 163 | $listener = null; 164 | } 165 | $reject(new \RuntimeException('Stream closed')); 166 | }); 167 | }, function ($_, $reject) use ($stream, $event, &$listener) { 168 | $stream->removeListener($event, $listener); 169 | $reject(new \RuntimeException('Operation cancelled')); 170 | }); 171 | } 172 | 173 | /** 174 | * Create a `Promise` which will be fulfilled with an array of all the event data. 175 | * 176 | * ```php 177 | * $stream = accessSomeJsonStream(); 178 | * 179 | * React\Promise\Stream\all($stream)->then(function (array $chunks) { 180 | * echo 'The stream consists of ' . count($chunks) . ' chunk(s)'; 181 | * }); 182 | * ``` 183 | * 184 | * The promise will be fulfilled with an `array` once the stream closes. The array 185 | * will contain whatever all events emitted or `null` values if the events do not pass any data. 186 | * If you do not pass a custom event name, then it will wait for all the "data" 187 | * events. 188 | * For common streams of type `ReadableStreamInterface`, this means it will be 189 | * fulfilled with a `string[]` array containing all the data chunk. 190 | * 191 | * The promise will be fulfilled with an empty `array` if the stream is already closed. 192 | * 193 | * The promise will be rejected with a `RuntimeException` if the stream emits an error. 194 | * 195 | * The promise will be rejected with a `RuntimeException` if it is cancelled. 196 | * 197 | * @param ReadableStreamInterface|WritableStreamInterface $stream 198 | * @param string $event 199 | * @return PromiseInterface 200 | */ 201 | function all(EventEmitterInterface $stream, $event = 'data') 202 | { 203 | // stream already ended => resolve with empty buffer 204 | if ($stream instanceof ReadableStreamInterface) { 205 | // readable or duplex stream not readable => already closed 206 | // a half-open duplex stream is considered closed if its readable side is closed 207 | if (!$stream->isReadable()) { 208 | return Promise\resolve(array()); 209 | } 210 | } elseif ($stream instanceof WritableStreamInterface) { 211 | // writable-only stream (not duplex) not writable => already closed 212 | if (!$stream->isWritable()) { 213 | return Promise\resolve(array()); 214 | } 215 | } 216 | 217 | $buffer = array(); 218 | $bufferer = function ($data = null) use (&$buffer) { 219 | $buffer []= $data; 220 | }; 221 | $stream->on($event, $bufferer); 222 | 223 | $promise = new Promise\Promise(function ($resolve, $reject) use ($stream, &$buffer) { 224 | $stream->on('error', function (\Exception $e) use ($reject) { 225 | $reject(new \RuntimeException( 226 | 'An error occured on the underlying stream while buffering: ' . $e->getMessage(), 227 | $e->getCode(), 228 | $e 229 | )); 230 | }); 231 | 232 | $stream->on('close', function () use ($resolve, &$buffer) { 233 | $resolve($buffer); 234 | }); 235 | }, function ($_, $reject) { 236 | $reject(new \RuntimeException('Cancelled buffering')); 237 | }); 238 | 239 | return $promise->then(null, function ($error) use (&$buffer, $bufferer, $stream, $event) { 240 | // promise rejected => clear buffer and buffering 241 | $buffer = array(); 242 | $stream->removeListener($event, $bufferer); 243 | 244 | throw $error; 245 | }); 246 | } 247 | 248 | /** 249 | * Unwrap a `Promise` which will be fulfilled with a `ReadableStreamInterface`. 250 | * 251 | * This function returns a readable stream instance (implementing `ReadableStreamInterface`) 252 | * right away which acts as a proxy for the future promise resolution. 253 | * Once the given Promise will be fulfilled with a `ReadableStreamInterface`, its 254 | * data will be piped to the output stream. 255 | * 256 | * ```php 257 | * //$promise = someFunctionWhichResolvesWithAStream(); 258 | * $promise = startDownloadStream($uri); 259 | * 260 | * $stream = React\Promise\Stream\unwrapReadable($promise); 261 | * 262 | * $stream->on('data', function (string $data) { 263 | * echo $data; 264 | * }); 265 | * 266 | * $stream->on('end', function () { 267 | * echo 'DONE'; 268 | * }); 269 | * ``` 270 | * 271 | * If the given promise is either rejected or fulfilled with anything but an 272 | * instance of `ReadableStreamInterface`, then the output stream will emit 273 | * an `error` event and close: 274 | * 275 | * ```php 276 | * $promise = startDownloadStream($invalidUri); 277 | * 278 | * $stream = React\Promise\Stream\unwrapReadable($promise); 279 | * 280 | * $stream->on('error', function (Exception $error) { 281 | * echo 'Error: ' . $error->getMessage(); 282 | * }); 283 | * ``` 284 | * 285 | * The given `$promise` SHOULD be pending, i.e. it SHOULD NOT be fulfilled or rejected 286 | * at the time of invoking this function. 287 | * If the given promise is already settled and does not fulfill with an instance of 288 | * `ReadableStreamInterface`, then you will not be able to receive the `error` event. 289 | * 290 | * You can `close()` the resulting stream at any time, which will either try to 291 | * `cancel()` the pending promise or try to `close()` the underlying stream. 292 | * 293 | * ```php 294 | * $promise = startDownloadStream($uri); 295 | * 296 | * $stream = React\Promise\Stream\unwrapReadable($promise); 297 | * 298 | * $loop->addTimer(2.0, function () use ($stream) { 299 | * $stream->close(); 300 | * }); 301 | * ``` 302 | * 303 | * @param PromiseInterface> $promise 304 | * @return ReadableStreamInterface 305 | */ 306 | function unwrapReadable(PromiseInterface $promise) 307 | { 308 | return new UnwrapReadableStream($promise); 309 | } 310 | 311 | /** 312 | * unwrap a `Promise` which will be fulfilled with a `WritableStreamInterface`. 313 | * 314 | * This function returns a writable stream instance (implementing `WritableStreamInterface`) 315 | * right away which acts as a proxy for the future promise resolution. 316 | * Any writes to this instance will be buffered in memory for when the promise will 317 | * be fulfilled. 318 | * Once the given Promise will be fulfilled with a `WritableStreamInterface`, any 319 | * data you have written to the proxy will be forwarded transparently to the inner 320 | * stream. 321 | * 322 | * ```php 323 | * //$promise = someFunctionWhichResolvesWithAStream(); 324 | * $promise = startUploadStream($uri); 325 | * 326 | * $stream = React\Promise\Stream\unwrapWritable($promise); 327 | * 328 | * $stream->write('hello'); 329 | * $stream->end('world'); 330 | * 331 | * $stream->on('close', function () { 332 | * echo 'DONE'; 333 | * }); 334 | * ``` 335 | * 336 | * If the given promise is either rejected or fulfilled with anything but an 337 | * instance of `WritableStreamInterface`, then the output stream will emit 338 | * an `error` event and close: 339 | * 340 | * ```php 341 | * $promise = startUploadStream($invalidUri); 342 | * 343 | * $stream = React\Promise\Stream\unwrapWritable($promise); 344 | * 345 | * $stream->on('error', function (Exception $error) { 346 | * echo 'Error: ' . $error->getMessage(); 347 | * }); 348 | * ``` 349 | * 350 | * The given `$promise` SHOULD be pending, i.e. it SHOULD NOT be fulfilled or rejected 351 | * at the time of invoking this function. 352 | * If the given promise is already settled and does not fulfill with an instance of 353 | * `WritableStreamInterface`, then you will not be able to receive the `error` event. 354 | * 355 | * You can `close()` the resulting stream at any time, which will either try to 356 | * `cancel()` the pending promise or try to `close()` the underlying stream. 357 | * 358 | * ```php 359 | * $promise = startUploadStream($uri); 360 | * 361 | * $stream = React\Promise\Stream\unwrapWritable($promise); 362 | * 363 | * $loop->addTimer(2.0, function () use ($stream) { 364 | * $stream->close(); 365 | * }); 366 | * ``` 367 | * 368 | * @param PromiseInterface> $promise 369 | * @return WritableStreamInterface 370 | */ 371 | function unwrapWritable(PromiseInterface $promise) 372 | { 373 | return new UnwrapWritableStream($promise); 374 | } 375 | -------------------------------------------------------------------------------- /src/functions_include.php: -------------------------------------------------------------------------------- 1 |