├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Deferred.php ├── Exception ├── CompositeException.php └── LengthException.php ├── Internal ├── CancellationQueue.php ├── FulfilledPromise.php └── RejectedPromise.php ├── Promise.php ├── PromiseInterface.php ├── functions.php └── functions_include.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.2.0 (2024-05-24) 4 | 5 | * Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations. 6 | (#260 by @Ayesh) 7 | 8 | * Feature: Include previous exceptions when reporting unhandled promise rejections. 9 | (#262 by @clue) 10 | 11 | * Update test suite to improve PHP 8.4+ support. 12 | (#261 by @SimonFrings) 13 | 14 | ## 3.1.0 (2023-11-16) 15 | 16 | * Feature: Full PHP 8.3 compatibility. 17 | (#255 by @clue) 18 | 19 | * Feature: Describe all callable arguments with types for `Promise` and `Deferred`. 20 | (#253 by @clue) 21 | 22 | * Update test suite and minor documentation improvements. 23 | (#251 by @ondrejmirtes and #250 by @SQKo) 24 | 25 | ## 3.0.0 (2023-07-11) 26 | 27 | A major new feature release, see [**release announcement**](https://clue.engineering/2023/announcing-reactphp-promise-v3). 28 | 29 | * We'd like to emphasize that this component is production ready and battle-tested. 30 | We plan to support all long-term support (LTS) releases for at least 24 months, 31 | so you have a rock-solid foundation to build on top of. 32 | 33 | * The v3 release will be the way forward for this package. However, we will still 34 | actively support v2 and v1 to provide a smooth upgrade path for those not yet 35 | on the latest versions. 36 | 37 | This update involves some major new features and a minor BC break over the 38 | `v2.0.0` release. We've tried hard to avoid BC breaks where possible and 39 | minimize impact otherwise. We expect that most consumers of this package will be 40 | affected by BC breaks, but updating should take no longer than a few minutes. 41 | See below for more details: 42 | 43 | * BC break: PHP 8.1+ recommended, PHP 7.1+ required. 44 | (#138 and #149 by @WyriHaximus) 45 | 46 | * Feature / BC break: The `PromiseInterface` now includes the functionality of the old ~~`ExtendedPromiseInterface`~~ and ~~`CancellablePromiseInterface`~~. 47 | Each promise now always includes the `then()`, `catch()`, `finally()` and `cancel()` methods. 48 | The new `catch()` and `finally()` methods replace the deprecated ~~`otherwise()`~~ and ~~`always()`~~ methods which continue to exist for BC reasons. 49 | The old ~~`ExtendedPromiseInterface`~~ and ~~`CancellablePromiseInterface`~~ are no longer needed and have been removed as a consequence. 50 | (#75 by @jsor and #208 by @clue and @WyriHaximus) 51 | 52 | ```php 53 | // old (multiple interfaces may or may not be implemented) 54 | assert($promise instanceof PromiseInterface); 55 | assert(method_exists($promise, 'then')); 56 | if ($promise instanceof ExtendedPromiseInterface) { assert(method_exists($promise, 'otherwise')); } 57 | if ($promise instanceof ExtendedPromiseInterface) { assert(method_exists($promise, 'always')); } 58 | if ($promise instanceof CancellablePromiseInterface) { assert(method_exists($promise, 'cancel')); } 59 | 60 | // new (single PromiseInterface with all methods) 61 | assert($promise instanceof PromiseInterface); 62 | assert(method_exists($promise, 'then')); 63 | assert(method_exists($promise, 'catch')); 64 | assert(method_exists($promise, 'finally')); 65 | assert(method_exists($promise, 'cancel')); 66 | ``` 67 | 68 | * Feature / BC break: Improve type safety of promises. Require `mixed` fulfillment value argument and `Throwable` (or `Exception`) as rejection reason. 69 | Add PHPStan template types to ensure strict types for `resolve(T $value): PromiseInterface` and `reject(Throwable $reason): PromiseInterface`. 70 | It is no longer possible to resolve a promise without a value (use `null` instead) or reject a promise without a reason (use `Throwable` instead). 71 | (#93, #141 and #142 by @jsor, #138, #149 and #247 by @WyriHaximus and #213 and #246 by @clue) 72 | 73 | ```php 74 | // old (arguments used to be optional) 75 | $promise = resolve(); 76 | $promise = reject(); 77 | 78 | // new (already supported before) 79 | $promise = resolve(null); 80 | $promise = reject(new RuntimeException()); 81 | ``` 82 | 83 | * Feature / BC break: Report all unhandled rejections by default and remove ~~`done()`~~ method. 84 | Add new `set_rejection_handler()` function to set the global rejection handler for unhandled promise rejections. 85 | (#248, #249 and #224 by @clue) 86 | 87 | ```php 88 | // Unhandled promise rejection with RuntimeException: Unhandled in example.php:2 89 | reject(new RuntimeException('Unhandled')); 90 | ``` 91 | 92 | * BC break: Remove all deprecated APIs and reduce API surface. 93 | Remove ~~`some()`~~, ~~`map()`~~, ~~`reduce()`~~ functions, use `any()` and `all()` functions instead. 94 | Remove internal ~~`FulfilledPromise`~~ and ~~`RejectedPromise`~~ classes, use `resolve()` and `reject()` functions instead. 95 | Remove legacy promise progress API (deprecated third argument to `then()` method) and deprecated ~~`LazyPromise`~~ class. 96 | (#32 and #98 by @jsor and #164, #219 and #220 by @clue) 97 | 98 | * BC break: Make all classes final to encourage composition over inheritance. 99 | (#80 by @jsor) 100 | 101 | * Feature / BC break: Require `array` (or `iterable`) type for `all()` + `race()` + `any()` functions and bring in line with ES6 specification. 102 | These functions now require a single argument with a variable number of promises or values as input. 103 | (#225 by @clue and #35 by @jsor) 104 | 105 | * Fix / BC break: Fix `race()` to return a forever pending promise when called with an empty `array` (or `iterable`) and bring in line with ES6 specification. 106 | (#83 by @jsor and #225 by @clue) 107 | 108 | * Minor performance improvements by initializing `Deferred` in the constructor and avoiding `call_user_func()` calls. 109 | (#151 by @WyriHaximus and #171 by @Kubo2) 110 | 111 | * Minor documentation improvements. 112 | (#110 by @seregazhuk, #132 by @CharlotteDunois, #145 by @danielecr, #178 by @WyriHaximus, #189 by @srdante, #212 by @clue, #214, #239 and #243 by @SimonFrings and #231 by @nhedger) 113 | 114 | The following changes had to be ported to this release due to our branching 115 | strategy, but also appeared in the [`2.x` branch](https://github.com/reactphp/promise/tree/2.x): 116 | 117 | * Feature: Support union types and address deprecation of `ReflectionType::getClass()` (PHP 8+). 118 | (#197 by @cdosoftei and @SimonFrings) 119 | 120 | * Feature: Support intersection types (PHP 8.1+). 121 | (#209 by @bzikarsky) 122 | 123 | * Feature: Support DNS types (PHP 8.2+). 124 | (#236 by @nhedger) 125 | 126 | * Feature: Port all memory improvements from `2.x` to `3.x`. 127 | (#150 by @clue and @WyriHaximus) 128 | 129 | * Fix: Fix checking whether cancellable promise is an object and avoid possible warning. 130 | (#161 by @smscr) 131 | 132 | * Improve performance by prefixing all global functions calls with \ to skip the look up and resolve process and go straight to the global function. 133 | (#134 by @WyriHaximus) 134 | 135 | * Improve test suite, update PHPUnit and PHP versions and add `.gitattributes` to exclude dev files from exports. 136 | (#107 by @carusogabriel, #148 and #234 by @WyriHaximus, #153 by @reedy, #162, #230 and #240 by @clue, #173, #177, #185 and #199 by @SimonFrings, #193 by @woodongwong and #210 by @bzikarsky) 137 | 138 | The following changes were originally planned for this release but later reverted 139 | and are not part of the final release: 140 | 141 | * Add iterative callback queue handler to avoid recursion (later removed to improve Fiber support). 142 | (#28, #82 and #86 by @jsor, #158 by @WyriHaximus and #229 and #238 by @clue) 143 | 144 | * Trigger an `E_USER_ERROR` instead of throwing an exception from `done()` (later removed entire `done()` method to globally report unhandled rejections). 145 | (#97 by @jsor and #224 and #248 by @clue) 146 | 147 | * Add type declarations for `some()` (later removed entire `some()` function). 148 | (#172 by @WyriHaximus and #219 by @clue) 149 | 150 | ## 2.0.0 (2013-12-10) 151 | 152 | See [`2.x` CHANGELOG](https://github.com/reactphp/promise/blob/2.x/CHANGELOG.md) for more details. 153 | 154 | ## 1.0.0 (2012-11-07) 155 | 156 | See [`1.x` CHANGELOG](https://github.com/reactphp/promise/blob/1.x/CHANGELOG.md) for more details. 157 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012 Jan Sorgalla, Christian Lück, Cees-Jan Kiewiet, Chris Boden 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Promise 2 | ======= 3 | 4 | A lightweight implementation of 5 | [CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. 6 | 7 | [![CI status](https://github.com/reactphp/promise/workflows/CI/badge.svg)](https://github.com/reactphp/promise/actions) 8 | [![installs on Packagist](https://img.shields.io/packagist/dt/react/promise?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/promise) 9 | 10 | Table of Contents 11 | ----------------- 12 | 13 | 1. [Introduction](#introduction) 14 | 2. [Concepts](#concepts) 15 | * [Deferred](#deferred) 16 | * [Promise](#promise-1) 17 | 3. [API](#api) 18 | * [Deferred](#deferred-1) 19 | * [Deferred::promise()](#deferredpromise) 20 | * [Deferred::resolve()](#deferredresolve) 21 | * [Deferred::reject()](#deferredreject) 22 | * [PromiseInterface](#promiseinterface) 23 | * [PromiseInterface::then()](#promiseinterfacethen) 24 | * [PromiseInterface::catch()](#promiseinterfacecatch) 25 | * [PromiseInterface::finally()](#promiseinterfacefinally) 26 | * [PromiseInterface::cancel()](#promiseinterfacecancel) 27 | * [~~PromiseInterface::otherwise()~~](#promiseinterfaceotherwise) 28 | * [~~PromiseInterface::always()~~](#promiseinterfacealways) 29 | * [Promise](#promise-2) 30 | * [Functions](#functions) 31 | * [resolve()](#resolve) 32 | * [reject()](#reject) 33 | * [all()](#all) 34 | * [race()](#race) 35 | * [any()](#any) 36 | * [set_rejection_handler()](#set_rejection_handler) 37 | 4. [Examples](#examples) 38 | * [How to use Deferred](#how-to-use-deferred) 39 | * [How promise forwarding works](#how-promise-forwarding-works) 40 | * [Resolution forwarding](#resolution-forwarding) 41 | * [Rejection forwarding](#rejection-forwarding) 42 | * [Mixed resolution and rejection forwarding](#mixed-resolution-and-rejection-forwarding) 43 | 5. [Install](#install) 44 | 6. [Tests](#tests) 45 | 7. [Credits](#credits) 46 | 8. [License](#license) 47 | 48 | Introduction 49 | ------------ 50 | 51 | Promise is a library implementing 52 | [CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. 53 | 54 | It also provides several other useful promise-related concepts, such as joining 55 | multiple promises and mapping and reducing collections of promises. 56 | 57 | If you've never heard about promises before, 58 | [read this first](https://gist.github.com/domenic/3889970). 59 | 60 | Concepts 61 | -------- 62 | 63 | ### Deferred 64 | 65 | A **Deferred** represents a computation or unit of work that may not have 66 | completed yet. Typically (but not always), that computation will be something 67 | that executes asynchronously and completes at some point in the future. 68 | 69 | ### Promise 70 | 71 | While a deferred represents the computation itself, a **Promise** represents 72 | the result of that computation. Thus, each deferred has a promise that acts as 73 | a placeholder for its actual result. 74 | 75 | API 76 | --- 77 | 78 | ### Deferred 79 | 80 | A deferred represents an operation whose resolution is pending. It has separate 81 | promise and resolver parts. 82 | 83 | ```php 84 | $deferred = new React\Promise\Deferred(); 85 | 86 | $promise = $deferred->promise(); 87 | 88 | $deferred->resolve(mixed $value); 89 | $deferred->reject(\Throwable $reason); 90 | ``` 91 | 92 | The `promise` method returns the promise of the deferred. 93 | 94 | The `resolve` and `reject` methods control the state of the deferred. 95 | 96 | The constructor of the `Deferred` accepts an optional `$canceller` argument. 97 | See [Promise](#promise-2) for more information. 98 | 99 | #### Deferred::promise() 100 | 101 | ```php 102 | $promise = $deferred->promise(); 103 | ``` 104 | 105 | Returns the promise of the deferred, which you can hand out to others while 106 | keeping the authority to modify its state to yourself. 107 | 108 | #### Deferred::resolve() 109 | 110 | ```php 111 | $deferred->resolve(mixed $value); 112 | ``` 113 | 114 | Resolves the promise returned by `promise()`. All consumers are notified by 115 | having `$onFulfilled` (which they registered via `$promise->then()`) called with 116 | `$value`. 117 | 118 | If `$value` itself is a promise, the promise will transition to the state of 119 | this promise once it is resolved. 120 | 121 | See also the [`resolve()` function](#resolve). 122 | 123 | #### Deferred::reject() 124 | 125 | ```php 126 | $deferred->reject(\Throwable $reason); 127 | ``` 128 | 129 | Rejects the promise returned by `promise()`, signalling that the deferred's 130 | computation failed. 131 | All consumers are notified by having `$onRejected` (which they registered via 132 | `$promise->then()`) called with `$reason`. 133 | 134 | See also the [`reject()` function](#reject). 135 | 136 | ### PromiseInterface 137 | 138 | The promise interface provides the common interface for all promise 139 | implementations. 140 | See [Promise](#promise-2) for the only public implementation exposed by this 141 | package. 142 | 143 | A promise represents an eventual outcome, which is either fulfillment (success) 144 | and an associated value, or rejection (failure) and an associated reason. 145 | 146 | Once in the fulfilled or rejected state, a promise becomes immutable. 147 | Neither its state nor its result (or error) can be modified. 148 | 149 | #### PromiseInterface::then() 150 | 151 | ```php 152 | $transformedPromise = $promise->then(callable $onFulfilled = null, callable $onRejected = null); 153 | ``` 154 | 155 | Transforms a promise's value by applying a function to the promise's fulfillment 156 | or rejection value. Returns a new promise for the transformed result. 157 | 158 | The `then()` method registers new fulfilled and rejection handlers with a promise 159 | (all parameters are optional): 160 | 161 | * `$onFulfilled` will be invoked once the promise is fulfilled and passed 162 | the result as the first argument. 163 | * `$onRejected` will be invoked once the promise is rejected and passed the 164 | reason as the first argument. 165 | 166 | It returns a new promise that will fulfill with the return value of either 167 | `$onFulfilled` or `$onRejected`, whichever is called, or will reject with 168 | the thrown exception if either throws. 169 | 170 | A promise makes the following guarantees about handlers registered in 171 | the same call to `then()`: 172 | 173 | 1. Only one of `$onFulfilled` or `$onRejected` will be called, 174 | never both. 175 | 2. `$onFulfilled` and `$onRejected` will never be called more 176 | than once. 177 | 178 | #### See also 179 | 180 | * [resolve()](#resolve) - Creating a resolved promise 181 | * [reject()](#reject) - Creating a rejected promise 182 | 183 | #### PromiseInterface::catch() 184 | 185 | ```php 186 | $promise->catch(callable $onRejected); 187 | ``` 188 | 189 | Registers a rejection handler for promise. It is a shortcut for: 190 | 191 | ```php 192 | $promise->then(null, $onRejected); 193 | ``` 194 | 195 | Additionally, you can type hint the `$reason` argument of `$onRejected` to catch 196 | only specific errors. 197 | 198 | ```php 199 | $promise 200 | ->catch(function (\RuntimeException $reason) { 201 | // Only catch \RuntimeException instances 202 | // All other types of errors will propagate automatically 203 | }) 204 | ->catch(function (\Throwable $reason) { 205 | // Catch other errors 206 | }); 207 | ``` 208 | 209 | #### PromiseInterface::finally() 210 | 211 | ```php 212 | $newPromise = $promise->finally(callable $onFulfilledOrRejected); 213 | ``` 214 | 215 | Allows you to execute "cleanup" type tasks in a promise chain. 216 | 217 | It arranges for `$onFulfilledOrRejected` to be called, with no arguments, 218 | when the promise is either fulfilled or rejected. 219 | 220 | * If `$promise` fulfills, and `$onFulfilledOrRejected` returns successfully, 221 | `$newPromise` will fulfill with the same value as `$promise`. 222 | * If `$promise` fulfills, and `$onFulfilledOrRejected` throws or returns a 223 | rejected promise, `$newPromise` will reject with the thrown exception or 224 | rejected promise's reason. 225 | * If `$promise` rejects, and `$onFulfilledOrRejected` returns successfully, 226 | `$newPromise` will reject with the same reason as `$promise`. 227 | * If `$promise` rejects, and `$onFulfilledOrRejected` throws or returns a 228 | rejected promise, `$newPromise` will reject with the thrown exception or 229 | rejected promise's reason. 230 | 231 | `finally()` behaves similarly to the synchronous finally statement. When combined 232 | with `catch()`, `finally()` allows you to write code that is similar to the familiar 233 | synchronous catch/finally pair. 234 | 235 | Consider the following synchronous code: 236 | 237 | ```php 238 | try { 239 | return doSomething(); 240 | } catch (\Throwable $e) { 241 | return handleError($e); 242 | } finally { 243 | cleanup(); 244 | } 245 | ``` 246 | 247 | Similar asynchronous code (with `doSomething()` that returns a promise) can be 248 | written: 249 | 250 | ```php 251 | return doSomething() 252 | ->catch('handleError') 253 | ->finally('cleanup'); 254 | ``` 255 | 256 | #### PromiseInterface::cancel() 257 | 258 | ``` php 259 | $promise->cancel(); 260 | ``` 261 | 262 | The `cancel()` method notifies the creator of the promise that there is no 263 | further interest in the results of the operation. 264 | 265 | Once a promise is settled (either fulfilled or rejected), calling `cancel()` on 266 | a promise has no effect. 267 | 268 | #### ~~PromiseInterface::otherwise()~~ 269 | 270 | > Deprecated since v3.0.0, see [`catch()`](#promiseinterfacecatch) instead. 271 | 272 | The `otherwise()` method registers a rejection handler for a promise. 273 | 274 | This method continues to exist only for BC reasons and to ease upgrading 275 | between versions. It is an alias for: 276 | 277 | ```php 278 | $promise->catch($onRejected); 279 | ``` 280 | 281 | #### ~~PromiseInterface::always()~~ 282 | 283 | > Deprecated since v3.0.0, see [`finally()`](#promiseinterfacefinally) instead. 284 | 285 | The `always()` method allows you to execute "cleanup" type tasks in a promise chain. 286 | 287 | This method continues to exist only for BC reasons and to ease upgrading 288 | between versions. It is an alias for: 289 | 290 | ```php 291 | $promise->finally($onFulfilledOrRejected); 292 | ``` 293 | 294 | ### Promise 295 | 296 | Creates a promise whose state is controlled by the functions passed to 297 | `$resolver`. 298 | 299 | ```php 300 | $resolver = function (callable $resolve, callable $reject) { 301 | // Do some work, possibly asynchronously, and then 302 | // resolve or reject. 303 | 304 | $resolve($awesomeResult); 305 | // or throw new Exception('Promise rejected'); 306 | // or $resolve($anotherPromise); 307 | // or $reject($nastyError); 308 | }; 309 | 310 | $canceller = function () { 311 | // Cancel/abort any running operations like network connections, streams etc. 312 | 313 | // Reject promise by throwing an exception 314 | throw new Exception('Promise cancelled'); 315 | }; 316 | 317 | $promise = new React\Promise\Promise($resolver, $canceller); 318 | ``` 319 | 320 | The promise constructor receives a resolver function and an optional canceller 321 | function which both will be called with two arguments: 322 | 323 | * `$resolve($value)` - Primary function that seals the fate of the 324 | returned promise. Accepts either a non-promise value, or another promise. 325 | When called with a non-promise value, fulfills promise with that value. 326 | When called with another promise, e.g. `$resolve($otherPromise)`, promise's 327 | fate will be equivalent to that of `$otherPromise`. 328 | * `$reject($reason)` - Function that rejects the promise. It is recommended to 329 | just throw an exception instead of using `$reject()`. 330 | 331 | If the resolver or canceller throw an exception, the promise will be rejected 332 | with that thrown exception as the rejection reason. 333 | 334 | The resolver function will be called immediately, the canceller function only 335 | once all consumers called the `cancel()` method of the promise. 336 | 337 | ### Functions 338 | 339 | Useful functions for creating and joining collections of promises. 340 | 341 | All functions working on promise collections (like `all()`, `race()`, 342 | etc.) support cancellation. This means, if you call `cancel()` on the returned 343 | promise, all promises in the collection are cancelled. 344 | 345 | #### resolve() 346 | 347 | ```php 348 | $promise = React\Promise\resolve(mixed $promiseOrValue); 349 | ``` 350 | 351 | Creates a promise for the supplied `$promiseOrValue`. 352 | 353 | If `$promiseOrValue` is a value, it will be the resolution value of the 354 | returned promise. 355 | 356 | If `$promiseOrValue` is a thenable (any object that provides a `then()` method), 357 | a trusted promise that follows the state of the thenable is returned. 358 | 359 | If `$promiseOrValue` is a promise, it will be returned as is. 360 | 361 | The resulting `$promise` implements the [`PromiseInterface`](#promiseinterface) 362 | and can be consumed like any other promise: 363 | 364 | ```php 365 | $promise = React\Promise\resolve(42); 366 | 367 | $promise->then(function (int $result): void { 368 | var_dump($result); 369 | }, function (\Throwable $e): void { 370 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 371 | }); 372 | ``` 373 | 374 | #### reject() 375 | 376 | ```php 377 | $promise = React\Promise\reject(\Throwable $reason); 378 | ``` 379 | 380 | Creates a rejected promise for the supplied `$reason`. 381 | 382 | Note that the [`\Throwable`](https://www.php.net/manual/en/class.throwable.php) interface introduced in PHP 7 covers 383 | both user land [`\Exception`](https://www.php.net/manual/en/class.exception.php)'s and 384 | [`\Error`](https://www.php.net/manual/en/class.error.php) internal PHP errors. By enforcing `\Throwable` as reason to 385 | reject a promise, any language error or user land exception can be used to reject a promise. 386 | 387 | The resulting `$promise` implements the [`PromiseInterface`](#promiseinterface) 388 | and can be consumed like any other promise: 389 | 390 | ```php 391 | $promise = React\Promise\reject(new RuntimeException('Request failed')); 392 | 393 | $promise->then(function (int $result): void { 394 | var_dump($result); 395 | }, function (\Throwable $e): void { 396 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 397 | }); 398 | ``` 399 | 400 | Note that rejected promises should always be handled similar to how any 401 | exceptions should always be caught in a `try` + `catch` block. If you remove the 402 | last reference to a rejected promise that has not been handled, it will 403 | report an unhandled promise rejection: 404 | 405 | ```php 406 | function incorrect(): int 407 | { 408 | $promise = React\Promise\reject(new RuntimeException('Request failed')); 409 | 410 | // Commented out: No rejection handler registered here. 411 | // $promise->then(null, function (\Throwable $e): void { /* ignore */ }); 412 | 413 | // Returning from a function will remove all local variable references, hence why 414 | // this will report an unhandled promise rejection here. 415 | return 42; 416 | } 417 | 418 | // Calling this function will log an error message plus its stack trace: 419 | // Unhandled promise rejection with RuntimeException: Request failed in example.php:10 420 | incorrect(); 421 | ``` 422 | 423 | A rejected promise will be considered "handled" if you catch the rejection 424 | reason with either the [`then()` method](#promiseinterfacethen), the 425 | [`catch()` method](#promiseinterfacecatch), or the 426 | [`finally()` method](#promiseinterfacefinally). Note that each of these methods 427 | return a new promise that may again be rejected if you re-throw an exception. 428 | 429 | A rejected promise will also be considered "handled" if you abort the operation 430 | with the [`cancel()` method](#promiseinterfacecancel) (which in turn would 431 | usually reject the promise if it is still pending). 432 | 433 | See also the [`set_rejection_handler()` function](#set_rejection_handler). 434 | 435 | #### all() 436 | 437 | ```php 438 | $promise = React\Promise\all(iterable $promisesOrValues); 439 | ``` 440 | 441 | Returns a promise that will resolve only once all the items in 442 | `$promisesOrValues` have resolved. The resolution value of the returned promise 443 | will be an array containing the resolution values of each of the items in 444 | `$promisesOrValues`. 445 | 446 | #### race() 447 | 448 | ```php 449 | $promise = React\Promise\race(iterable $promisesOrValues); 450 | ``` 451 | 452 | Initiates a competitive race that allows one winner. Returns a promise which is 453 | resolved in the same way the first settled promise resolves. 454 | 455 | The returned promise will become **infinitely pending** if `$promisesOrValues` 456 | contains 0 items. 457 | 458 | #### any() 459 | 460 | ```php 461 | $promise = React\Promise\any(iterable $promisesOrValues); 462 | ``` 463 | 464 | Returns a promise that will resolve when any one of the items in 465 | `$promisesOrValues` resolves. The resolution value of the returned promise 466 | will be the resolution value of the triggering item. 467 | 468 | The returned promise will only reject if *all* items in `$promisesOrValues` are 469 | rejected. The rejection value will be a `React\Promise\Exception\CompositeException` 470 | which holds all rejection reasons. The rejection reasons can be obtained with 471 | `CompositeException::getThrowables()`. 472 | 473 | The returned promise will also reject with a `React\Promise\Exception\LengthException` 474 | if `$promisesOrValues` contains 0 items. 475 | 476 | #### set_rejection_handler() 477 | 478 | ```php 479 | React\Promise\set_rejection_handler(?callable $callback): ?callable; 480 | ``` 481 | 482 | Sets the global rejection handler for unhandled promise rejections. 483 | 484 | Note that rejected promises should always be handled similar to how any 485 | exceptions should always be caught in a `try` + `catch` block. If you remove 486 | the last reference to a rejected promise that has not been handled, it will 487 | report an unhandled promise rejection. See also the [`reject()` function](#reject) 488 | for more details. 489 | 490 | The `?callable $callback` argument MUST be a valid callback function that 491 | accepts a single `Throwable` argument or a `null` value to restore the 492 | default promise rejection handler. The return value of the callback function 493 | will be ignored and has no effect, so you SHOULD return a `void` value. The 494 | callback function MUST NOT throw or the program will be terminated with a 495 | fatal error. 496 | 497 | The function returns the previous rejection handler or `null` if using the 498 | default promise rejection handler. 499 | 500 | The default promise rejection handler will log an error message plus its stack 501 | trace: 502 | 503 | ```php 504 | // Unhandled promise rejection with RuntimeException: Unhandled in example.php:2 505 | React\Promise\reject(new RuntimeException('Unhandled')); 506 | ``` 507 | 508 | The promise rejection handler may be used to use customize the log message or 509 | write to custom log targets. As a rule of thumb, this function should only be 510 | used as a last resort and promise rejections are best handled with either the 511 | [`then()` method](#promiseinterfacethen), the 512 | [`catch()` method](#promiseinterfacecatch), or the 513 | [`finally()` method](#promiseinterfacefinally). 514 | See also the [`reject()` function](#reject) for more details. 515 | 516 | Examples 517 | -------- 518 | 519 | ### How to use Deferred 520 | 521 | ```php 522 | function getAwesomeResultPromise() 523 | { 524 | $deferred = new React\Promise\Deferred(); 525 | 526 | // Execute a Node.js-style function using the callback pattern 527 | computeAwesomeResultAsynchronously(function (\Throwable $error, $result) use ($deferred) { 528 | if ($error) { 529 | $deferred->reject($error); 530 | } else { 531 | $deferred->resolve($result); 532 | } 533 | }); 534 | 535 | // Return the promise 536 | return $deferred->promise(); 537 | } 538 | 539 | getAwesomeResultPromise() 540 | ->then( 541 | function ($value) { 542 | // Deferred resolved, do something with $value 543 | }, 544 | function (\Throwable $reason) { 545 | // Deferred rejected, do something with $reason 546 | } 547 | ); 548 | ``` 549 | 550 | ### How promise forwarding works 551 | 552 | A few simple examples to show how the mechanics of Promises/A forwarding works. 553 | These examples are contrived, of course, and in real usage, promise chains will 554 | typically be spread across several function calls, or even several levels of 555 | your application architecture. 556 | 557 | #### Resolution forwarding 558 | 559 | Resolved promises forward resolution values to the next promise. 560 | The first promise, `$deferred->promise()`, will resolve with the value passed 561 | to `$deferred->resolve()` below. 562 | 563 | Each call to `then()` returns a new promise that will resolve with the return 564 | value of the previous handler. This creates a promise "pipeline". 565 | 566 | ```php 567 | $deferred = new React\Promise\Deferred(); 568 | 569 | $deferred->promise() 570 | ->then(function ($x) { 571 | // $x will be the value passed to $deferred->resolve() below 572 | // and returns a *new promise* for $x + 1 573 | return $x + 1; 574 | }) 575 | ->then(function ($x) { 576 | // $x === 2 577 | // This handler receives the return value of the 578 | // previous handler. 579 | return $x + 1; 580 | }) 581 | ->then(function ($x) { 582 | // $x === 3 583 | // This handler receives the return value of the 584 | // previous handler. 585 | return $x + 1; 586 | }) 587 | ->then(function ($x) { 588 | // $x === 4 589 | // This handler receives the return value of the 590 | // previous handler. 591 | echo 'Resolve ' . $x; 592 | }); 593 | 594 | $deferred->resolve(1); // Prints "Resolve 4" 595 | ``` 596 | 597 | #### Rejection forwarding 598 | 599 | Rejected promises behave similarly, and also work similarly to try/catch: 600 | When you catch an exception, you must rethrow for it to propagate. 601 | 602 | Similarly, when you handle a rejected promise, to propagate the rejection, 603 | "rethrow" it by either returning a rejected promise, or actually throwing 604 | (since promise translates thrown exceptions into rejections) 605 | 606 | ```php 607 | $deferred = new React\Promise\Deferred(); 608 | 609 | $deferred->promise() 610 | ->then(function ($x) { 611 | throw new \Exception($x + 1); 612 | }) 613 | ->catch(function (\Exception $x) { 614 | // Propagate the rejection 615 | throw $x; 616 | }) 617 | ->catch(function (\Exception $x) { 618 | // Can also propagate by returning another rejection 619 | return React\Promise\reject( 620 | new \Exception($x->getMessage() + 1) 621 | ); 622 | }) 623 | ->catch(function ($x) { 624 | echo 'Reject ' . $x->getMessage(); // 3 625 | }); 626 | 627 | $deferred->resolve(1); // Prints "Reject 3" 628 | ``` 629 | 630 | #### Mixed resolution and rejection forwarding 631 | 632 | Just like try/catch, you can choose to propagate or not. Mixing resolutions and 633 | rejections will still forward handler results in a predictable way. 634 | 635 | ```php 636 | $deferred = new React\Promise\Deferred(); 637 | 638 | $deferred->promise() 639 | ->then(function ($x) { 640 | return $x + 1; 641 | }) 642 | ->then(function ($x) { 643 | throw new \Exception($x + 1); 644 | }) 645 | ->catch(function (\Exception $x) { 646 | // Handle the rejection, and don't propagate. 647 | // This is like catch without a rethrow 648 | return $x->getMessage() + 1; 649 | }) 650 | ->then(function ($x) { 651 | echo 'Mixed ' . $x; // 4 652 | }); 653 | 654 | $deferred->resolve(1); // Prints "Mixed 4" 655 | ``` 656 | 657 | Install 658 | ------- 659 | 660 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 661 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 662 | 663 | This project follows [SemVer](https://semver.org/). 664 | This will install the latest supported version from this branch: 665 | 666 | ```bash 667 | composer require react/promise:^3.2 668 | ``` 669 | 670 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 671 | 672 | This project aims to run on any platform and thus does not require any PHP 673 | extensions and supports running on PHP 7.1 through current PHP 8+. 674 | It's *highly recommended to use the latest supported PHP version* for this project. 675 | 676 | We're committed to providing long-term support (LTS) options and to provide a 677 | smooth upgrade path. If you're using an older PHP version, you may use the 678 | [`2.x` branch](https://github.com/reactphp/promise/tree/2.x) (PHP 5.4+) or 679 | [`1.x` branch](https://github.com/reactphp/promise/tree/1.x) (PHP 5.3+) which both 680 | provide a compatible API but do not take advantage of newer language features. 681 | You may target multiple versions at the same time to support a wider range of 682 | PHP versions like this: 683 | 684 | ```bash 685 | composer require "react/promise:^3 || ^2 || ^1" 686 | ``` 687 | 688 | ## Tests 689 | 690 | To run the test suite, you first need to clone this repo and then install all 691 | dependencies [through Composer](https://getcomposer.org/): 692 | 693 | ```bash 694 | composer install 695 | ``` 696 | 697 | To run the test suite, go to the project root and run: 698 | 699 | ```bash 700 | vendor/bin/phpunit 701 | ``` 702 | 703 | On top of this, we use PHPStan on max level to ensure type safety across the project: 704 | 705 | ```bash 706 | vendor/bin/phpstan 707 | ``` 708 | 709 | Credits 710 | ------- 711 | 712 | Promise is a port of [when.js](https://github.com/cujojs/when) 713 | by [Brian Cavalier](https://github.com/briancavalier). 714 | 715 | Also, large parts of the documentation have been ported from the when.js 716 | [Wiki](https://github.com/cujojs/when/wiki) and the 717 | [API docs](https://github.com/cujojs/when/blob/master/docs/api.md). 718 | 719 | License 720 | ------- 721 | 722 | Released under the [MIT](LICENSE) license. 723 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react/promise", 3 | "description": "A lightweight implementation of CommonJS Promises/A for PHP", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Jan Sorgalla", 8 | "homepage": "https://sorgalla.com/", 9 | "email": "jsorgalla@gmail.com" 10 | }, 11 | { 12 | "name": "Christian Lück", 13 | "homepage": "https://clue.engineering/", 14 | "email": "christian@clue.engineering" 15 | }, 16 | { 17 | "name": "Cees-Jan Kiewiet", 18 | "homepage": "https://wyrihaximus.net/", 19 | "email": "reactphp@ceesjankiewiet.nl" 20 | }, 21 | { 22 | "name": "Chris Boden", 23 | "homepage": "https://cboden.dev/", 24 | "email": "cboden@gmail.com" 25 | } 26 | ], 27 | "require": { 28 | "php": ">=7.1.0" 29 | }, 30 | "require-dev": { 31 | "phpstan/phpstan": "1.10.39 || 1.4.10", 32 | "phpunit/phpunit": "^9.6 || ^7.5" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "React\\Promise\\": "src/" 37 | }, 38 | "files": [ 39 | "src/functions_include.php" 40 | ] 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "React\\Promise\\": [ 45 | "tests/fixtures/", 46 | "tests/" 47 | ] 48 | }, 49 | "files": [ 50 | "tests/Fiber.php" 51 | ] 52 | }, 53 | "keywords": [ 54 | "promise", 55 | "promises" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/Deferred.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | private $promise; 14 | 15 | /** @var callable(T):void */ 16 | private $resolveCallback; 17 | 18 | /** @var callable(\Throwable):void */ 19 | private $rejectCallback; 20 | 21 | /** 22 | * @param (callable(callable(T):void,callable(\Throwable):void):void)|null $canceller 23 | */ 24 | public function __construct(?callable $canceller = null) 25 | { 26 | $this->promise = new Promise(function ($resolve, $reject): void { 27 | $this->resolveCallback = $resolve; 28 | $this->rejectCallback = $reject; 29 | }, $canceller); 30 | } 31 | 32 | /** 33 | * @return PromiseInterface 34 | */ 35 | public function promise(): PromiseInterface 36 | { 37 | return $this->promise; 38 | } 39 | 40 | /** 41 | * @param T $value 42 | */ 43 | public function resolve($value): void 44 | { 45 | ($this->resolveCallback)($value); 46 | } 47 | 48 | public function reject(\Throwable $reason): void 49 | { 50 | ($this->rejectCallback)($reason); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Exception/CompositeException.php: -------------------------------------------------------------------------------- 1 | throwables = $throwables; 23 | } 24 | 25 | /** 26 | * @return \Throwable[] 27 | */ 28 | public function getThrowables(): array 29 | { 30 | return $this->throwables; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/LengthException.php: -------------------------------------------------------------------------------- 1 | started) { 19 | return; 20 | } 21 | 22 | $this->started = true; 23 | $this->drain(); 24 | } 25 | 26 | /** 27 | * @param mixed $cancellable 28 | */ 29 | public function enqueue($cancellable): void 30 | { 31 | if (!\is_object($cancellable) || !\method_exists($cancellable, 'then') || !\method_exists($cancellable, 'cancel')) { 32 | return; 33 | } 34 | 35 | $length = \array_push($this->queue, $cancellable); 36 | 37 | if ($this->started && 1 === $length) { 38 | $this->drain(); 39 | } 40 | } 41 | 42 | private function drain(): void 43 | { 44 | for ($i = \key($this->queue); isset($this->queue[$i]); $i++) { 45 | $cancellable = $this->queue[$i]; 46 | assert(\method_exists($cancellable, 'cancel')); 47 | 48 | $exception = null; 49 | 50 | try { 51 | $cancellable->cancel(); 52 | } catch (\Throwable $exception) { 53 | } 54 | 55 | unset($this->queue[$i]); 56 | 57 | if ($exception) { 58 | throw $exception; 59 | } 60 | } 61 | 62 | $this->queue = []; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Internal/FulfilledPromise.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class FulfilledPromise implements PromiseInterface 15 | { 16 | /** @var T */ 17 | private $value; 18 | 19 | /** 20 | * @param T $value 21 | * @throws \InvalidArgumentException 22 | */ 23 | public function __construct($value = null) 24 | { 25 | if ($value instanceof PromiseInterface) { 26 | throw new \InvalidArgumentException('You cannot create React\Promise\FulfilledPromise with a promise. Use React\Promise\resolve($promiseOrValue) instead.'); 27 | } 28 | 29 | $this->value = $value; 30 | } 31 | 32 | /** 33 | * @template TFulfilled 34 | * @param ?(callable((T is void ? null : T)): (PromiseInterface|TFulfilled)) $onFulfilled 35 | * @return PromiseInterface<($onFulfilled is null ? T : TFulfilled)> 36 | */ 37 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface 38 | { 39 | if (null === $onFulfilled) { 40 | return $this; 41 | } 42 | 43 | try { 44 | /** 45 | * @var PromiseInterface|T $result 46 | */ 47 | $result = $onFulfilled($this->value); 48 | return resolve($result); 49 | } catch (\Throwable $exception) { 50 | return new RejectedPromise($exception); 51 | } 52 | } 53 | 54 | public function catch(callable $onRejected): PromiseInterface 55 | { 56 | return $this; 57 | } 58 | 59 | public function finally(callable $onFulfilledOrRejected): PromiseInterface 60 | { 61 | return $this->then(function ($value) use ($onFulfilledOrRejected): PromiseInterface { 62 | return resolve($onFulfilledOrRejected())->then(function () use ($value) { 63 | return $value; 64 | }); 65 | }); 66 | } 67 | 68 | public function cancel(): void 69 | { 70 | } 71 | 72 | /** 73 | * @deprecated 3.0.0 Use `catch()` instead 74 | * @see self::catch() 75 | */ 76 | public function otherwise(callable $onRejected): PromiseInterface 77 | { 78 | return $this->catch($onRejected); 79 | } 80 | 81 | /** 82 | * @deprecated 3.0.0 Use `finally()` instead 83 | * @see self::finally() 84 | */ 85 | public function always(callable $onFulfilledOrRejected): PromiseInterface 86 | { 87 | return $this->finally($onFulfilledOrRejected); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Internal/RejectedPromise.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class RejectedPromise implements PromiseInterface 16 | { 17 | /** @var \Throwable */ 18 | private $reason; 19 | 20 | /** @var bool */ 21 | private $handled = false; 22 | 23 | /** 24 | * @param \Throwable $reason 25 | */ 26 | public function __construct(\Throwable $reason) 27 | { 28 | $this->reason = $reason; 29 | } 30 | 31 | /** @throws void */ 32 | public function __destruct() 33 | { 34 | if ($this->handled) { 35 | return; 36 | } 37 | 38 | $handler = set_rejection_handler(null); 39 | if ($handler === null) { 40 | $message = 'Unhandled promise rejection with ' . $this->reason; 41 | 42 | \error_log($message); 43 | return; 44 | } 45 | 46 | try { 47 | $handler($this->reason); 48 | } catch (\Throwable $e) { 49 | \preg_match('/^([^:\s]++)(.*+)$/sm', (string) $e, $match); 50 | \assert(isset($match[1], $match[2])); 51 | $message = 'Fatal error: Uncaught ' . $match[1] . ' from unhandled promise rejection handler' . $match[2]; 52 | 53 | \error_log($message); 54 | exit(255); 55 | } 56 | } 57 | 58 | /** 59 | * @template TRejected 60 | * @param ?callable $onFulfilled 61 | * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected 62 | * @return PromiseInterface<($onRejected is null ? never : TRejected)> 63 | */ 64 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface 65 | { 66 | if (null === $onRejected) { 67 | return $this; 68 | } 69 | 70 | $this->handled = true; 71 | 72 | try { 73 | return resolve($onRejected($this->reason)); 74 | } catch (\Throwable $exception) { 75 | return new RejectedPromise($exception); 76 | } 77 | } 78 | 79 | /** 80 | * @template TThrowable of \Throwable 81 | * @template TRejected 82 | * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected 83 | * @return PromiseInterface 84 | */ 85 | public function catch(callable $onRejected): PromiseInterface 86 | { 87 | if (!_checkTypehint($onRejected, $this->reason)) { 88 | return $this; 89 | } 90 | 91 | /** 92 | * @var callable(\Throwable):(PromiseInterface|TRejected) $onRejected 93 | */ 94 | return $this->then(null, $onRejected); 95 | } 96 | 97 | public function finally(callable $onFulfilledOrRejected): PromiseInterface 98 | { 99 | return $this->then(null, function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface { 100 | return resolve($onFulfilledOrRejected())->then(function () use ($reason): PromiseInterface { 101 | return new RejectedPromise($reason); 102 | }); 103 | }); 104 | } 105 | 106 | public function cancel(): void 107 | { 108 | $this->handled = true; 109 | } 110 | 111 | /** 112 | * @deprecated 3.0.0 Use `catch()` instead 113 | * @see self::catch() 114 | */ 115 | public function otherwise(callable $onRejected): PromiseInterface 116 | { 117 | return $this->catch($onRejected); 118 | } 119 | 120 | /** 121 | * @deprecated 3.0.0 Use `always()` instead 122 | * @see self::always() 123 | */ 124 | public function always(callable $onFulfilledOrRejected): PromiseInterface 125 | { 126 | return $this->finally($onFulfilledOrRejected); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Promise.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class Promise implements PromiseInterface 12 | { 13 | /** @var (callable(callable(T):void,callable(\Throwable):void):void)|null */ 14 | private $canceller; 15 | 16 | /** @var ?PromiseInterface */ 17 | private $result; 18 | 19 | /** @var list):void> */ 20 | private $handlers = []; 21 | 22 | /** @var int */ 23 | private $requiredCancelRequests = 0; 24 | 25 | /** @var bool */ 26 | private $cancelled = false; 27 | 28 | /** 29 | * @param callable(callable(T):void,callable(\Throwable):void):void $resolver 30 | * @param (callable(callable(T):void,callable(\Throwable):void):void)|null $canceller 31 | */ 32 | public function __construct(callable $resolver, ?callable $canceller = null) 33 | { 34 | $this->canceller = $canceller; 35 | 36 | // Explicitly overwrite arguments with null values before invoking 37 | // resolver function. This ensure that these arguments do not show up 38 | // in the stack trace in PHP 7+ only. 39 | $cb = $resolver; 40 | $resolver = $canceller = null; 41 | $this->call($cb); 42 | } 43 | 44 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface 45 | { 46 | if (null !== $this->result) { 47 | return $this->result->then($onFulfilled, $onRejected); 48 | } 49 | 50 | if (null === $this->canceller) { 51 | return new static($this->resolver($onFulfilled, $onRejected)); 52 | } 53 | 54 | // This promise has a canceller, so we create a new child promise which 55 | // has a canceller that invokes the parent canceller if all other 56 | // followers are also cancelled. We keep a reference to this promise 57 | // instance for the static canceller function and clear this to avoid 58 | // keeping a cyclic reference between parent and follower. 59 | $parent = $this; 60 | ++$parent->requiredCancelRequests; 61 | 62 | return new static( 63 | $this->resolver($onFulfilled, $onRejected), 64 | static function () use (&$parent): void { 65 | assert($parent instanceof self); 66 | --$parent->requiredCancelRequests; 67 | 68 | if ($parent->requiredCancelRequests <= 0) { 69 | $parent->cancel(); 70 | } 71 | 72 | $parent = null; 73 | } 74 | ); 75 | } 76 | 77 | /** 78 | * @template TThrowable of \Throwable 79 | * @template TRejected 80 | * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected 81 | * @return PromiseInterface 82 | */ 83 | public function catch(callable $onRejected): PromiseInterface 84 | { 85 | return $this->then(null, static function (\Throwable $reason) use ($onRejected) { 86 | if (!_checkTypehint($onRejected, $reason)) { 87 | return new RejectedPromise($reason); 88 | } 89 | 90 | /** 91 | * @var callable(\Throwable):(PromiseInterface|TRejected) $onRejected 92 | */ 93 | return $onRejected($reason); 94 | }); 95 | } 96 | 97 | public function finally(callable $onFulfilledOrRejected): PromiseInterface 98 | { 99 | return $this->then(static function ($value) use ($onFulfilledOrRejected): PromiseInterface { 100 | return resolve($onFulfilledOrRejected())->then(function () use ($value) { 101 | return $value; 102 | }); 103 | }, static function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface { 104 | return resolve($onFulfilledOrRejected())->then(function () use ($reason): RejectedPromise { 105 | return new RejectedPromise($reason); 106 | }); 107 | }); 108 | } 109 | 110 | public function cancel(): void 111 | { 112 | $this->cancelled = true; 113 | $canceller = $this->canceller; 114 | $this->canceller = null; 115 | 116 | $parentCanceller = null; 117 | 118 | if (null !== $this->result) { 119 | // Forward cancellation to rejected promise to avoid reporting unhandled rejection 120 | if ($this->result instanceof RejectedPromise) { 121 | $this->result->cancel(); 122 | } 123 | 124 | // Go up the promise chain and reach the top most promise which is 125 | // itself not following another promise 126 | $root = $this->unwrap($this->result); 127 | 128 | // Return if the root promise is already resolved or a 129 | // FulfilledPromise or RejectedPromise 130 | if (!$root instanceof self || null !== $root->result) { 131 | return; 132 | } 133 | 134 | $root->requiredCancelRequests--; 135 | 136 | if ($root->requiredCancelRequests <= 0) { 137 | $parentCanceller = [$root, 'cancel']; 138 | } 139 | } 140 | 141 | if (null !== $canceller) { 142 | $this->call($canceller); 143 | } 144 | 145 | // For BC, we call the parent canceller after our own canceller 146 | if ($parentCanceller) { 147 | $parentCanceller(); 148 | } 149 | } 150 | 151 | /** 152 | * @deprecated 3.0.0 Use `catch()` instead 153 | * @see self::catch() 154 | */ 155 | public function otherwise(callable $onRejected): PromiseInterface 156 | { 157 | return $this->catch($onRejected); 158 | } 159 | 160 | /** 161 | * @deprecated 3.0.0 Use `finally()` instead 162 | * @see self::finally() 163 | */ 164 | public function always(callable $onFulfilledOrRejected): PromiseInterface 165 | { 166 | return $this->finally($onFulfilledOrRejected); 167 | } 168 | 169 | private function resolver(?callable $onFulfilled = null, ?callable $onRejected = null): callable 170 | { 171 | return function (callable $resolve, callable $reject) use ($onFulfilled, $onRejected): void { 172 | $this->handlers[] = static function (PromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject): void { 173 | $promise = $promise->then($onFulfilled, $onRejected); 174 | 175 | if ($promise instanceof self && $promise->result === null) { 176 | $promise->handlers[] = static function (PromiseInterface $promise) use ($resolve, $reject): void { 177 | $promise->then($resolve, $reject); 178 | }; 179 | } else { 180 | $promise->then($resolve, $reject); 181 | } 182 | }; 183 | }; 184 | } 185 | 186 | private function reject(\Throwable $reason): void 187 | { 188 | if (null !== $this->result) { 189 | return; 190 | } 191 | 192 | $this->settle(reject($reason)); 193 | } 194 | 195 | /** 196 | * @param PromiseInterface $result 197 | */ 198 | private function settle(PromiseInterface $result): void 199 | { 200 | $result = $this->unwrap($result); 201 | 202 | if ($result === $this) { 203 | $result = new RejectedPromise( 204 | new \LogicException('Cannot resolve a promise with itself.') 205 | ); 206 | } 207 | 208 | if ($result instanceof self) { 209 | $result->requiredCancelRequests++; 210 | } else { 211 | // Unset canceller only when not following a pending promise 212 | $this->canceller = null; 213 | } 214 | 215 | $handlers = $this->handlers; 216 | 217 | $this->handlers = []; 218 | $this->result = $result; 219 | 220 | foreach ($handlers as $handler) { 221 | $handler($result); 222 | } 223 | 224 | // Forward cancellation to rejected promise to avoid reporting unhandled rejection 225 | if ($this->cancelled && $result instanceof RejectedPromise) { 226 | $result->cancel(); 227 | } 228 | } 229 | 230 | /** 231 | * @param PromiseInterface $promise 232 | * @return PromiseInterface 233 | */ 234 | private function unwrap(PromiseInterface $promise): PromiseInterface 235 | { 236 | while ($promise instanceof self && null !== $promise->result) { 237 | /** @var PromiseInterface $promise */ 238 | $promise = $promise->result; 239 | } 240 | 241 | return $promise; 242 | } 243 | 244 | /** 245 | * @param callable(callable(mixed):void,callable(\Throwable):void):void $cb 246 | */ 247 | private function call(callable $cb): void 248 | { 249 | // Explicitly overwrite argument with null value. This ensure that this 250 | // argument does not show up in the stack trace in PHP 7+ only. 251 | $callback = $cb; 252 | $cb = null; 253 | 254 | // Use reflection to inspect number of arguments expected by this callback. 255 | // We did some careful benchmarking here: Using reflection to avoid unneeded 256 | // function arguments is actually faster than blindly passing them. 257 | // Also, this helps avoiding unnecessary function arguments in the call stack 258 | // if the callback creates an Exception (creating garbage cycles). 259 | if (\is_array($callback)) { 260 | $ref = new \ReflectionMethod($callback[0], $callback[1]); 261 | } elseif (\is_object($callback) && !$callback instanceof \Closure) { 262 | $ref = new \ReflectionMethod($callback, '__invoke'); 263 | } else { 264 | assert($callback instanceof \Closure || \is_string($callback)); 265 | $ref = new \ReflectionFunction($callback); 266 | } 267 | $args = $ref->getNumberOfParameters(); 268 | 269 | try { 270 | if ($args === 0) { 271 | $callback(); 272 | } else { 273 | // Keep references to this promise instance for the static resolve/reject functions. 274 | // By using static callbacks that are not bound to this instance 275 | // and passing the target promise instance by reference, we can 276 | // still execute its resolving logic and still clear this 277 | // reference when settling the promise. This helps avoiding 278 | // garbage cycles if any callback creates an Exception. 279 | // These assumptions are covered by the test suite, so if you ever feel like 280 | // refactoring this, go ahead, any alternative suggestions are welcome! 281 | $target =& $this; 282 | 283 | $callback( 284 | static function ($value) use (&$target): void { 285 | if ($target !== null) { 286 | $target->settle(resolve($value)); 287 | $target = null; 288 | } 289 | }, 290 | static function (\Throwable $reason) use (&$target): void { 291 | if ($target !== null) { 292 | $target->reject($reason); 293 | $target = null; 294 | } 295 | } 296 | ); 297 | } 298 | } catch (\Throwable $e) { 299 | $target = null; 300 | $this->reject($e); 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/PromiseInterface.php: -------------------------------------------------------------------------------- 1 | |TFulfilled)) $onFulfilled 37 | * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected 38 | * @return PromiseInterface<($onRejected is null ? ($onFulfilled is null ? T : TFulfilled) : ($onFulfilled is null ? T|TRejected : TFulfilled|TRejected))> 39 | */ 40 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface; 41 | 42 | /** 43 | * Registers a rejection handler for promise. It is a shortcut for: 44 | * 45 | * ```php 46 | * $promise->then(null, $onRejected); 47 | * ``` 48 | * 49 | * Additionally, you can type hint the `$reason` argument of `$onRejected` to catch 50 | * only specific errors. 51 | * 52 | * @template TThrowable of \Throwable 53 | * @template TRejected 54 | * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected 55 | * @return PromiseInterface 56 | */ 57 | public function catch(callable $onRejected): PromiseInterface; 58 | 59 | /** 60 | * Allows you to execute "cleanup" type tasks in a promise chain. 61 | * 62 | * It arranges for `$onFulfilledOrRejected` to be called, with no arguments, 63 | * when the promise is either fulfilled or rejected. 64 | * 65 | * * If `$promise` fulfills, and `$onFulfilledOrRejected` returns successfully, 66 | * `$newPromise` will fulfill with the same value as `$promise`. 67 | * * If `$promise` fulfills, and `$onFulfilledOrRejected` throws or returns a 68 | * rejected promise, `$newPromise` will reject with the thrown exception or 69 | * rejected promise's reason. 70 | * * If `$promise` rejects, and `$onFulfilledOrRejected` returns successfully, 71 | * `$newPromise` will reject with the same reason as `$promise`. 72 | * * If `$promise` rejects, and `$onFulfilledOrRejected` throws or returns a 73 | * rejected promise, `$newPromise` will reject with the thrown exception or 74 | * rejected promise's reason. 75 | * 76 | * `finally()` behaves similarly to the synchronous finally statement. When combined 77 | * with `catch()`, `finally()` allows you to write code that is similar to the familiar 78 | * synchronous catch/finally pair. 79 | * 80 | * Consider the following synchronous code: 81 | * 82 | * ```php 83 | * try { 84 | * return doSomething(); 85 | * } catch(\Exception $e) { 86 | * return handleError($e); 87 | * } finally { 88 | * cleanup(); 89 | * } 90 | * ``` 91 | * 92 | * Similar asynchronous code (with `doSomething()` that returns a promise) can be 93 | * written: 94 | * 95 | * ```php 96 | * return doSomething() 97 | * ->catch('handleError') 98 | * ->finally('cleanup'); 99 | * ``` 100 | * 101 | * @param callable(): (void|PromiseInterface) $onFulfilledOrRejected 102 | * @return PromiseInterface 103 | */ 104 | public function finally(callable $onFulfilledOrRejected): PromiseInterface; 105 | 106 | /** 107 | * The `cancel()` method notifies the creator of the promise that there is no 108 | * further interest in the results of the operation. 109 | * 110 | * Once a promise is settled (either fulfilled or rejected), calling `cancel()` on 111 | * a promise has no effect. 112 | * 113 | * @return void 114 | */ 115 | public function cancel(): void; 116 | 117 | /** 118 | * [Deprecated] Registers a rejection handler for a promise. 119 | * 120 | * This method continues to exist only for BC reasons and to ease upgrading 121 | * between versions. It is an alias for: 122 | * 123 | * ```php 124 | * $promise->catch($onRejected); 125 | * ``` 126 | * 127 | * @template TThrowable of \Throwable 128 | * @template TRejected 129 | * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected 130 | * @return PromiseInterface 131 | * @deprecated 3.0.0 Use catch() instead 132 | * @see self::catch() 133 | */ 134 | public function otherwise(callable $onRejected): PromiseInterface; 135 | 136 | /** 137 | * [Deprecated] Allows you to execute "cleanup" type tasks in a promise chain. 138 | * 139 | * This method continues to exist only for BC reasons and to ease upgrading 140 | * between versions. It is an alias for: 141 | * 142 | * ```php 143 | * $promise->finally($onFulfilledOrRejected); 144 | * ``` 145 | * 146 | * @param callable(): (void|PromiseInterface) $onFulfilledOrRejected 147 | * @return PromiseInterface 148 | * @deprecated 3.0.0 Use finally() instead 149 | * @see self::finally() 150 | */ 151 | public function always(callable $onFulfilledOrRejected): PromiseInterface; 152 | } 153 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | |T $promiseOrValue 22 | * @return PromiseInterface 23 | */ 24 | function resolve($promiseOrValue): PromiseInterface 25 | { 26 | if ($promiseOrValue instanceof PromiseInterface) { 27 | return $promiseOrValue; 28 | } 29 | 30 | if (\is_object($promiseOrValue) && \method_exists($promiseOrValue, 'then')) { 31 | $canceller = null; 32 | 33 | if (\method_exists($promiseOrValue, 'cancel')) { 34 | $canceller = [$promiseOrValue, 'cancel']; 35 | assert(\is_callable($canceller)); 36 | } 37 | 38 | /** @var Promise */ 39 | return new Promise(function (callable $resolve, callable $reject) use ($promiseOrValue): void { 40 | $promiseOrValue->then($resolve, $reject); 41 | }, $canceller); 42 | } 43 | 44 | return new FulfilledPromise($promiseOrValue); 45 | } 46 | 47 | /** 48 | * Creates a rejected promise for the supplied `$reason`. 49 | * 50 | * If `$reason` is a value, it will be the rejection value of the 51 | * returned promise. 52 | * 53 | * If `$reason` is a promise, its completion value will be the rejected 54 | * value of the returned promise. 55 | * 56 | * This can be useful in situations where you need to reject a promise without 57 | * throwing an exception. For example, it allows you to propagate a rejection with 58 | * the value of another promise. 59 | * 60 | * @return PromiseInterface 61 | */ 62 | function reject(\Throwable $reason): PromiseInterface 63 | { 64 | return new RejectedPromise($reason); 65 | } 66 | 67 | /** 68 | * Returns a promise that will resolve only once all the items in 69 | * `$promisesOrValues` have resolved. The resolution value of the returned promise 70 | * will be an array containing the resolution values of each of the items in 71 | * `$promisesOrValues`. 72 | * 73 | * @template T 74 | * @param iterable|T> $promisesOrValues 75 | * @return PromiseInterface> 76 | */ 77 | function all(iterable $promisesOrValues): PromiseInterface 78 | { 79 | $cancellationQueue = new Internal\CancellationQueue(); 80 | 81 | /** @var Promise> */ 82 | return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { 83 | $toResolve = 0; 84 | /** @var bool */ 85 | $continue = true; 86 | $values = []; 87 | 88 | foreach ($promisesOrValues as $i => $promiseOrValue) { 89 | $cancellationQueue->enqueue($promiseOrValue); 90 | $values[$i] = null; 91 | ++$toResolve; 92 | 93 | resolve($promiseOrValue)->then( 94 | function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void { 95 | $values[$i] = $value; 96 | 97 | if (0 === --$toResolve && !$continue) { 98 | $resolve($values); 99 | } 100 | }, 101 | function (\Throwable $reason) use (&$continue, $reject): void { 102 | $continue = false; 103 | $reject($reason); 104 | } 105 | ); 106 | 107 | if (!$continue && !\is_array($promisesOrValues)) { 108 | break; 109 | } 110 | } 111 | 112 | $continue = false; 113 | if ($toResolve === 0) { 114 | $resolve($values); 115 | } 116 | }, $cancellationQueue); 117 | } 118 | 119 | /** 120 | * Initiates a competitive race that allows one winner. Returns a promise which is 121 | * resolved in the same way the first settled promise resolves. 122 | * 123 | * The returned promise will become **infinitely pending** if `$promisesOrValues` 124 | * contains 0 items. 125 | * 126 | * @template T 127 | * @param iterable|T> $promisesOrValues 128 | * @return PromiseInterface 129 | */ 130 | function race(iterable $promisesOrValues): PromiseInterface 131 | { 132 | $cancellationQueue = new Internal\CancellationQueue(); 133 | 134 | /** @var Promise */ 135 | return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { 136 | $continue = true; 137 | 138 | foreach ($promisesOrValues as $promiseOrValue) { 139 | $cancellationQueue->enqueue($promiseOrValue); 140 | 141 | resolve($promiseOrValue)->then($resolve, $reject)->finally(function () use (&$continue): void { 142 | $continue = false; 143 | }); 144 | 145 | if (!$continue && !\is_array($promisesOrValues)) { 146 | break; 147 | } 148 | } 149 | }, $cancellationQueue); 150 | } 151 | 152 | /** 153 | * Returns a promise that will resolve when any one of the items in 154 | * `$promisesOrValues` resolves. The resolution value of the returned promise 155 | * will be the resolution value of the triggering item. 156 | * 157 | * The returned promise will only reject if *all* items in `$promisesOrValues` are 158 | * rejected. The rejection value will be an array of all rejection reasons. 159 | * 160 | * The returned promise will also reject with a `React\Promise\Exception\LengthException` 161 | * if `$promisesOrValues` contains 0 items. 162 | * 163 | * @template T 164 | * @param iterable|T> $promisesOrValues 165 | * @return PromiseInterface 166 | */ 167 | function any(iterable $promisesOrValues): PromiseInterface 168 | { 169 | $cancellationQueue = new Internal\CancellationQueue(); 170 | 171 | /** @var Promise */ 172 | return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { 173 | $toReject = 0; 174 | $continue = true; 175 | $reasons = []; 176 | 177 | foreach ($promisesOrValues as $i => $promiseOrValue) { 178 | $cancellationQueue->enqueue($promiseOrValue); 179 | ++$toReject; 180 | 181 | resolve($promiseOrValue)->then( 182 | function ($value) use ($resolve, &$continue): void { 183 | $continue = false; 184 | $resolve($value); 185 | }, 186 | function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continue): void { 187 | $reasons[$i] = $reason; 188 | 189 | if (0 === --$toReject && !$continue) { 190 | $reject(new CompositeException( 191 | $reasons, 192 | 'All promises rejected.' 193 | )); 194 | } 195 | } 196 | ); 197 | 198 | if (!$continue && !\is_array($promisesOrValues)) { 199 | break; 200 | } 201 | } 202 | 203 | $continue = false; 204 | if ($toReject === 0 && !$reasons) { 205 | $reject(new Exception\LengthException( 206 | 'Must contain at least 1 item but contains only 0 items.' 207 | )); 208 | } elseif ($toReject === 0) { 209 | $reject(new CompositeException( 210 | $reasons, 211 | 'All promises rejected.' 212 | )); 213 | } 214 | }, $cancellationQueue); 215 | } 216 | 217 | /** 218 | * Sets the global rejection handler for unhandled promise rejections. 219 | * 220 | * Note that rejected promises should always be handled similar to how any 221 | * exceptions should always be caught in a `try` + `catch` block. If you remove 222 | * the last reference to a rejected promise that has not been handled, it will 223 | * report an unhandled promise rejection. See also the [`reject()` function](#reject) 224 | * for more details. 225 | * 226 | * The `?callable $callback` argument MUST be a valid callback function that 227 | * accepts a single `Throwable` argument or a `null` value to restore the 228 | * default promise rejection handler. The return value of the callback function 229 | * will be ignored and has no effect, so you SHOULD return a `void` value. The 230 | * callback function MUST NOT throw or the program will be terminated with a 231 | * fatal error. 232 | * 233 | * The function returns the previous rejection handler or `null` if using the 234 | * default promise rejection handler. 235 | * 236 | * The default promise rejection handler will log an error message plus its 237 | * stack trace: 238 | * 239 | * ```php 240 | * // Unhandled promise rejection with RuntimeException: Unhandled in example.php:2 241 | * React\Promise\reject(new RuntimeException('Unhandled')); 242 | * ``` 243 | * 244 | * The promise rejection handler may be used to use customize the log message or 245 | * write to custom log targets. As a rule of thumb, this function should only be 246 | * used as a last resort and promise rejections are best handled with either the 247 | * [`then()` method](#promiseinterfacethen), the 248 | * [`catch()` method](#promiseinterfacecatch), or the 249 | * [`finally()` method](#promiseinterfacefinally). 250 | * See also the [`reject()` function](#reject) for more details. 251 | * 252 | * @param callable(\Throwable):void|null $callback 253 | * @return callable(\Throwable):void|null 254 | */ 255 | function set_rejection_handler(?callable $callback): ?callable 256 | { 257 | static $current = null; 258 | $previous = $current; 259 | $current = $callback; 260 | 261 | return $previous; 262 | } 263 | 264 | /** 265 | * @internal 266 | */ 267 | function _checkTypehint(callable $callback, \Throwable $reason): bool 268 | { 269 | if (\is_array($callback)) { 270 | $callbackReflection = new \ReflectionMethod($callback[0], $callback[1]); 271 | } elseif (\is_object($callback) && !$callback instanceof \Closure) { 272 | $callbackReflection = new \ReflectionMethod($callback, '__invoke'); 273 | } else { 274 | assert($callback instanceof \Closure || \is_string($callback)); 275 | $callbackReflection = new \ReflectionFunction($callback); 276 | } 277 | 278 | $parameters = $callbackReflection->getParameters(); 279 | 280 | if (!isset($parameters[0])) { 281 | return true; 282 | } 283 | 284 | $expectedException = $parameters[0]; 285 | 286 | // Extract the type of the argument and handle different possibilities 287 | $type = $expectedException->getType(); 288 | 289 | $isTypeUnion = true; 290 | $types = []; 291 | 292 | switch (true) { 293 | case $type === null: 294 | break; 295 | case $type instanceof \ReflectionNamedType: 296 | $types = [$type]; 297 | break; 298 | case $type instanceof \ReflectionIntersectionType: 299 | $isTypeUnion = false; 300 | case $type instanceof \ReflectionUnionType: 301 | $types = $type->getTypes(); 302 | break; 303 | default: 304 | throw new \LogicException('Unexpected return value of ReflectionParameter::getType'); 305 | } 306 | 307 | // If there is no type restriction, it matches 308 | if (empty($types)) { 309 | return true; 310 | } 311 | 312 | foreach ($types as $type) { 313 | 314 | if ($type instanceof \ReflectionIntersectionType) { 315 | foreach ($type->getTypes() as $typeToMatch) { 316 | assert($typeToMatch instanceof \ReflectionNamedType); 317 | $name = $typeToMatch->getName(); 318 | if (!($matches = (!$typeToMatch->isBuiltin() && $reason instanceof $name))) { 319 | break; 320 | } 321 | } 322 | assert(isset($matches)); 323 | } else { 324 | assert($type instanceof \ReflectionNamedType); 325 | $name = $type->getName(); 326 | $matches = !$type->isBuiltin() && $reason instanceof $name; 327 | } 328 | 329 | // If we look for a single match (union), we can return early on match 330 | // If we look for a full match (intersection), we can return early on mismatch 331 | if ($matches) { 332 | if ($isTypeUnion) { 333 | return true; 334 | } 335 | } else { 336 | if (!$isTypeUnion) { 337 | return false; 338 | } 339 | } 340 | } 341 | 342 | // If we look for a single match (union) and did not return early, we matched no type and are false 343 | // If we look for a full match (intersection) and did not return early, we matched all types and are true 344 | return $isTypeUnion ? false : true; 345 | } 346 | -------------------------------------------------------------------------------- /src/functions_include.php: -------------------------------------------------------------------------------- 1 |