├── src ├── functions_include.php ├── FiberInterface.php ├── FiberFactory.php ├── FiberMap.php ├── SimpleFiber.php └── functions.php ├── LICENSE ├── composer.json ├── CHANGELOG.md └── README.md /src/functions_include.php: -------------------------------------------------------------------------------- 1 | new SimpleFiber(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/FiberMap.php: -------------------------------------------------------------------------------- 1 | > */ 15 | private static array $map = []; 16 | 17 | /** 18 | * @param \Fiber $fiber 19 | * @param PromiseInterface $promise 20 | */ 21 | public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void 22 | { 23 | self::$map[\spl_object_id($fiber)] = $promise; 24 | } 25 | 26 | /** 27 | * @param \Fiber $fiber 28 | */ 29 | public static function unsetPromise(\Fiber $fiber): void 30 | { 31 | unset(self::$map[\spl_object_id($fiber)]); 32 | } 33 | 34 | /** 35 | * @param \Fiber $fiber 36 | * @return ?PromiseInterface 37 | */ 38 | public static function getPromise(\Fiber $fiber): ?PromiseInterface 39 | { 40 | return self::$map[\spl_object_id($fiber)] ?? null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react/async", 3 | "description": "Async utilities and fibers for ReactPHP", 4 | "keywords": ["async", "ReactPHP"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Christian Lück", 9 | "homepage": "https://clue.engineering/", 10 | "email": "christian@clue.engineering" 11 | }, 12 | { 13 | "name": "Cees-Jan Kiewiet", 14 | "homepage": "https://wyrihaximus.net/", 15 | "email": "reactphp@ceesjankiewiet.nl" 16 | }, 17 | { 18 | "name": "Jan Sorgalla", 19 | "homepage": "https://sorgalla.com/", 20 | "email": "jsorgalla@gmail.com" 21 | }, 22 | { 23 | "name": "Chris Boden", 24 | "homepage": "https://cboden.dev/", 25 | "email": "cboden@gmail.com" 26 | } 27 | ], 28 | "require": { 29 | "php": ">=8.1", 30 | "react/event-loop": "^1.2", 31 | "react/promise": "^3.2 || ^2.8 || ^1.2.1" 32 | }, 33 | "require-dev": { 34 | "phpstan/phpstan": "1.10.39", 35 | "phpunit/phpunit": "^9.6" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "React\\Async\\": "src/" 40 | }, 41 | "files": [ 42 | "src/functions_include.php" 43 | ] 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "React\\Tests\\Async\\": "tests/" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/SimpleFiber.php: -------------------------------------------------------------------------------- 1 | */ 13 | private static ?\Fiber $scheduler = null; 14 | 15 | private static ?\Closure $suspend = null; 16 | 17 | /** @var ?\Fiber */ 18 | private ?\Fiber $fiber = null; 19 | 20 | public function __construct() 21 | { 22 | $this->fiber = \Fiber::getCurrent(); 23 | } 24 | 25 | public function resume(mixed $value): void 26 | { 27 | if ($this->fiber !== null) { 28 | $this->fiber->resume($value); 29 | } else { 30 | self::$suspend = static fn() => $value; 31 | } 32 | 33 | if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) { 34 | $suspend = self::$suspend; 35 | self::$suspend = null; 36 | 37 | \Fiber::suspend($suspend); 38 | } 39 | } 40 | 41 | public function throw(\Throwable $throwable): void 42 | { 43 | if ($this->fiber !== null) { 44 | $this->fiber->throw($throwable); 45 | } else { 46 | self::$suspend = static fn() => throw $throwable; 47 | } 48 | 49 | if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) { 50 | $suspend = self::$suspend; 51 | self::$suspend = null; 52 | 53 | \Fiber::suspend($suspend); 54 | } 55 | } 56 | 57 | public function suspend(): mixed 58 | { 59 | if ($this->fiber === null) { 60 | if (self::$scheduler === null || self::$scheduler->isTerminated()) { 61 | self::$scheduler = new \Fiber(static fn() => Loop::run()); 62 | // Run event loop to completion on shutdown. 63 | \register_shutdown_function(static function (): void { 64 | assert(self::$scheduler instanceof \Fiber); 65 | if (self::$scheduler->isSuspended()) { 66 | self::$scheduler->resume(); 67 | } 68 | }); 69 | } 70 | 71 | $ret = (self::$scheduler->isStarted() ? self::$scheduler->resume() : self::$scheduler->start()); 72 | assert(\is_callable($ret)); 73 | 74 | return $ret(); 75 | } 76 | 77 | return \Fiber::suspend(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.3.0 (2024-06-04) 4 | 5 | * Feature: Improve performance by avoiding unneeded references in `FiberMap`. 6 | (#88 by @clue) 7 | 8 | * Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations. 9 | (#87 by @clue) 10 | 11 | * Improve type safety for test environment. 12 | (#86 by @SimonFrings) 13 | 14 | ## 4.2.0 (2023-11-22) 15 | 16 | * Feature: Add Promise v3 template types for all public functions. 17 | (#40 by @WyriHaximus and @clue) 18 | 19 | All our public APIs now use Promise v3 template types to guide IDEs and static 20 | analysis tools (like PHPStan), helping with proper type usage and improving 21 | code quality: 22 | 23 | ```php 24 | assertType('bool', await(resolve(true))); 25 | assertType('PromiseInterface', async(fn(): bool => true)()); 26 | assertType('PromiseInterface', coroutine(fn(): bool => true)); 27 | ``` 28 | 29 | * Feature: Full PHP 8.3 compatibility. 30 | (#81 by @clue) 31 | 32 | * Update test suite to avoid unhandled promise rejections. 33 | (#79 by @clue) 34 | 35 | ## 4.1.0 (2023-06-22) 36 | 37 | * Feature: Add new `delay()` function to delay program execution. 38 | (#69 and #78 by @clue) 39 | 40 | ```php 41 | echo 'a'; 42 | Loop::addTimer(1.0, function () { 43 | echo 'b'; 44 | }); 45 | React\Async\delay(3.0); 46 | echo 'c'; 47 | 48 | // prints "a" at t=0.0s 49 | // prints "b" at t=1.0s 50 | // prints "c" at t=3.0s 51 | ``` 52 | 53 | * Update test suite, add PHPStan with `max` level and report failed assertions. 54 | (#66 and #76 by @clue and #61 and #73 by @WyriHaximus) 55 | 56 | ## 4.0.0 (2022-07-11) 57 | 58 | A major new feature release, see [**release announcement**](https://clue.engineering/2022/announcing-reactphp-async). 59 | 60 | * We'd like to emphasize that this component is production ready and battle-tested. 61 | We plan to support all long-term support (LTS) releases for at least 24 months, 62 | so you have a rock-solid foundation to build on top of. 63 | 64 | * The v4 release will be the way forward for this package. However, we will still 65 | actively support v3 and v2 to provide a smooth upgrade path for those not yet 66 | on PHP 8.1+. If you're using an older PHP version, you may use either version 67 | which all provide a compatible API but may not take advantage of newer language 68 | features. You may target multiple versions at the same time to support a wider range of 69 | PHP versions: 70 | 71 | * [`4.x` branch](https://github.com/reactphp/async/tree/4.x) (PHP 8.1+) 72 | * [`3.x` branch](https://github.com/reactphp/async/tree/3.x) (PHP 7.1+) 73 | * [`2.x` branch](https://github.com/reactphp/async/tree/2.x) (PHP 5.3+) 74 | 75 | This update involves some major new features and a minor BC break over the 76 | `v3.0.0` release. We've tried hard to avoid BC breaks where possible and 77 | minimize impact otherwise. We expect that most consumers of this package will be 78 | affected by BC breaks, but updating should take no longer than a few minutes. 79 | See below for more details: 80 | 81 | * Feature / BC break: Require PHP 8.1+ and add `mixed` type declarations. 82 | (#14 by @clue) 83 | 84 | * Feature: Add Fiber-based `async()` and `await()` functions. 85 | (#15, #18, #19 and #20 by @WyriHaximus and #26, #28, #30, #32, #34, #55 and #57 by @clue) 86 | 87 | * Project maintenance, rename `main` branch to `4.x` and update installation instructions. 88 | (#29 by @clue) 89 | 90 | The following changes had to be ported to this release due to our branching 91 | strategy, but also appeared in the `v3.0.0` release: 92 | 93 | * Feature: Support iterable type for `parallel()` + `series()` + `waterfall()`. 94 | (#49 by @clue) 95 | 96 | * Feature: Forward compatibility with upcoming Promise v3. 97 | (#48 by @clue) 98 | 99 | * Minor documentation improvements. 100 | (#36 by @SimonFrings and #51 by @nhedger) 101 | 102 | ## 3.0.0 (2022-07-11) 103 | 104 | See [`3.x` CHANGELOG](https://github.com/reactphp/async/blob/3.x/CHANGELOG.md) for more details. 105 | 106 | ## 2.0.0 (2022-07-11) 107 | 108 | See [`2.x` CHANGELOG](https://github.com/reactphp/async/blob/2.x/CHANGELOG.md) for more details. 109 | 110 | ## 1.0.0 (2013-02-07) 111 | 112 | * First tagged release 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Async Utilities 2 | 3 | [![CI status](https://github.com/reactphp/async/workflows/CI/badge.svg)](https://github.com/reactphp/async/actions) 4 | [![installs on Packagist](https://img.shields.io/packagist/dt/react/async?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/async) 5 | 6 | Async utilities and fibers for [ReactPHP](https://reactphp.org/). 7 | 8 | This library allows you to manage async control flow. It provides a number of 9 | combinators for [Promise](https://github.com/reactphp/promise)-based APIs. 10 | Instead of nesting or chaining promise callbacks, you can declare them as a 11 | list, which is resolved sequentially in an async manner. 12 | React/Async will not automagically change blocking code to be async. You need 13 | to have an actual event loop and non-blocking libraries interacting with that 14 | event loop for it to work. As long as you have a Promise-based API that runs in 15 | an event loop, it can be used with this library. 16 | 17 | **Table of Contents** 18 | 19 | * [Usage](#usage) 20 | * [async()](#async) 21 | * [await()](#await) 22 | * [coroutine()](#coroutine) 23 | * [delay()](#delay) 24 | * [parallel()](#parallel) 25 | * [series()](#series) 26 | * [waterfall()](#waterfall) 27 | * [Todo](#todo) 28 | * [Install](#install) 29 | * [Tests](#tests) 30 | * [License](#license) 31 | 32 | ## Usage 33 | 34 | This lightweight library consists only of a few simple functions. 35 | All functions reside under the `React\Async` namespace. 36 | 37 | The below examples refer to all functions with their fully-qualified names like this: 38 | 39 | ```php 40 | React\Async\await(…); 41 | ``` 42 | 43 | As of PHP 5.6+ you can also import each required function into your code like this: 44 | 45 | ```php 46 | use function React\Async\await; 47 | 48 | await(…); 49 | ``` 50 | 51 | Alternatively, you can also use an import statement similar to this: 52 | 53 | ```php 54 | use React\Async; 55 | 56 | Async\await(…); 57 | ``` 58 | 59 | ### async() 60 | 61 | The `async(callable():(PromiseInterface|T) $function): (callable():PromiseInterface)` function can be used to 62 | return an async function for a function that uses [`await()`](#await) internally. 63 | 64 | This function is specifically designed to complement the [`await()` function](#await). 65 | The [`await()` function](#await) can be considered *blocking* from the 66 | perspective of the calling code. You can avoid this blocking behavior by 67 | wrapping it in an `async()` function call. Everything inside this function 68 | will still be blocked, but everything outside this function can be executed 69 | asynchronously without blocking: 70 | 71 | ```php 72 | Loop::addTimer(0.5, React\Async\async(function () { 73 | echo 'a'; 74 | React\Async\await(React\Promise\Timer\sleep(1.0)); 75 | echo 'c'; 76 | })); 77 | 78 | Loop::addTimer(1.0, function () { 79 | echo 'b'; 80 | }); 81 | 82 | // prints "a" at t=0.5s 83 | // prints "b" at t=1.0s 84 | // prints "c" at t=1.5s 85 | ``` 86 | 87 | See also the [`await()` function](#await) for more details. 88 | 89 | Note that this function only works in tandem with the [`await()` function](#await). 90 | In particular, this function does not "magically" make any blocking function 91 | non-blocking: 92 | 93 | ```php 94 | Loop::addTimer(0.5, React\Async\async(function () { 95 | echo 'a'; 96 | sleep(1); // broken: using PHP's blocking sleep() for demonstration purposes 97 | echo 'c'; 98 | })); 99 | 100 | Loop::addTimer(1.0, function () { 101 | echo 'b'; 102 | }); 103 | 104 | // prints "a" at t=0.5s 105 | // prints "c" at t=1.5s: Correct timing, but wrong order 106 | // prints "b" at t=1.5s: Triggered too late because it was blocked 107 | ``` 108 | 109 | As an alternative, you should always make sure to use this function in tandem 110 | with the [`await()` function](#await) and an async API returning a promise 111 | as shown in the previous example. 112 | 113 | The `async()` function is specifically designed for cases where it is used 114 | as a callback (such as an event loop timer, event listener, or promise 115 | callback). For this reason, it returns a new function wrapping the given 116 | `$function` instead of directly invoking it and returning its value. 117 | 118 | ```php 119 | use function React\Async\async; 120 | 121 | Loop::addTimer(1.0, async(function () { … })); 122 | $connection->on('close', async(function () { … })); 123 | $stream->on('data', async(function ($data) { … })); 124 | $promise->then(async(function (int $result) { … })); 125 | ``` 126 | 127 | You can invoke this wrapping function to invoke the given `$function` with 128 | any arguments given as-is. The function will always return a Promise which 129 | will be fulfilled with whatever your `$function` returns. Likewise, it will 130 | return a promise that will be rejected if you throw an `Exception` or 131 | `Throwable` from your `$function`. This allows you to easily create 132 | Promise-based functions: 133 | 134 | ```php 135 | $promise = React\Async\async(function (): int { 136 | $browser = new React\Http\Browser(); 137 | $urls = [ 138 | 'https://example.com/alice', 139 | 'https://example.com/bob' 140 | ]; 141 | 142 | $bytes = 0; 143 | foreach ($urls as $url) { 144 | $response = React\Async\await($browser->get($url)); 145 | assert($response instanceof Psr\Http\Message\ResponseInterface); 146 | $bytes += $response->getBody()->getSize(); 147 | } 148 | return $bytes; 149 | })(); 150 | 151 | $promise->then(function (int $bytes) { 152 | echo 'Total size: ' . $bytes . PHP_EOL; 153 | }, function (Exception $e) { 154 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 155 | }); 156 | ``` 157 | 158 | The previous example uses [`await()`](#await) inside a loop to highlight how 159 | this vastly simplifies consuming asynchronous operations. At the same time, 160 | this naive example does not leverage concurrent execution, as it will 161 | essentially "await" between each operation. In order to take advantage of 162 | concurrent execution within the given `$function`, you can "await" multiple 163 | promises by using a single [`await()`](#await) together with Promise-based 164 | primitives like this: 165 | 166 | ```php 167 | $promise = React\Async\async(function (): int { 168 | $browser = new React\Http\Browser(); 169 | $urls = [ 170 | 'https://example.com/alice', 171 | 'https://example.com/bob' 172 | ]; 173 | 174 | $promises = []; 175 | foreach ($urls as $url) { 176 | $promises[] = $browser->get($url); 177 | } 178 | 179 | try { 180 | $responses = React\Async\await(React\Promise\all($promises)); 181 | } catch (Exception $e) { 182 | foreach ($promises as $promise) { 183 | $promise->cancel(); 184 | } 185 | throw $e; 186 | } 187 | 188 | $bytes = 0; 189 | foreach ($responses as $response) { 190 | assert($response instanceof Psr\Http\Message\ResponseInterface); 191 | $bytes += $response->getBody()->getSize(); 192 | } 193 | return $bytes; 194 | })(); 195 | 196 | $promise->then(function (int $bytes) { 197 | echo 'Total size: ' . $bytes . PHP_EOL; 198 | }, function (Exception $e) { 199 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 200 | }); 201 | ``` 202 | 203 | The returned promise is implemented in such a way that it can be cancelled 204 | when it is still pending. Cancelling a pending promise will cancel any awaited 205 | promises inside that fiber or any nested fibers. As such, the following example 206 | will only output `ab` and cancel the pending [`delay()`](#delay). 207 | The [`await()`](#await) calls in this example would throw a `RuntimeException` 208 | from the cancelled [`delay()`](#delay) call that bubbles up through the fibers. 209 | 210 | ```php 211 | $promise = async(static function (): int { 212 | echo 'a'; 213 | await(async(static function (): void { 214 | echo 'b'; 215 | delay(2); 216 | echo 'c'; 217 | })()); 218 | echo 'd'; 219 | 220 | return time(); 221 | })(); 222 | 223 | $promise->cancel(); 224 | await($promise); 225 | ``` 226 | 227 | ### await() 228 | 229 | The `await(PromiseInterface $promise): T` function can be used to 230 | block waiting for the given `$promise` to be fulfilled. 231 | 232 | ```php 233 | $result = React\Async\await($promise); 234 | ``` 235 | 236 | This function will only return after the given `$promise` has settled, i.e. 237 | either fulfilled or rejected. While the promise is pending, this function 238 | can be considered *blocking* from the perspective of the calling code. 239 | You can avoid this blocking behavior by wrapping it in an [`async()` function](#async) 240 | call. Everything inside this function will still be blocked, but everything 241 | outside this function can be executed asynchronously without blocking: 242 | 243 | ```php 244 | Loop::addTimer(0.5, React\Async\async(function () { 245 | echo 'a'; 246 | React\Async\await(React\Promise\Timer\sleep(1.0)); 247 | echo 'c'; 248 | })); 249 | 250 | Loop::addTimer(1.0, function () { 251 | echo 'b'; 252 | }); 253 | 254 | // prints "a" at t=0.5s 255 | // prints "b" at t=1.0s 256 | // prints "c" at t=1.5s 257 | ``` 258 | 259 | See also the [`async()` function](#async) for more details. 260 | 261 | Once the promise is fulfilled, this function will return whatever the promise 262 | resolved to. 263 | 264 | Once the promise is rejected, this will throw whatever the promise rejected 265 | with. If the promise did not reject with an `Exception` or `Throwable`, then 266 | this function will throw an `UnexpectedValueException` instead. 267 | 268 | ```php 269 | try { 270 | $result = React\Async\await($promise); 271 | // promise successfully fulfilled with $result 272 | echo 'Result: ' . $result; 273 | } catch (Throwable $e) { 274 | // promise rejected with $e 275 | echo 'Error: ' . $e->getMessage(); 276 | } 277 | ``` 278 | 279 | ### coroutine() 280 | 281 | The `coroutine(callable(mixed ...$args):(\Generator|PromiseInterface|T) $function, mixed ...$args): PromiseInterface` function can be used to 282 | execute a Generator-based coroutine to "await" promises. 283 | 284 | ```php 285 | React\Async\coroutine(function () { 286 | $browser = new React\Http\Browser(); 287 | 288 | try { 289 | $response = yield $browser->get('https://example.com/'); 290 | assert($response instanceof Psr\Http\Message\ResponseInterface); 291 | echo $response->getBody(); 292 | } catch (Exception $e) { 293 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 294 | } 295 | }); 296 | ``` 297 | 298 | Using Generator-based coroutines is an alternative to directly using the 299 | underlying promise APIs. For many use cases, this makes using promise-based 300 | APIs much simpler, as it resembles a synchronous code flow more closely. 301 | The above example performs the equivalent of directly using the promise APIs: 302 | 303 | ```php 304 | $browser = new React\Http\Browser(); 305 | 306 | $browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { 307 | echo $response->getBody(); 308 | }, function (Exception $e) { 309 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 310 | }); 311 | ``` 312 | 313 | The `yield` keyword can be used to "await" a promise resolution. Internally, 314 | it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php). 315 | This allows the execution to be interrupted and resumed at the same place 316 | when the promise is fulfilled. The `yield` statement returns whatever the 317 | promise is fulfilled with. If the promise is rejected, it will throw an 318 | `Exception` or `Throwable`. 319 | 320 | The `coroutine()` function will always return a Promise which will be 321 | fulfilled with whatever your `$function` returns. Likewise, it will return 322 | a promise that will be rejected if you throw an `Exception` or `Throwable` 323 | from your `$function`. This allows you to easily create Promise-based 324 | functions: 325 | 326 | ```php 327 | $promise = React\Async\coroutine(function () { 328 | $browser = new React\Http\Browser(); 329 | $urls = [ 330 | 'https://example.com/alice', 331 | 'https://example.com/bob' 332 | ]; 333 | 334 | $bytes = 0; 335 | foreach ($urls as $url) { 336 | $response = yield $browser->get($url); 337 | assert($response instanceof Psr\Http\Message\ResponseInterface); 338 | $bytes += $response->getBody()->getSize(); 339 | } 340 | return $bytes; 341 | }); 342 | 343 | $promise->then(function (int $bytes) { 344 | echo 'Total size: ' . $bytes . PHP_EOL; 345 | }, function (Exception $e) { 346 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 347 | }); 348 | ``` 349 | 350 | The previous example uses a `yield` statement inside a loop to highlight how 351 | this vastly simplifies consuming asynchronous operations. At the same time, 352 | this naive example does not leverage concurrent execution, as it will 353 | essentially "await" between each operation. In order to take advantage of 354 | concurrent execution within the given `$function`, you can "await" multiple 355 | promises by using a single `yield` together with Promise-based primitives 356 | like this: 357 | 358 | ```php 359 | $promise = React\Async\coroutine(function () { 360 | $browser = new React\Http\Browser(); 361 | $urls = [ 362 | 'https://example.com/alice', 363 | 'https://example.com/bob' 364 | ]; 365 | 366 | $promises = []; 367 | foreach ($urls as $url) { 368 | $promises[] = $browser->get($url); 369 | } 370 | 371 | try { 372 | $responses = yield React\Promise\all($promises); 373 | } catch (Exception $e) { 374 | foreach ($promises as $promise) { 375 | $promise->cancel(); 376 | } 377 | throw $e; 378 | } 379 | 380 | $bytes = 0; 381 | foreach ($responses as $response) { 382 | assert($response instanceof Psr\Http\Message\ResponseInterface); 383 | $bytes += $response->getBody()->getSize(); 384 | } 385 | return $bytes; 386 | }); 387 | 388 | $promise->then(function (int $bytes) { 389 | echo 'Total size: ' . $bytes . PHP_EOL; 390 | }, function (Exception $e) { 391 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 392 | }); 393 | ``` 394 | 395 | ### delay() 396 | 397 | The `delay(float $seconds): void` function can be used to 398 | delay program execution for duration given in `$seconds`. 399 | 400 | ```php 401 | React\Async\delay($seconds); 402 | ``` 403 | 404 | This function will only return after the given number of `$seconds` have 405 | elapsed. If there are no other events attached to this loop, it will behave 406 | similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php). 407 | 408 | ```php 409 | echo 'a'; 410 | React\Async\delay(1.0); 411 | echo 'b'; 412 | 413 | // prints "a" at t=0.0s 414 | // prints "b" at t=1.0s 415 | ``` 416 | 417 | Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php), 418 | this function may not necessarily halt execution of the entire process thread. 419 | Instead, it allows the event loop to run any other events attached to the 420 | same loop until the delay returns: 421 | 422 | ```php 423 | echo 'a'; 424 | Loop::addTimer(1.0, function (): void { 425 | echo 'b'; 426 | }); 427 | React\Async\delay(3.0); 428 | echo 'c'; 429 | 430 | // prints "a" at t=0.0s 431 | // prints "b" at t=1.0s 432 | // prints "c" at t=3.0s 433 | ``` 434 | 435 | This behavior is especially useful if you want to delay the program execution 436 | of a particular routine, such as when building a simple polling or retry 437 | mechanism: 438 | 439 | ```php 440 | try { 441 | something(); 442 | } catch (Throwable) { 443 | // in case of error, retry after a short delay 444 | React\Async\delay(1.0); 445 | something(); 446 | } 447 | ``` 448 | 449 | Because this function only returns after some time has passed, it can be 450 | considered *blocking* from the perspective of the calling code. You can avoid 451 | this blocking behavior by wrapping it in an [`async()` function](#async) call. 452 | Everything inside this function will still be blocked, but everything outside 453 | this function can be executed asynchronously without blocking: 454 | 455 | ```php 456 | Loop::addTimer(0.5, React\Async\async(function (): void { 457 | echo 'a'; 458 | React\Async\delay(1.0); 459 | echo 'c'; 460 | })); 461 | 462 | Loop::addTimer(1.0, function (): void { 463 | echo 'b'; 464 | }); 465 | 466 | // prints "a" at t=0.5s 467 | // prints "b" at t=1.0s 468 | // prints "c" at t=1.5s 469 | ``` 470 | 471 | See also the [`async()` function](#async) for more details. 472 | 473 | Internally, the `$seconds` argument will be used as a timer for the loop so that 474 | it keeps running until this timer triggers. This implies that if you pass a 475 | really small (or negative) value, it will still start a timer and will thus 476 | trigger at the earliest possible time in the future. 477 | 478 | The function is implemented in such a way that it can be cancelled when it is 479 | running inside an [`async()` function](#async). Cancelling the resulting 480 | promise will clean up any pending timers and throw a `RuntimeException` from 481 | the pending delay which in turn would reject the resulting promise. 482 | 483 | ```php 484 | $promise = async(function (): void { 485 | echo 'a'; 486 | delay(3.0); 487 | echo 'b'; 488 | })(); 489 | 490 | Loop::addTimer(2.0, function () use ($promise): void { 491 | $promise->cancel(); 492 | }); 493 | 494 | // prints "a" at t=0.0s 495 | // rejects $promise at t=2.0 496 | // never prints "b" 497 | ``` 498 | 499 | ### parallel() 500 | 501 | The `parallel(iterable> $tasks): PromiseInterface>` function can be used 502 | like this: 503 | 504 | ```php 505 | then(function (array $results) { 533 | foreach ($results as $result) { 534 | var_dump($result); 535 | } 536 | }, function (Exception $e) { 537 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 538 | }); 539 | ``` 540 | 541 | ### series() 542 | 543 | The `series(iterable> $tasks): PromiseInterface>` function can be used 544 | like this: 545 | 546 | ```php 547 | then(function (array $results) { 575 | foreach ($results as $result) { 576 | var_dump($result); 577 | } 578 | }, function (Exception $e) { 579 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 580 | }); 581 | ``` 582 | 583 | ### waterfall() 584 | 585 | The `waterfall(iterable> $tasks): PromiseInterface` function can be used 586 | like this: 587 | 588 | ```php 589 | then(function ($prev) { 607 | echo "Final result is $prev\n"; 608 | }, function (Exception $e) { 609 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 610 | }); 611 | ``` 612 | 613 | ## Todo 614 | 615 | * Implement queue() 616 | 617 | ## Install 618 | 619 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 620 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 621 | 622 | This project follows [SemVer](https://semver.org/). 623 | This will install the latest supported version from this branch: 624 | 625 | ```bash 626 | composer require react/async:^4.3 627 | ``` 628 | 629 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 630 | 631 | This project aims to run on any platform and thus does not require any PHP 632 | extensions and supports running on PHP 8.1+. 633 | It's *highly recommended to use the latest supported PHP version* for this project. 634 | 635 | We're committed to providing long-term support (LTS) options and to provide a 636 | smooth upgrade path. If you're using an older PHP version, you may use the 637 | [`3.x` branch](https://github.com/reactphp/async/tree/3.x) (PHP 7.1+) or 638 | [`2.x` branch](https://github.com/reactphp/async/tree/2.x) (PHP 5.3+) which both 639 | provide a compatible API but do not take advantage of newer language features. 640 | You may target multiple versions at the same time to support a wider range of 641 | PHP versions like this: 642 | 643 | ```bash 644 | composer require "react/async:^4 || ^3 || ^2" 645 | ``` 646 | 647 | ## Tests 648 | 649 | To run the test suite, you first need to clone this repo and then install all 650 | dependencies [through Composer](https://getcomposer.org/): 651 | 652 | ```bash 653 | composer install 654 | ``` 655 | 656 | To run the test suite, go to the project root and run: 657 | 658 | ```bash 659 | vendor/bin/phpunit 660 | ``` 661 | 662 | On top of this, we use PHPStan on max level to ensure type safety across the project: 663 | 664 | ```bash 665 | vendor/bin/phpstan 666 | ``` 667 | 668 | ## License 669 | 670 | MIT, see [LICENSE file](LICENSE). 671 | 672 | This project is heavily influenced by [async.js](https://github.com/caolan/async). 673 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | on('close', async(function () { … })); 75 | * $stream->on('data', async(function ($data) { … })); 76 | * $promise->then(async(function (int $result) { … })); 77 | * ``` 78 | * 79 | * You can invoke this wrapping function to invoke the given `$function` with 80 | * any arguments given as-is. The function will always return a Promise which 81 | * will be fulfilled with whatever your `$function` returns. Likewise, it will 82 | * return a promise that will be rejected if you throw an `Exception` or 83 | * `Throwable` from your `$function`. This allows you to easily create 84 | * Promise-based functions: 85 | * 86 | * ```php 87 | * $promise = React\Async\async(function (): int { 88 | * $browser = new React\Http\Browser(); 89 | * $urls = [ 90 | * 'https://example.com/alice', 91 | * 'https://example.com/bob' 92 | * ]; 93 | * 94 | * $bytes = 0; 95 | * foreach ($urls as $url) { 96 | * $response = React\Async\await($browser->get($url)); 97 | * assert($response instanceof Psr\Http\Message\ResponseInterface); 98 | * $bytes += $response->getBody()->getSize(); 99 | * } 100 | * return $bytes; 101 | * })(); 102 | * 103 | * $promise->then(function (int $bytes) { 104 | * echo 'Total size: ' . $bytes . PHP_EOL; 105 | * }, function (Exception $e) { 106 | * echo 'Error: ' . $e->getMessage() . PHP_EOL; 107 | * }); 108 | * ``` 109 | * 110 | * The previous example uses [`await()`](#await) inside a loop to highlight how 111 | * this vastly simplifies consuming asynchronous operations. At the same time, 112 | * this naive example does not leverage concurrent execution, as it will 113 | * essentially "await" between each operation. In order to take advantage of 114 | * concurrent execution within the given `$function`, you can "await" multiple 115 | * promises by using a single [`await()`](#await) together with Promise-based 116 | * primitives like this: 117 | * 118 | * ```php 119 | * $promise = React\Async\async(function (): int { 120 | * $browser = new React\Http\Browser(); 121 | * $urls = [ 122 | * 'https://example.com/alice', 123 | * 'https://example.com/bob' 124 | * ]; 125 | * 126 | * $promises = []; 127 | * foreach ($urls as $url) { 128 | * $promises[] = $browser->get($url); 129 | * } 130 | * 131 | * try { 132 | * $responses = React\Async\await(React\Promise\all($promises)); 133 | * } catch (Exception $e) { 134 | * foreach ($promises as $promise) { 135 | * $promise->cancel(); 136 | * } 137 | * throw $e; 138 | * } 139 | * 140 | * $bytes = 0; 141 | * foreach ($responses as $response) { 142 | * assert($response instanceof Psr\Http\Message\ResponseInterface); 143 | * $bytes += $response->getBody()->getSize(); 144 | * } 145 | * return $bytes; 146 | * })(); 147 | * 148 | * $promise->then(function (int $bytes) { 149 | * echo 'Total size: ' . $bytes . PHP_EOL; 150 | * }, function (Exception $e) { 151 | * echo 'Error: ' . $e->getMessage() . PHP_EOL; 152 | * }); 153 | * ``` 154 | * 155 | * The returned promise is implemented in such a way that it can be cancelled 156 | * when it is still pending. Cancelling a pending promise will cancel any awaited 157 | * promises inside that fiber or any nested fibers. As such, the following example 158 | * will only output `ab` and cancel the pending [`delay()`](#delay). 159 | * The [`await()`](#await) calls in this example would throw a `RuntimeException` 160 | * from the cancelled [`delay()`](#delay) call that bubbles up through the fibers. 161 | * 162 | * ```php 163 | * $promise = async(static function (): int { 164 | * echo 'a'; 165 | * await(async(static function (): void { 166 | * echo 'b'; 167 | * delay(2); 168 | * echo 'c'; 169 | * })()); 170 | * echo 'd'; 171 | * 172 | * return time(); 173 | * })(); 174 | * 175 | * $promise->cancel(); 176 | * await($promise); 177 | * ``` 178 | * 179 | * @template T 180 | * @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214) 181 | * @template A2 182 | * @template A3 183 | * @template A4 184 | * @template A5 185 | * @param callable(A1,A2,A3,A4,A5): (PromiseInterface|T) $function 186 | * @return callable(A1=,A2=,A3=,A4=,A5=): PromiseInterface 187 | * @since 4.0.0 188 | * @see coroutine() 189 | */ 190 | function async(callable $function): callable 191 | { 192 | return static function (mixed ...$args) use ($function): PromiseInterface { 193 | $fiber = null; 194 | /** @var PromiseInterface $promise*/ 195 | $promise = new Promise(function (callable $resolve, callable $reject) use ($function, $args, &$fiber): void { 196 | $fiber = new \Fiber(function () use ($resolve, $reject, $function, $args, &$fiber): void { 197 | try { 198 | $resolve($function(...$args)); 199 | } catch (\Throwable $exception) { 200 | $reject($exception); 201 | } finally { 202 | assert($fiber instanceof \Fiber); 203 | FiberMap::unsetPromise($fiber); 204 | } 205 | }); 206 | 207 | $fiber->start(); 208 | }, function () use (&$fiber): void { 209 | assert($fiber instanceof \Fiber); 210 | $promise = FiberMap::getPromise($fiber); 211 | if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { 212 | $promise->cancel(); 213 | } 214 | }); 215 | 216 | $lowLevelFiber = \Fiber::getCurrent(); 217 | if ($lowLevelFiber !== null) { 218 | FiberMap::setPromise($lowLevelFiber, $promise); 219 | } 220 | 221 | return $promise; 222 | }; 223 | } 224 | 225 | /** 226 | * Block waiting for the given `$promise` to be fulfilled. 227 | * 228 | * ```php 229 | * $result = React\Async\await($promise); 230 | * ``` 231 | * 232 | * This function will only return after the given `$promise` has settled, i.e. 233 | * either fulfilled or rejected. While the promise is pending, this function 234 | * can be considered *blocking* from the perspective of the calling code. 235 | * You can avoid this blocking behavior by wrapping it in an [`async()` function](#async) 236 | * call. Everything inside this function will still be blocked, but everything 237 | * outside this function can be executed asynchronously without blocking: 238 | * 239 | * ```php 240 | * Loop::addTimer(0.5, React\Async\async(function () { 241 | * echo 'a'; 242 | * React\Async\await(React\Promise\Timer\sleep(1.0)); 243 | * echo 'c'; 244 | * })); 245 | * 246 | * Loop::addTimer(1.0, function () { 247 | * echo 'b'; 248 | * }); 249 | * 250 | * // prints "a" at t=0.5s 251 | * // prints "b" at t=1.0s 252 | * // prints "c" at t=1.5s 253 | * ``` 254 | * 255 | * See also the [`async()` function](#async) for more details. 256 | * 257 | * Once the promise is fulfilled, this function will return whatever the promise 258 | * resolved to. 259 | * 260 | * Once the promise is rejected, this will throw whatever the promise rejected 261 | * with. If the promise did not reject with an `Exception` or `Throwable`, then 262 | * this function will throw an `UnexpectedValueException` instead. 263 | * 264 | * ```php 265 | * try { 266 | * $result = React\Async\await($promise); 267 | * // promise successfully fulfilled with $result 268 | * echo 'Result: ' . $result; 269 | * } catch (Throwable $e) { 270 | * // promise rejected with $e 271 | * echo 'Error: ' . $e->getMessage(); 272 | * } 273 | * ``` 274 | * 275 | * @template T 276 | * @param PromiseInterface $promise 277 | * @return T returns whatever the promise resolves to 278 | * @throws \Exception when the promise is rejected with an `Exception` 279 | * @throws \Throwable when the promise is rejected with a `Throwable` 280 | * @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only) 281 | */ 282 | function await(PromiseInterface $promise): mixed 283 | { 284 | $fiber = null; 285 | $resolved = false; 286 | $rejected = false; 287 | 288 | /** @var T $resolvedValue */ 289 | $resolvedValue = null; 290 | $rejectedThrowable = null; 291 | $lowLevelFiber = \Fiber::getCurrent(); 292 | 293 | $promise->then( 294 | function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFiber): void { 295 | if ($lowLevelFiber !== null) { 296 | FiberMap::unsetPromise($lowLevelFiber); 297 | } 298 | 299 | /** @var ?\Fiber $fiber */ 300 | if ($fiber === null) { 301 | $resolved = true; 302 | /** @var T $resolvedValue */ 303 | $resolvedValue = $value; 304 | return; 305 | } 306 | 307 | $fiber->resume($value); 308 | }, 309 | function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowLevelFiber): void { 310 | if ($lowLevelFiber !== null) { 311 | FiberMap::unsetPromise($lowLevelFiber); 312 | } 313 | 314 | if (!$throwable instanceof \Throwable) { 315 | $throwable = new \UnexpectedValueException( 316 | 'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable)) /** @phpstan-ignore-line */ 317 | ); 318 | 319 | // avoid garbage references by replacing all closures in call stack. 320 | // what a lovely piece of code! 321 | $r = new \ReflectionProperty('Exception', 'trace'); 322 | $trace = $r->getValue($throwable); 323 | assert(\is_array($trace)); 324 | 325 | // Exception trace arguments only available when zend.exception_ignore_args is not set 326 | // @codeCoverageIgnoreStart 327 | foreach ($trace as $ti => $one) { 328 | if (isset($one['args'])) { 329 | foreach ($one['args'] as $ai => $arg) { 330 | if ($arg instanceof \Closure) { 331 | $trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')'; 332 | } 333 | } 334 | } 335 | } 336 | // @codeCoverageIgnoreEnd 337 | $r->setValue($throwable, $trace); 338 | } 339 | 340 | if ($fiber === null) { 341 | $rejected = true; 342 | $rejectedThrowable = $throwable; 343 | return; 344 | } 345 | 346 | $fiber->throw($throwable); 347 | } 348 | ); 349 | 350 | if ($resolved) { 351 | return $resolvedValue; 352 | } 353 | 354 | if ($rejected) { 355 | assert($rejectedThrowable instanceof \Throwable); 356 | throw $rejectedThrowable; 357 | } 358 | 359 | if ($lowLevelFiber !== null) { 360 | FiberMap::setPromise($lowLevelFiber, $promise); 361 | } 362 | 363 | $fiber = FiberFactory::create(); 364 | 365 | return $fiber->suspend(); 366 | } 367 | 368 | /** 369 | * Delay program execution for duration given in `$seconds`. 370 | * 371 | * ```php 372 | * React\Async\delay($seconds); 373 | * ``` 374 | * 375 | * This function will only return after the given number of `$seconds` have 376 | * elapsed. If there are no other events attached to this loop, it will behave 377 | * similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php). 378 | * 379 | * ```php 380 | * echo 'a'; 381 | * React\Async\delay(1.0); 382 | * echo 'b'; 383 | * 384 | * // prints "a" at t=0.0s 385 | * // prints "b" at t=1.0s 386 | * ``` 387 | * 388 | * Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php), 389 | * this function may not necessarily halt execution of the entire process thread. 390 | * Instead, it allows the event loop to run any other events attached to the 391 | * same loop until the delay returns: 392 | * 393 | * ```php 394 | * echo 'a'; 395 | * Loop::addTimer(1.0, function (): void { 396 | * echo 'b'; 397 | * }); 398 | * React\Async\delay(3.0); 399 | * echo 'c'; 400 | * 401 | * // prints "a" at t=0.0s 402 | * // prints "b" at t=1.0s 403 | * // prints "c" at t=3.0s 404 | * ``` 405 | * 406 | * This behavior is especially useful if you want to delay the program execution 407 | * of a particular routine, such as when building a simple polling or retry 408 | * mechanism: 409 | * 410 | * ```php 411 | * try { 412 | * something(); 413 | * } catch (Throwable) { 414 | * // in case of error, retry after a short delay 415 | * React\Async\delay(1.0); 416 | * something(); 417 | * } 418 | * ``` 419 | * 420 | * Because this function only returns after some time has passed, it can be 421 | * considered *blocking* from the perspective of the calling code. You can avoid 422 | * this blocking behavior by wrapping it in an [`async()` function](#async) call. 423 | * Everything inside this function will still be blocked, but everything outside 424 | * this function can be executed asynchronously without blocking: 425 | * 426 | * ```php 427 | * Loop::addTimer(0.5, React\Async\async(function (): void { 428 | * echo 'a'; 429 | * React\Async\delay(1.0); 430 | * echo 'c'; 431 | * })); 432 | * 433 | * Loop::addTimer(1.0, function (): void { 434 | * echo 'b'; 435 | * }); 436 | * 437 | * // prints "a" at t=0.5s 438 | * // prints "b" at t=1.0s 439 | * // prints "c" at t=1.5s 440 | * ``` 441 | * 442 | * See also the [`async()` function](#async) for more details. 443 | * 444 | * Internally, the `$seconds` argument will be used as a timer for the loop so that 445 | * it keeps running until this timer triggers. This implies that if you pass a 446 | * really small (or negative) value, it will still start a timer and will thus 447 | * trigger at the earliest possible time in the future. 448 | * 449 | * The function is implemented in such a way that it can be cancelled when it is 450 | * running inside an [`async()` function](#async). Cancelling the resulting 451 | * promise will clean up any pending timers and throw a `RuntimeException` from 452 | * the pending delay which in turn would reject the resulting promise. 453 | * 454 | * ```php 455 | * $promise = async(function (): void { 456 | * echo 'a'; 457 | * delay(3.0); 458 | * echo 'b'; 459 | * })(); 460 | * 461 | * Loop::addTimer(2.0, function () use ($promise): void { 462 | * $promise->cancel(); 463 | * }); 464 | * 465 | * // prints "a" at t=0.0s 466 | * // rejects $promise at t=2.0 467 | * // never prints "b" 468 | * ``` 469 | * 470 | * @return void 471 | * @throws \RuntimeException when the function is cancelled inside an `async()` function 472 | * @see async() 473 | * @uses await() 474 | */ 475 | function delay(float $seconds): void 476 | { 477 | /** @var ?TimerInterface $timer */ 478 | $timer = null; 479 | 480 | await(new Promise(function (callable $resolve) use ($seconds, &$timer): void { 481 | $timer = Loop::addTimer($seconds, fn() => $resolve(null)); 482 | }, function () use (&$timer): void { 483 | assert($timer instanceof TimerInterface); 484 | Loop::cancelTimer($timer); 485 | throw new \RuntimeException('Delay cancelled'); 486 | })); 487 | } 488 | 489 | /** 490 | * Execute a Generator-based coroutine to "await" promises. 491 | * 492 | * ```php 493 | * React\Async\coroutine(function () { 494 | * $browser = new React\Http\Browser(); 495 | * 496 | * try { 497 | * $response = yield $browser->get('https://example.com/'); 498 | * assert($response instanceof Psr\Http\Message\ResponseInterface); 499 | * echo $response->getBody(); 500 | * } catch (Exception $e) { 501 | * echo 'Error: ' . $e->getMessage() . PHP_EOL; 502 | * } 503 | * }); 504 | * ``` 505 | * 506 | * Using Generator-based coroutines is an alternative to directly using the 507 | * underlying promise APIs. For many use cases, this makes using promise-based 508 | * APIs much simpler, as it resembles a synchronous code flow more closely. 509 | * The above example performs the equivalent of directly using the promise APIs: 510 | * 511 | * ```php 512 | * $browser = new React\Http\Browser(); 513 | * 514 | * $browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { 515 | * echo $response->getBody(); 516 | * }, function (Exception $e) { 517 | * echo 'Error: ' . $e->getMessage() . PHP_EOL; 518 | * }); 519 | * ``` 520 | * 521 | * The `yield` keyword can be used to "await" a promise resolution. Internally, 522 | * it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php). 523 | * This allows the execution to be interrupted and resumed at the same place 524 | * when the promise is fulfilled. The `yield` statement returns whatever the 525 | * promise is fulfilled with. If the promise is rejected, it will throw an 526 | * `Exception` or `Throwable`. 527 | * 528 | * The `coroutine()` function will always return a Promise which will be 529 | * fulfilled with whatever your `$function` returns. Likewise, it will return 530 | * a promise that will be rejected if you throw an `Exception` or `Throwable` 531 | * from your `$function`. This allows you to easily create Promise-based 532 | * functions: 533 | * 534 | * ```php 535 | * $promise = React\Async\coroutine(function () { 536 | * $browser = new React\Http\Browser(); 537 | * $urls = [ 538 | * 'https://example.com/alice', 539 | * 'https://example.com/bob' 540 | * ]; 541 | * 542 | * $bytes = 0; 543 | * foreach ($urls as $url) { 544 | * $response = yield $browser->get($url); 545 | * assert($response instanceof Psr\Http\Message\ResponseInterface); 546 | * $bytes += $response->getBody()->getSize(); 547 | * } 548 | * return $bytes; 549 | * }); 550 | * 551 | * $promise->then(function (int $bytes) { 552 | * echo 'Total size: ' . $bytes . PHP_EOL; 553 | * }, function (Exception $e) { 554 | * echo 'Error: ' . $e->getMessage() . PHP_EOL; 555 | * }); 556 | * ``` 557 | * 558 | * The previous example uses a `yield` statement inside a loop to highlight how 559 | * this vastly simplifies consuming asynchronous operations. At the same time, 560 | * this naive example does not leverage concurrent execution, as it will 561 | * essentially "await" between each operation. In order to take advantage of 562 | * concurrent execution within the given `$function`, you can "await" multiple 563 | * promises by using a single `yield` together with Promise-based primitives 564 | * like this: 565 | * 566 | * ```php 567 | * $promise = React\Async\coroutine(function () { 568 | * $browser = new React\Http\Browser(); 569 | * $urls = [ 570 | * 'https://example.com/alice', 571 | * 'https://example.com/bob' 572 | * ]; 573 | * 574 | * $promises = []; 575 | * foreach ($urls as $url) { 576 | * $promises[] = $browser->get($url); 577 | * } 578 | * 579 | * try { 580 | * $responses = yield React\Promise\all($promises); 581 | * } catch (Exception $e) { 582 | * foreach ($promises as $promise) { 583 | * $promise->cancel(); 584 | * } 585 | * throw $e; 586 | * } 587 | * 588 | * $bytes = 0; 589 | * foreach ($responses as $response) { 590 | * assert($response instanceof Psr\Http\Message\ResponseInterface); 591 | * $bytes += $response->getBody()->getSize(); 592 | * } 593 | * return $bytes; 594 | * }); 595 | * 596 | * $promise->then(function (int $bytes) { 597 | * echo 'Total size: ' . $bytes . PHP_EOL; 598 | * }, function (Exception $e) { 599 | * echo 'Error: ' . $e->getMessage() . PHP_EOL; 600 | * }); 601 | * ``` 602 | * 603 | * @template T 604 | * @template TYield 605 | * @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214) 606 | * @template A2 607 | * @template A3 608 | * @template A4 609 | * @template A5 610 | * @param callable(A1, A2, A3, A4, A5):(\Generator, TYield, PromiseInterface|T>|PromiseInterface|T) $function 611 | * @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is 612 | * @return PromiseInterface 613 | * @since 3.0.0 614 | */ 615 | function coroutine(callable $function, mixed ...$args): PromiseInterface 616 | { 617 | try { 618 | $generator = $function(...$args); 619 | } catch (\Throwable $e) { 620 | return reject($e); 621 | } 622 | 623 | if (!$generator instanceof \Generator) { 624 | return resolve($generator); 625 | } 626 | 627 | $promise = null; 628 | /** @var Deferred $deferred*/ 629 | $deferred = new Deferred(function () use (&$promise) { 630 | /** @var ?PromiseInterface $promise */ 631 | if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { 632 | $promise->cancel(); 633 | } 634 | $promise = null; 635 | }); 636 | 637 | /** @var callable $next */ 638 | $next = function () use ($deferred, $generator, &$next, &$promise) { 639 | try { 640 | if (!$generator->valid()) { 641 | $next = null; 642 | $deferred->resolve($generator->getReturn()); 643 | return; 644 | } 645 | } catch (\Throwable $e) { 646 | $next = null; 647 | $deferred->reject($e); 648 | return; 649 | } 650 | 651 | $promise = $generator->current(); 652 | if (!$promise instanceof PromiseInterface) { 653 | $next = null; 654 | $deferred->reject(new \UnexpectedValueException( 655 | 'Expected coroutine to yield ' . PromiseInterface::class . ', but got ' . (is_object($promise) ? get_class($promise) : gettype($promise)) 656 | )); 657 | return; 658 | } 659 | 660 | /** @var PromiseInterface $promise */ 661 | assert($next instanceof \Closure); 662 | $promise->then(function ($value) use ($generator, $next) { 663 | $generator->send($value); 664 | $next(); 665 | }, function (\Throwable $reason) use ($generator, $next) { 666 | $generator->throw($reason); 667 | $next(); 668 | })->then(null, function (\Throwable $reason) use ($deferred, &$next) { 669 | $next = null; 670 | $deferred->reject($reason); 671 | }); 672 | }; 673 | $next(); 674 | 675 | return $deferred->promise(); 676 | } 677 | 678 | /** 679 | * @template T 680 | * @param iterable|T)> $tasks 681 | * @return PromiseInterface> 682 | */ 683 | function parallel(iterable $tasks): PromiseInterface 684 | { 685 | /** @var array> $pending */ 686 | $pending = []; 687 | /** @var Deferred> $deferred */ 688 | $deferred = new Deferred(function () use (&$pending) { 689 | foreach ($pending as $promise) { 690 | if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { 691 | $promise->cancel(); 692 | } 693 | } 694 | $pending = []; 695 | }); 696 | $results = []; 697 | $continue = true; 698 | 699 | $taskErrback = function ($error) use (&$pending, $deferred, &$continue) { 700 | $continue = false; 701 | $deferred->reject($error); 702 | 703 | foreach ($pending as $promise) { 704 | if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { 705 | $promise->cancel(); 706 | } 707 | } 708 | $pending = []; 709 | }; 710 | 711 | foreach ($tasks as $i => $task) { 712 | $taskCallback = function ($result) use (&$results, &$pending, &$continue, $i, $deferred) { 713 | $results[$i] = $result; 714 | unset($pending[$i]); 715 | 716 | if (!$pending && !$continue) { 717 | $deferred->resolve($results); 718 | } 719 | }; 720 | 721 | $promise = \call_user_func($task); 722 | assert($promise instanceof PromiseInterface); 723 | $pending[$i] = $promise; 724 | 725 | $promise->then($taskCallback, $taskErrback); 726 | 727 | if (!$continue) { 728 | break; 729 | } 730 | } 731 | 732 | $continue = false; 733 | if (!$pending) { 734 | $deferred->resolve($results); 735 | } 736 | 737 | /** @var PromiseInterface> Remove once defining `Deferred()` above is supported by PHPStan, see https://github.com/phpstan/phpstan/issues/11032 */ 738 | return $deferred->promise(); 739 | } 740 | 741 | /** 742 | * @template T 743 | * @param iterable|T)> $tasks 744 | * @return PromiseInterface> 745 | */ 746 | function series(iterable $tasks): PromiseInterface 747 | { 748 | $pending = null; 749 | /** @var Deferred> $deferred */ 750 | $deferred = new Deferred(function () use (&$pending) { 751 | /** @var ?PromiseInterface $pending */ 752 | if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) { 753 | $pending->cancel(); 754 | } 755 | $pending = null; 756 | }); 757 | $results = []; 758 | 759 | if ($tasks instanceof \IteratorAggregate) { 760 | $tasks = $tasks->getIterator(); 761 | assert($tasks instanceof \Iterator); 762 | } 763 | 764 | $taskCallback = function ($result) use (&$results, &$next) { 765 | $results[] = $result; 766 | /** @var \Closure $next */ 767 | $next(); 768 | }; 769 | 770 | $next = function () use (&$tasks, $taskCallback, $deferred, &$results, &$pending) { 771 | if ($tasks instanceof \Iterator ? !$tasks->valid() : !$tasks) { 772 | $deferred->resolve($results); 773 | return; 774 | } 775 | 776 | if ($tasks instanceof \Iterator) { 777 | $task = $tasks->current(); 778 | $tasks->next(); 779 | } else { 780 | assert(\is_array($tasks)); 781 | $task = \array_shift($tasks); 782 | } 783 | 784 | assert(\is_callable($task)); 785 | $promise = \call_user_func($task); 786 | assert($promise instanceof PromiseInterface); 787 | $pending = $promise; 788 | 789 | $promise->then($taskCallback, array($deferred, 'reject')); 790 | }; 791 | 792 | $next(); 793 | 794 | /** @var PromiseInterface> Remove once defining `Deferred()` above is supported by PHPStan, see https://github.com/phpstan/phpstan/issues/11032 */ 795 | return $deferred->promise(); 796 | } 797 | 798 | /** 799 | * @template T 800 | * @param iterable<(callable():(PromiseInterface|T))|(callable(mixed):(PromiseInterface|T))> $tasks 801 | * @return PromiseInterface<($tasks is non-empty-array|\Traversable ? T : null)> 802 | */ 803 | function waterfall(iterable $tasks): PromiseInterface 804 | { 805 | $pending = null; 806 | /** @var Deferred $deferred*/ 807 | $deferred = new Deferred(function () use (&$pending) { 808 | /** @var ?PromiseInterface $pending */ 809 | if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) { 810 | $pending->cancel(); 811 | } 812 | $pending = null; 813 | }); 814 | 815 | if ($tasks instanceof \IteratorAggregate) { 816 | $tasks = $tasks->getIterator(); 817 | assert($tasks instanceof \Iterator); 818 | } 819 | 820 | /** @var callable $next */ 821 | $next = function ($value = null) use (&$tasks, &$next, $deferred, &$pending) { 822 | if ($tasks instanceof \Iterator ? !$tasks->valid() : !$tasks) { 823 | $deferred->resolve($value); 824 | return; 825 | } 826 | 827 | if ($tasks instanceof \Iterator) { 828 | $task = $tasks->current(); 829 | $tasks->next(); 830 | } else { 831 | assert(\is_array($tasks)); 832 | $task = \array_shift($tasks); 833 | } 834 | 835 | assert(\is_callable($task)); 836 | $promise = \call_user_func_array($task, func_get_args()); 837 | assert($promise instanceof PromiseInterface); 838 | $pending = $promise; 839 | 840 | $promise->then($next, array($deferred, 'reject')); 841 | }; 842 | 843 | $next(); 844 | 845 | return $deferred->promise(); 846 | } 847 | --------------------------------------------------------------------------------