├── src ├── functions_include.php ├── Exception │ ├── LengthException.php │ └── CompositeException.php ├── Deferred.php ├── Internal │ ├── CancellationQueue.php │ ├── FulfilledPromise.php │ └── RejectedPromise.php ├── PromiseInterface.php ├── Promise.php └── functions.php ├── LICENSE ├── composer.json ├── README.md ├── CHANGELOG.md └── documentation.md /src/functions_include.php: -------------------------------------------------------------------------------- 1 | throwables; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Deferred.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private $promise; 16 | 17 | /** @var callable(T):void */ 18 | private $resolveCallback; 19 | 20 | /** @var callable(\Throwable):void */ 21 | private $rejectCallback; 22 | 23 | /** 24 | * @param (callable(callable(T):void,callable(\Throwable):void):void)|null $canceller 25 | */ 26 | public function __construct(?callable $canceller = null) 27 | { 28 | $this->promise = new Promise(function ($resolve, $reject): void { 29 | $this->resolveCallback = $resolve; 30 | $this->rejectCallback = $reject; 31 | }, $canceller); 32 | } 33 | 34 | /** 35 | * @return PromiseInterface 36 | */ 37 | public function promise(): PromiseInterface 38 | { 39 | return $this->promise; 40 | } 41 | 42 | /** 43 | * @param T $value 44 | */ 45 | public function resolve(mixed $value): void 46 | { 47 | ($this->resolveCallback)($value); 48 | } 49 | 50 | public function reject(\Throwable $reason): void 51 | { 52 | ($this->rejectCallback)($reason); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Internal/CancellationQueue.php: -------------------------------------------------------------------------------- 1 | queue, $cancellable); 25 | 26 | if ($this->started && $length === 1) { 27 | $this->drain(); 28 | } 29 | } 30 | 31 | public function __invoke(): void 32 | { 33 | if ($this->started) { 34 | return; 35 | } 36 | 37 | $this->started = true; 38 | $this->drain(); 39 | } 40 | 41 | private function drain(): void 42 | { 43 | for ($i = \key($this->queue); isset($this->queue[$i]); $i++) { 44 | $cancellable = $this->queue[$i]; 45 | \assert(\method_exists($cancellable, 'cancel')); 46 | 47 | $exception = null; 48 | 49 | try { 50 | $cancellable->cancel(); 51 | } catch (\Throwable $exception) { 52 | } 53 | 54 | unset($this->queue[$i]); 55 | 56 | if ($exception) { 57 | throw $exception; 58 | } 59 | } 60 | 61 | $this->queue = []; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "internal/promise", 3 | "description": "A lightweight implementation of CommonJS Promises/A for PHP", 4 | "license": "MIT", 5 | "keywords": [ 6 | "promise", 7 | "promises" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Jan Sorgalla", 12 | "homepage": "https://sorgalla.com/", 13 | "email": "jsorgalla@gmail.com" 14 | }, 15 | { 16 | "name": "Christian Lück", 17 | "homepage": "https://clue.engineering/", 18 | "email": "christian@clue.engineering" 19 | }, 20 | { 21 | "name": "Cees-Jan Kiewiet", 22 | "homepage": "https://wyrihaximus.net/", 23 | "email": "reactphp@ceesjankiewiet.nl" 24 | }, 25 | { 26 | "name": "Chris Boden", 27 | "homepage": "https://cboden.dev/", 28 | "email": "cboden@gmail.com" 29 | } 30 | ], 31 | "require": { 32 | "php": ">=8.1.0" 33 | }, 34 | "replace": { 35 | "react/promise": "^3.0" 36 | }, 37 | "require-dev": { 38 | "phpstan/phpstan": "1.12.28", 39 | "phpunit/phpunit": "^10.5", 40 | "rector/rector": "^1.2", 41 | "spiral/code-style": "^2.2", 42 | "ta-tikoma/phpunit-architecture-test": "^0.8.5" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "React\\Promise\\": "src/" 47 | }, 48 | "files": [ 49 | "src/functions_include.php" 50 | ] 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "React\\Promise\\Tests\\": [ 55 | "tests/" 56 | ] 57 | } 58 | }, 59 | "config": { 60 | "audit": { 61 | "abandoned": "report" 62 | }, 63 | "sort-packages": true 64 | }, 65 | "scripts": { 66 | "cs:fix": "php-cs-fixer fix -v", 67 | "stan": "phpstan", 68 | "test": "phpunit --color=always --testdox", 69 | "test:arch": "phpunit --color=always --testdox --testsuite=Arch", 70 | "test:feat": "phpunit --color=always --testdox --testsuite=Feature", 71 | "test:unit": "phpunit --color=always --testdox --testsuite=Unit", 72 | "refactor": "rector process --config=rector.php", 73 | "test:cc": [ 74 | "@putenv XDEBUG_MODE=coverage", 75 | "phpunit --coverage-clover=runtime/phpunit/logs/clover.xml --color=always" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Internal/FulfilledPromise.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class FulfilledPromise implements PromiseInterface 18 | { 19 | /** @var T */ 20 | private $value; 21 | 22 | /** 23 | * @param T $value 24 | * @throws \InvalidArgumentException 25 | */ 26 | public function __construct(mixed $value = null) 27 | { 28 | if ($value instanceof PromiseInterface) { 29 | throw new \InvalidArgumentException('You cannot create React\Promise\FulfilledPromise with a promise. Use React\Promise\resolve($promiseOrValue) instead.'); 30 | } 31 | 32 | $this->value = $value; 33 | } 34 | 35 | /** 36 | * @template TFulfilled 37 | * @param ?(callable((T is void ? null : T)): (PromiseInterface|TFulfilled)) $onFulfilled 38 | * @return PromiseInterface<($onFulfilled is null ? T : TFulfilled)> 39 | */ 40 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface 41 | { 42 | if ($onFulfilled === null) { 43 | return $this; 44 | } 45 | 46 | try { 47 | /** 48 | * @var PromiseInterface|T $result 49 | */ 50 | $result = $onFulfilled($this->value); 51 | return resolve($result); 52 | } catch (\Throwable $exception) { 53 | return new RejectedPromise($exception); 54 | } 55 | } 56 | 57 | public function catch(callable $onRejected): PromiseInterface 58 | { 59 | return $this; 60 | } 61 | 62 | public function finally(callable $onFulfilledOrRejected): PromiseInterface 63 | { 64 | return $this->then( 65 | static fn(mixed $value): PromiseInterface => resolve($onFulfilledOrRejected()) 66 | ->then(static fn(): mixed => $value), 67 | ); 68 | } 69 | 70 | public function cancel(): void {} 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Promise 2 | 3 | A lightweight implementation of [CommonJS Promises/A][CommonJS Promises/A] for PHP. 4 | 5 | > [!NOTE] 6 | > This is a fork of [reactphp/promise][reactphp/promise]. 7 | > 8 | > Improvements over original reactphp/promise: 9 | > 10 | > - PHP 8.1+ compatibility 11 | > - `declare(strict_types=1);` in all PHP files 12 | > - `@yield` annotation in the PromiseInterface 13 | > 14 | > Version 3.x specific: 15 | > - Replaces `react/promise` v3 in Composer 16 | > - Rejection handler is reusable now. `error_log()` is still used by default. 17 | > - Removed `exit(255)` from RejectionPromise. 18 | > 19 | > Version 2.x specific: 20 | > - Replaces `react/promise` v2 in Composer 21 | > - Enhanced type annotations 22 | 23 | ## Install 24 | 25 | ```bash 26 | composer require internal/promise 27 | ``` 28 | 29 | [![PHP](https://img.shields.io/packagist/php-v/internal/promise.svg?style=flat-square&logo=php)](https://packagist.org/packages/internal/promise) 30 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/internal/promise.svg?style=flat-square&logo=packagist)](https://packagist.org/packages/internal/promise) 31 | [![License](https://img.shields.io/packagist/l/internal/promise.svg?style=flat-square)](LICENSE.md) 32 | [![Total Downloads](https://img.shields.io/packagist/dt/internal/promise.svg?style=flat-square)](https://packagist.org/packages/buggregator/trap) 33 | 34 | ## Tests 35 | 36 | To run the test suite, go to the project root and run: 37 | 38 | ```bash 39 | composer test 40 | ``` 41 | 42 | On top of this, we use PHPStan on max level to ensure type safety across the project: 43 | 44 | ```bash 45 | composer stan 46 | ``` 47 | 48 | ## Credits 49 | 50 | This fork is based on [reactphp/promise][reactphp/promise], which is a port of [when.js][when.js] 51 | by [Brian Cavalier][Brian Cavalier]. 52 | 53 | Also, large parts of the [documentation][documentation] have been ported from the when.js 54 | [Wiki][Wiki] and the 55 | [API docs][API docs]. 56 | 57 | [documentation]: documentation.md 58 | [CommonJS Promises/A]: http://wiki.commonjs.org/wiki/Promises/A 59 | [CI status]: https://img.shields.io/github/actions/workflow/status/internal/promise/ci.yml?branch=2.x 60 | [CI status link]: https://github.com/internal/promise/actions 61 | [installs]: https://img.shields.io/packagist/dt/internal/promise?color=blue&label=installs%20on%20Packagist 62 | [packagist link]: https://packagist.org/packages/internal/promise 63 | [Composer]: https://getcomposer.org 64 | [when.js]: https://github.com/cujojs/when 65 | [Brian Cavalier]: https://github.com/briancavalier 66 | [reactphp/promise]: https://github.com/reactphp/promise 67 | [Wiki]: https://github.com/cujojs/when/wiki 68 | [API docs]: https://github.com/cujojs/when/blob/master/docs/api.md 69 | -------------------------------------------------------------------------------- /src/Internal/RejectedPromise.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class RejectedPromise implements PromiseInterface 18 | { 19 | private bool $handled = false; 20 | private static ?\Closure $rejectionHandler = null; 21 | 22 | public function __construct(private readonly \Throwable $reason) {} 23 | 24 | public static function setRejectionHandler(?callable $handler): void 25 | { 26 | self::$rejectionHandler = $handler === null ? null : $handler(...); 27 | } 28 | 29 | /** 30 | * @template TRejected 31 | * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected 32 | * @return PromiseInterface<($onRejected is null ? never : TRejected)> 33 | */ 34 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface 35 | { 36 | if ($onRejected === null) { 37 | return $this; 38 | } 39 | 40 | $this->handled = true; 41 | 42 | try { 43 | return resolve($onRejected($this->reason)); 44 | } catch (\Throwable $exception) { 45 | return new RejectedPromise($exception); 46 | } 47 | } 48 | 49 | /** 50 | * @template TThrowable of \Throwable 51 | * @template TRejected 52 | * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected 53 | * @return PromiseInterface 54 | */ 55 | public function catch(callable $onRejected): PromiseInterface 56 | { 57 | if (!_checkTypehint($onRejected, $this->reason)) { 58 | return $this; 59 | } 60 | 61 | /** 62 | * @var callable(\Throwable):(PromiseInterface|TRejected) $onRejected 63 | */ 64 | return $this->then(null, $onRejected); 65 | } 66 | 67 | public function finally(callable $onFulfilledOrRejected): PromiseInterface 68 | { 69 | return $this->then( 70 | null, 71 | static fn(\Throwable $reason): PromiseInterface => resolve($onFulfilledOrRejected()) 72 | ->then(static fn(): PromiseInterface => new RejectedPromise($reason)), 73 | ); 74 | } 75 | 76 | public function cancel(): void 77 | { 78 | $this->handled = true; 79 | } 80 | 81 | /** 82 | * @deprecated 3.0.0 Use `catch()` instead 83 | * @see self::catch() 84 | */ 85 | public function otherwise(callable $onRejected): PromiseInterface 86 | { 87 | return $this->catch($onRejected); 88 | } 89 | 90 | /** 91 | * @deprecated 3.0.0 Use `always()` instead 92 | * @see self::always() 93 | */ 94 | public function always(callable $onFulfilledOrRejected): PromiseInterface 95 | { 96 | return $this->finally($onFulfilledOrRejected); 97 | } 98 | 99 | /** 100 | * @throws void 101 | */ 102 | public function __destruct() 103 | { 104 | if ($this->handled || self::$rejectionHandler === null) { 105 | return; 106 | } 107 | 108 | try { 109 | (self::$rejectionHandler)($this->reason); 110 | } catch (\Throwable $e) { 111 | \preg_match('/^([^:\s]++)(.*+)$/sm', (string) $e, $match); 112 | \assert(isset($match[1], $match[2])); 113 | $message = 'Fatal error: Uncaught ' . $match[1] . ' from unhandled promise rejection handler' . $match[2]; 114 | 115 | \error_log($message); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/PromiseInterface.php: -------------------------------------------------------------------------------- 1 | |TFulfilled)) $onFulfilled 41 | * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected 42 | * @return PromiseInterface<($onRejected is null ? ($onFulfilled is null ? T : TFulfilled) : ($onFulfilled is null ? T|TRejected : TFulfilled|TRejected))> 43 | */ 44 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface; 45 | 46 | /** 47 | * Registers a rejection handler for promise. It is a shortcut for: 48 | * 49 | * ```php 50 | * $promise->then(null, $onRejected); 51 | * ``` 52 | * 53 | * Additionally, you can type hint the `$reason` argument of `$onRejected` to catch 54 | * only specific errors. 55 | * 56 | * @template TThrowable of \Throwable 57 | * @template TRejected 58 | * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected 59 | * @return PromiseInterface 60 | */ 61 | public function catch(callable $onRejected): PromiseInterface; 62 | 63 | /** 64 | * Allows you to execute "cleanup" type tasks in a promise chain. 65 | * 66 | * It arranges for `$onFulfilledOrRejected` to be called, with no arguments, 67 | * when the promise is either fulfilled or rejected. 68 | * 69 | * * If `$promise` fulfills, and `$onFulfilledOrRejected` returns successfully, 70 | * `$newPromise` will fulfill with the same value as `$promise`. 71 | * * If `$promise` fulfills, and `$onFulfilledOrRejected` throws or returns a 72 | * rejected promise, `$newPromise` will reject with the thrown exception or 73 | * rejected promise's reason. 74 | * * If `$promise` rejects, and `$onFulfilledOrRejected` returns successfully, 75 | * `$newPromise` will reject with the same reason as `$promise`. 76 | * * If `$promise` rejects, and `$onFulfilledOrRejected` throws or returns a 77 | * rejected promise, `$newPromise` will reject with the thrown exception or 78 | * rejected promise's reason. 79 | * 80 | * `finally()` behaves similarly to the synchronous finally statement. When combined 81 | * with `catch()`, `finally()` allows you to write code that is similar to the familiar 82 | * synchronous catch/finally pair. 83 | * 84 | * Consider the following synchronous code: 85 | * 86 | * ```php 87 | * try { 88 | * return doSomething(); 89 | * } catch(\Exception $e) { 90 | * return handleError($e); 91 | * } finally { 92 | * cleanup(); 93 | * } 94 | * ``` 95 | * 96 | * Similar asynchronous code (with `doSomething()` that returns a promise) can be 97 | * written: 98 | * 99 | * ```php 100 | * return doSomething() 101 | * ->catch('handleError') 102 | * ->finally('cleanup'); 103 | * ``` 104 | * 105 | * @param callable(): (void|PromiseInterface) $onFulfilledOrRejected 106 | * @return PromiseInterface 107 | */ 108 | public function finally(callable $onFulfilledOrRejected): PromiseInterface; 109 | 110 | /** 111 | * The `cancel()` method notifies the creator of the promise that there is no 112 | * further interest in the results of the operation. 113 | * 114 | * Once a promise is settled (either fulfilled or rejected), calling `cancel()` on 115 | * a promise has no effect. 116 | */ 117 | public function cancel(): void; 118 | 119 | /** 120 | * [Deprecated] Registers a rejection handler for a promise. 121 | * 122 | * This method continues to exist only for BC reasons and to ease upgrading 123 | * between versions. It is an alias for: 124 | * 125 | * ```php 126 | * $promise->catch($onRejected); 127 | * ``` 128 | * 129 | * @template TThrowable of \Throwable 130 | * @template TRejected 131 | * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected 132 | * @return PromiseInterface 133 | * @deprecated 3.0.0 Use catch() instead 134 | * @see self::catch() 135 | */ 136 | public function otherwise(callable $onRejected): PromiseInterface; 137 | 138 | /** 139 | * [Deprecated] Allows you to execute "cleanup" type tasks in a promise chain. 140 | * 141 | * This method continues to exist only for BC reasons and to ease upgrading 142 | * between versions. It is an alias for: 143 | * 144 | * ```php 145 | * $promise->finally($onFulfilledOrRejected); 146 | * ``` 147 | * 148 | * @param callable(): (void|PromiseInterface) $onFulfilledOrRejected 149 | * @return PromiseInterface 150 | * @deprecated 3.0.0 Use finally() instead 151 | * @see self::finally() 152 | */ 153 | public function always(callable $onFulfilledOrRejected): PromiseInterface; 154 | } 155 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Promise.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class Promise implements PromiseInterface 14 | { 15 | /** @var (callable(callable(T):void,callable(\Throwable):void):void)|null */ 16 | private $canceller; 17 | 18 | /** @var ?PromiseInterface */ 19 | private $result; 20 | 21 | /** @var list):void> */ 22 | private $handlers = []; 23 | 24 | /** @var int */ 25 | private $requiredCancelRequests = 0; 26 | 27 | /** @var bool */ 28 | private $cancelled = false; 29 | 30 | /** 31 | * @param callable(callable(T):void,callable(\Throwable):void):void $resolver 32 | * @param (callable(callable(T):void,callable(\Throwable):void):void)|null $canceller 33 | */ 34 | public function __construct(callable $resolver, ?callable $canceller = null) 35 | { 36 | $this->canceller = $canceller; 37 | 38 | // Explicitly overwrite arguments with null values before invoking 39 | // resolver function. This ensure that these arguments do not show up 40 | // in the stack trace in PHP 7+ only. 41 | $cb = $resolver; 42 | $resolver = $canceller = null; 43 | $this->call($cb); 44 | } 45 | 46 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface 47 | { 48 | if ($this->result !== null) { 49 | return $this->result->then($onFulfilled, $onRejected); 50 | } 51 | 52 | if ($this->canceller === null) { 53 | return new self($this->resolver($onFulfilled, $onRejected)); 54 | } 55 | 56 | // This promise has a canceller, so we create a new child promise which 57 | // has a canceller that invokes the parent canceller if all other 58 | // followers are also cancelled. We keep a reference to this promise 59 | // instance for the static canceller function and clear this to avoid 60 | // keeping a cyclic reference between parent and follower. 61 | $parent = $this; 62 | ++$parent->requiredCancelRequests; 63 | 64 | return new self( 65 | $this->resolver($onFulfilled, $onRejected), 66 | static function () use (&$parent): void { 67 | \assert($parent instanceof self); 68 | --$parent->requiredCancelRequests; 69 | 70 | if ($parent->requiredCancelRequests <= 0) { 71 | $parent->cancel(); 72 | } 73 | 74 | $parent = null; 75 | }, 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 | return $this->then(null, static function (\Throwable $reason) use ($onRejected) { 88 | if (!_checkTypehint($onRejected, $reason)) { 89 | return new RejectedPromise($reason); 90 | } 91 | 92 | /** 93 | * @var callable(\Throwable):(PromiseInterface|TRejected) $onRejected 94 | */ 95 | return $onRejected($reason); 96 | }); 97 | } 98 | 99 | public function finally(callable $onFulfilledOrRejected): PromiseInterface 100 | { 101 | return $this 102 | ->then( 103 | static fn($value): PromiseInterface => resolve($onFulfilledOrRejected()) 104 | ->then(static fn(): mixed => $value), 105 | static fn(\Throwable $reason): PromiseInterface => resolve($onFulfilledOrRejected()) 106 | ->then(static fn(): RejectedPromise => new RejectedPromise($reason)), 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 ($this->result !== null) { 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 || $root->result !== null) { 131 | return; 132 | } 133 | 134 | $root->requiredCancelRequests--; 135 | 136 | if ($root->requiredCancelRequests <= 0) { 137 | $parentCanceller = $root->cancel(...); 138 | } 139 | } 140 | 141 | if ($canceller !== null) { 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 ( 173 | PromiseInterface $promise, 174 | ) use ($onFulfilled, $onRejected, $resolve, $reject): void { 175 | $promise = $promise->then($onFulfilled, $onRejected); 176 | 177 | if ($promise instanceof self && $promise->result === null) { 178 | $promise->handlers[] = static function (PromiseInterface $promise) use ($resolve, $reject): void { 179 | $promise->then($resolve, $reject); 180 | }; 181 | } else { 182 | $promise->then($resolve, $reject); 183 | } 184 | }; 185 | }; 186 | } 187 | 188 | private function reject(\Throwable $reason): void 189 | { 190 | if ($this->result !== null) { 191 | return; 192 | } 193 | 194 | $this->settle(reject($reason)); 195 | } 196 | 197 | /** 198 | * @param PromiseInterface $result 199 | */ 200 | private function settle(PromiseInterface $result): void 201 | { 202 | $result = $this->unwrap($result); 203 | 204 | if ($result === $this) { 205 | $result = new RejectedPromise( 206 | new \LogicException('Cannot resolve a promise with itself.'), 207 | ); 208 | } 209 | 210 | if ($result instanceof self) { 211 | $result->requiredCancelRequests++; 212 | } else { 213 | // Unset canceller only when not following a pending promise 214 | $this->canceller = null; 215 | } 216 | 217 | $handlers = $this->handlers; 218 | 219 | $this->handlers = []; 220 | $this->result = $result; 221 | 222 | foreach ($handlers as $handler) { 223 | $handler($result); 224 | } 225 | 226 | // Forward cancellation to rejected promise to avoid reporting unhandled rejection 227 | if ($this->cancelled && $result instanceof RejectedPromise) { 228 | $result->cancel(); 229 | } 230 | } 231 | 232 | /** 233 | * @param PromiseInterface $promise 234 | * @return PromiseInterface 235 | */ 236 | private function unwrap(PromiseInterface $promise): PromiseInterface 237 | { 238 | while ($promise instanceof self && $promise->result !== null) { 239 | /** @var PromiseInterface $promise */ 240 | $promise = $promise->result; 241 | } 242 | 243 | return $promise; 244 | } 245 | 246 | /** 247 | * @param callable(callable(mixed):void,callable(\Throwable):void):void $cb 248 | */ 249 | private function call(callable $cb): void 250 | { 251 | // Explicitly overwrite argument with null value. This ensure that this 252 | // argument does not show up in the stack trace in PHP 7+ only. 253 | $callback = $cb; 254 | $cb = null; 255 | 256 | // Use reflection to inspect number of arguments expected by this callback. 257 | // We did some careful benchmarking here: Using reflection to avoid unneeded 258 | // function arguments is actually faster than blindly passing them. 259 | // Also, this helps avoiding unnecessary function arguments in the call stack 260 | // if the callback creates an Exception (creating garbage cycles). 261 | if (\is_array($callback)) { 262 | $ref = new \ReflectionMethod($callback[0], $callback[1]); 263 | } elseif (\is_object($callback) && !$callback instanceof \Closure) { 264 | $ref = new \ReflectionMethod($callback, '__invoke'); 265 | } else { 266 | \assert($callback instanceof \Closure || \is_string($callback)); 267 | $ref = new \ReflectionFunction($callback); 268 | } 269 | $args = $ref->getNumberOfParameters(); 270 | 271 | try { 272 | if ($args === 0) { 273 | $callback(); 274 | } else { 275 | // Keep references to this promise instance for the static resolve/reject functions. 276 | // By using static callbacks that are not bound to this instance 277 | // and passing the target promise instance by reference, we can 278 | // still execute its resolving logic and still clear this 279 | // reference when settling the promise. This helps avoiding 280 | // garbage cycles if any callback creates an Exception. 281 | // These assumptions are covered by the test suite, so if you ever feel like 282 | // refactoring this, go ahead, any alternative suggestions are welcome! 283 | $target = & $this; 284 | 285 | $callback( 286 | static function ($value) use (&$target): void { 287 | if ($target !== null) { 288 | $target->settle(resolve($value)); 289 | $target = null; 290 | } 291 | }, 292 | static function (\Throwable $reason) use (&$target): void { 293 | if ($target !== null) { 294 | $target->reject($reason); 295 | $target = null; 296 | } 297 | }, 298 | ); 299 | } 300 | } catch (\Throwable $e) { 301 | $target = null; 302 | $this->reject($e); 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | |T $promiseOrValue 24 | * @return PromiseInterface 25 | */ 26 | function resolve(mixed $promiseOrValue): PromiseInterface 27 | { 28 | if ($promiseOrValue instanceof PromiseInterface) { 29 | return $promiseOrValue; 30 | } 31 | 32 | if (\is_object($promiseOrValue) && \method_exists($promiseOrValue, 'then')) { 33 | $canceller = null; 34 | 35 | if (\method_exists($promiseOrValue, 'cancel')) { 36 | $canceller = [$promiseOrValue, 'cancel']; 37 | \assert(\is_callable($canceller)); 38 | } 39 | 40 | /** @var Promise */ 41 | return new Promise(static function (callable $resolve, callable $reject) use ($promiseOrValue): void { 42 | $promiseOrValue->then($resolve, $reject); 43 | }, $canceller); 44 | } 45 | 46 | return new FulfilledPromise($promiseOrValue); 47 | } 48 | 49 | /** 50 | * Creates a rejected promise for the supplied `$reason`. 51 | * 52 | * If `$reason` is a value, it will be the rejection value of the 53 | * returned promise. 54 | * 55 | * If `$reason` is a promise, its completion value will be the rejected 56 | * value of the returned promise. 57 | * 58 | * This can be useful in situations where you need to reject a promise without 59 | * throwing an exception. For example, it allows you to propagate a rejection with 60 | * the value of another promise. 61 | * 62 | * @return PromiseInterface 63 | */ 64 | function reject(\Throwable $reason): PromiseInterface 65 | { 66 | return new RejectedPromise($reason); 67 | } 68 | 69 | /** 70 | * Returns a promise that will resolve only once all the items in 71 | * `$promisesOrValues` have resolved. The resolution value of the returned promise 72 | * will be an array containing the resolution values of each of the items in 73 | * `$promisesOrValues`. 74 | * 75 | * @template T 76 | * @param iterable|T> $promisesOrValues 77 | * @return PromiseInterface> 78 | */ 79 | function all(iterable $promisesOrValues): PromiseInterface 80 | { 81 | $cancellationQueue = new Internal\CancellationQueue(); 82 | 83 | /** @var Promise> */ 84 | return new Promise(static function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { 85 | $toResolve = 0; 86 | /** @var bool */ 87 | $continue = true; 88 | $values = []; 89 | 90 | foreach ($promisesOrValues as $i => $promiseOrValue) { 91 | $cancellationQueue->enqueue($promiseOrValue); 92 | $values[$i] = null; 93 | ++$toResolve; 94 | 95 | resolve($promiseOrValue)->then( 96 | static function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void { 97 | $values[$i] = $value; 98 | 99 | if (--$toResolve === 0 && !$continue) { 100 | $resolve($values); 101 | } 102 | }, 103 | static function (\Throwable $reason) use (&$continue, $reject): void { 104 | $continue = false; 105 | $reject($reason); 106 | }, 107 | ); 108 | 109 | if (!$continue && !\is_array($promisesOrValues)) { 110 | break; 111 | } 112 | } 113 | 114 | $continue = false; 115 | if ($toResolve === 0) { 116 | $resolve($values); 117 | } 118 | }, $cancellationQueue); 119 | } 120 | 121 | /** 122 | * Initiates a competitive race that allows one winner. Returns a promise which is 123 | * resolved in the same way the first settled promise resolves. 124 | * 125 | * The returned promise will become **infinitely pending** if `$promisesOrValues` 126 | * contains 0 items. 127 | * 128 | * @template T 129 | * @param iterable|T> $promisesOrValues 130 | * @return PromiseInterface 131 | */ 132 | function race(iterable $promisesOrValues): PromiseInterface 133 | { 134 | $cancellationQueue = new Internal\CancellationQueue(); 135 | 136 | /** @var Promise */ 137 | return new Promise(static function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { 138 | $continue = true; 139 | 140 | foreach ($promisesOrValues as $promiseOrValue) { 141 | $cancellationQueue->enqueue($promiseOrValue); 142 | 143 | resolve($promiseOrValue)->then($resolve, $reject)->finally(static function () use (&$continue): void { 144 | $continue = false; 145 | }); 146 | 147 | if (!$continue && !\is_array($promisesOrValues)) { 148 | break; 149 | } 150 | } 151 | }, $cancellationQueue); 152 | } 153 | 154 | /** 155 | * Returns a promise that will resolve when any one of the items in 156 | * `$promisesOrValues` resolves. The resolution value of the returned promise 157 | * will be the resolution value of the triggering item. 158 | * 159 | * The returned promise will only reject if *all* items in `$promisesOrValues` are 160 | * rejected. The rejection value will be an array of all rejection reasons. 161 | * 162 | * The returned promise will also reject with a `React\Promise\Exception\LengthException` 163 | * if `$promisesOrValues` contains 0 items. 164 | * 165 | * @template T 166 | * @param iterable|T> $promisesOrValues 167 | * @return PromiseInterface 168 | */ 169 | function any(iterable $promisesOrValues): PromiseInterface 170 | { 171 | $cancellationQueue = new Internal\CancellationQueue(); 172 | 173 | /** @var Promise */ 174 | return new Promise(static function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { 175 | $toReject = 0; 176 | $continue = true; 177 | $reasons = []; 178 | 179 | foreach ($promisesOrValues as $i => $promiseOrValue) { 180 | $cancellationQueue->enqueue($promiseOrValue); 181 | ++$toReject; 182 | 183 | resolve($promiseOrValue)->then( 184 | static function ($value) use ($resolve, &$continue): void { 185 | $continue = false; 186 | $resolve($value); 187 | }, 188 | static function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continue): void { 189 | $reasons[$i] = $reason; 190 | 191 | if (--$toReject === 0 && !$continue) { 192 | $reject(new CompositeException( 193 | $reasons, 194 | 'All promises rejected.', 195 | )); 196 | } 197 | }, 198 | ); 199 | 200 | if (!$continue && !\is_array($promisesOrValues)) { 201 | break; 202 | } 203 | } 204 | 205 | $continue = false; 206 | if ($toReject === 0 && !$reasons) { 207 | $reject(new Exception\LengthException( 208 | 'Must contain at least 1 item but contains only 0 items.', 209 | )); 210 | } elseif ($toReject === 0) { 211 | $reject(new CompositeException( 212 | $reasons, 213 | 'All promises rejected.', 214 | )); 215 | } 216 | }, $cancellationQueue); 217 | } 218 | 219 | /** 220 | * Sets the global rejection handler for unhandled promise rejections. 221 | * 222 | * Note that rejected promises should always be handled similar to how any 223 | * exceptions should always be caught in a `try` + `catch` block. If you remove 224 | * the last reference to a rejected promise that has not been handled, it will 225 | * report an unhandled promise rejection. See also the [`reject()` function](#reject) 226 | * for more details. 227 | * 228 | * The `?callable $callback` argument MUST be a valid callback function that 229 | * accepts a single `Throwable` argument or a `null` value to restore the 230 | * default promise rejection handler. The return value of the callback function 231 | * will be ignored and has no effect, so you SHOULD return a `void` value. The 232 | * callback function MUST NOT throw or the program will be terminated with a 233 | * fatal error. 234 | * 235 | * The function returns the previous rejection handler or `null` if using the 236 | * default promise rejection handler. 237 | * 238 | * The default promise rejection handler will log an error message plus its 239 | * stack trace: 240 | * 241 | * ```php 242 | * // Unhandled promise rejection with RuntimeException: Unhandled in example.php:2 243 | * React\Promise\reject(new RuntimeException('Unhandled')); 244 | * ``` 245 | * 246 | * The promise rejection handler may be used to use customize the log message or 247 | * write to custom log targets. As a rule of thumb, this function should only be 248 | * used as a last resort and promise rejections are best handled with either the 249 | * [`then()` method](#promiseinterfacethen), the 250 | * [`catch()` method](#promiseinterfacecatch), or the 251 | * [`finally()` method](#promiseinterfacefinally). 252 | * See also the [`reject()` function](#reject) for more details. 253 | * 254 | * @param callable(\Throwable):mixed|null $callback 255 | * @return callable(\Throwable):mixed|null 256 | */ 257 | function set_rejection_handler(?callable $callback): ?callable 258 | { 259 | static $current = null; 260 | $previous = $current; 261 | $current = $callback; 262 | RejectedPromise::setRejectionHandler($current); 263 | 264 | return $previous; 265 | } 266 | 267 | set_rejection_handler(static fn(\Throwable $reason) => \error_log('Unhandled promise rejection with ' . $reason)); 268 | 269 | /** 270 | * @internal 271 | */ 272 | function _checkTypehint(callable $callback, \Throwable $reason): bool 273 | { 274 | if (\is_array($callback)) { 275 | $callbackReflection = new \ReflectionMethod($callback[0], $callback[1]); 276 | } elseif (\is_object($callback) && !$callback instanceof \Closure) { 277 | $callbackReflection = new \ReflectionMethod($callback, '__invoke'); 278 | } else { 279 | \assert($callback instanceof \Closure || \is_string($callback)); 280 | $callbackReflection = new \ReflectionFunction($callback); 281 | } 282 | 283 | $parameters = $callbackReflection->getParameters(); 284 | 285 | if (!isset($parameters[0])) { 286 | return true; 287 | } 288 | 289 | $expectedException = $parameters[0]; 290 | 291 | // Extract the type of the argument and handle different possibilities 292 | $type = $expectedException->getType(); 293 | 294 | $isTypeUnion = true; 295 | $types = []; 296 | 297 | switch (true) { 298 | case $type === null: 299 | break; 300 | case $type instanceof \ReflectionNamedType: 301 | $types = [$type]; 302 | break; 303 | case $type instanceof \ReflectionIntersectionType: 304 | $isTypeUnion = false; 305 | // no break 306 | case $type instanceof \ReflectionUnionType: 307 | $types = $type->getTypes(); 308 | break; 309 | default: 310 | throw new \LogicException('Unexpected return value of ReflectionParameter::getType'); 311 | } 312 | 313 | // If there is no type restriction, it matches 314 | if (empty($types)) { 315 | return true; 316 | } 317 | 318 | foreach ($types as $type) { 319 | 320 | if ($type instanceof \ReflectionIntersectionType) { 321 | foreach ($type->getTypes() as $typeToMatch) { 322 | \assert($typeToMatch instanceof \ReflectionNamedType); 323 | $name = $typeToMatch->getName(); 324 | if (!($matches = (!$typeToMatch->isBuiltin() && $reason instanceof $name))) { 325 | break; 326 | } 327 | } 328 | \assert(isset($matches)); 329 | } else { 330 | \assert($type instanceof \ReflectionNamedType); 331 | $name = $type->getName(); 332 | $matches = !$type->isBuiltin() && $reason instanceof $name; 333 | } 334 | 335 | // If we look for a single match (union), we can return early on match 336 | // If we look for a full match (intersection), we can return early on mismatch 337 | if ($matches) { 338 | if ($isTypeUnion) { 339 | return true; 340 | } 341 | } else { 342 | if (!$isTypeUnion) { 343 | return false; 344 | } 345 | } 346 | } 347 | 348 | // If we look for a single match (union) and did not return early, we matched no type and are false 349 | // If we look for a full match (intersection) and did not return early, we matched all types and are true 350 | return $isTypeUnion ? false : true; 351 | } 352 | -------------------------------------------------------------------------------- /documentation.md: -------------------------------------------------------------------------------- 1 | # Promise 2 | 3 | A lightweight implementation of [CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Introduction](#introduction) 8 | 2. [Concepts](#concepts) 9 | * [Deferred](#deferred) 10 | * [Promise](#promise-1) 11 | 3. [API](#api) 12 | * [Deferred](#deferred-1) 13 | * [Deferred::promise()](#deferredpromise) 14 | * [Deferred::resolve()](#deferredresolve) 15 | * [Deferred::reject()](#deferredreject) 16 | * [PromiseInterface](#promiseinterface) 17 | * [PromiseInterface::then()](#promiseinterfacethen) 18 | * [PromiseInterface::catch()](#promiseinterfacecatch) 19 | * [PromiseInterface::finally()](#promiseinterfacefinally) 20 | * [PromiseInterface::cancel()](#promiseinterfacecancel) 21 | * [~~PromiseInterface::otherwise()~~](#promiseinterfaceotherwise) 22 | * [~~PromiseInterface::always()~~](#promiseinterfacealways) 23 | * [Promise](#promise-2) 24 | * [Functions](#functions) 25 | * [resolve()](#resolve) 26 | * [reject()](#reject) 27 | * [all()](#all) 28 | * [race()](#race) 29 | * [any()](#any) 30 | * [set_rejection_handler()](#set_rejection_handler) 31 | 4. [Examples](#examples) 32 | * [How to use Deferred](#how-to-use-deferred) 33 | * [How promise forwarding works](#how-promise-forwarding-works) 34 | * [Resolution forwarding](#resolution-forwarding) 35 | * [Rejection forwarding](#rejection-forwarding) 36 | * [Mixed resolution and rejection forwarding](#mixed-resolution-and-rejection-forwarding) 37 | 38 | ## Introduction 39 | 40 | Promise is a library implementing 41 | [CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. 42 | 43 | It also provides several other useful promise-related concepts, such as joining 44 | multiple promises and mapping and reducing collections of promises. 45 | 46 | If you've never heard about promises before, 47 | [read this first](https://gist.github.com/domenic/3889970). 48 | 49 | ## Concepts 50 | 51 | ### Deferred 52 | 53 | A **Deferred** represents a computation or unit of work that may not have 54 | completed yet. Typically (but not always), that computation will be something 55 | that executes asynchronously and completes at some point in the future. 56 | 57 | ### Promise 58 | 59 | While a deferred represents the computation itself, a **Promise** represents 60 | the result of that computation. Thus, each deferred has a promise that acts as 61 | a placeholder for its actual result. 62 | 63 | ## API 64 | 65 | ### Deferred 66 | 67 | A deferred represents an operation whose resolution is pending. It has separate 68 | promise and resolver parts. 69 | 70 | ```php 71 | $deferred = new React\Promise\Deferred(); 72 | 73 | $promise = $deferred->promise(); 74 | 75 | $deferred->resolve(mixed $value); 76 | $deferred->reject(\Throwable $reason); 77 | ``` 78 | 79 | The `promise` method returns the promise of the deferred. 80 | 81 | The `resolve` and `reject` methods control the state of the deferred. 82 | 83 | The constructor of the `Deferred` accepts an optional `$canceller` argument. 84 | See [Promise](#promise-2) for more information. 85 | 86 | #### Deferred::promise() 87 | 88 | ```php 89 | $promise = $deferred->promise(); 90 | ``` 91 | 92 | Returns the promise of the deferred, which you can hand out to others while 93 | keeping the authority to modify its state to yourself. 94 | 95 | #### Deferred::resolve() 96 | 97 | ```php 98 | $deferred->resolve(mixed $value); 99 | ``` 100 | 101 | Resolves the promise returned by `promise()`. All consumers are notified by 102 | having `$onFulfilled` (which they registered via `$promise->then()`) called with 103 | `$value`. 104 | 105 | If `$value` itself is a promise, the promise will transition to the state of 106 | this promise once it is resolved. 107 | 108 | See also the [`resolve()` function](#resolve). 109 | 110 | #### Deferred::reject() 111 | 112 | ```php 113 | $deferred->reject(\Throwable $reason); 114 | ``` 115 | 116 | Rejects the promise returned by `promise()`, signalling that the deferred's 117 | computation failed. 118 | All consumers are notified by having `$onRejected` (which they registered via 119 | `$promise->then()`) called with `$reason`. 120 | 121 | See also the [`reject()` function](#reject). 122 | 123 | ### PromiseInterface 124 | 125 | The promise interface provides the common interface for all promise 126 | implementations. 127 | See [Promise](#promise-2) for the only public implementation exposed by this 128 | package. 129 | 130 | A promise represents an eventual outcome, which is either fulfillment (success) 131 | and an associated value, or rejection (failure) and an associated reason. 132 | 133 | Once in the fulfilled or rejected state, a promise becomes immutable. 134 | Neither its state nor its result (or error) can be modified. 135 | 136 | #### PromiseInterface::then() 137 | 138 | ```php 139 | $transformedPromise = $promise->then(callable $onFulfilled = null, callable $onRejected = null); 140 | ``` 141 | 142 | Transforms a promise's value by applying a function to the promise's fulfillment 143 | or rejection value. Returns a new promise for the transformed result. 144 | 145 | The `then()` method registers new fulfilled and rejection handlers with a promise 146 | (all parameters are optional): 147 | 148 | * `$onFulfilled` will be invoked once the promise is fulfilled and passed 149 | the result as the first argument. 150 | * `$onRejected` will be invoked once the promise is rejected and passed the 151 | reason as the first argument. 152 | 153 | It returns a new promise that will fulfill with the return value of either 154 | `$onFulfilled` or `$onRejected`, whichever is called, or will reject with 155 | the thrown exception if either throws. 156 | 157 | A promise makes the following guarantees about handlers registered in 158 | the same call to `then()`: 159 | 160 | 1. Only one of `$onFulfilled` or `$onRejected` will be called, 161 | never both. 162 | 2. `$onFulfilled` and `$onRejected` will never be called more 163 | than once. 164 | 165 | #### See also 166 | 167 | * [resolve()](#resolve) - Creating a resolved promise 168 | * [reject()](#reject) - Creating a rejected promise 169 | 170 | #### PromiseInterface::catch() 171 | 172 | ```php 173 | $promise->catch(callable $onRejected); 174 | ``` 175 | 176 | Registers a rejection handler for promise. It is a shortcut for: 177 | 178 | ```php 179 | $promise->then(null, $onRejected); 180 | ``` 181 | 182 | Additionally, you can type hint the `$reason` argument of `$onRejected` to catch 183 | only specific errors. 184 | 185 | ```php 186 | $promise 187 | ->catch(function (\RuntimeException $reason) { 188 | // Only catch \RuntimeException instances 189 | // All other types of errors will propagate automatically 190 | }) 191 | ->catch(function (\Throwable $reason) { 192 | // Catch other errors 193 | }); 194 | ``` 195 | 196 | #### PromiseInterface::finally() 197 | 198 | ```php 199 | $newPromise = $promise->finally(callable $onFulfilledOrRejected); 200 | ``` 201 | 202 | Allows you to execute "cleanup" type tasks in a promise chain. 203 | 204 | It arranges for `$onFulfilledOrRejected` to be called, with no arguments, 205 | when the promise is either fulfilled or rejected. 206 | 207 | * If `$promise` fulfills, and `$onFulfilledOrRejected` returns successfully, 208 | `$newPromise` will fulfill with the same value as `$promise`. 209 | * If `$promise` fulfills, and `$onFulfilledOrRejected` throws or returns a 210 | rejected promise, `$newPromise` will reject with the thrown exception or 211 | rejected promise's reason. 212 | * If `$promise` rejects, and `$onFulfilledOrRejected` returns successfully, 213 | `$newPromise` will reject with the same reason as `$promise`. 214 | * If `$promise` rejects, and `$onFulfilledOrRejected` throws or returns a 215 | rejected promise, `$newPromise` will reject with the thrown exception or 216 | rejected promise's reason. 217 | 218 | `finally()` behaves similarly to the synchronous finally statement. When combined 219 | with `catch()`, `finally()` allows you to write code that is similar to the familiar 220 | synchronous catch/finally pair. 221 | 222 | Consider the following synchronous code: 223 | 224 | ```php 225 | try { 226 | return doSomething(); 227 | } catch (\Throwable $e) { 228 | return handleError($e); 229 | } finally { 230 | cleanup(); 231 | } 232 | ``` 233 | 234 | Similar asynchronous code (with `doSomething()` that returns a promise) can be 235 | written: 236 | 237 | ```php 238 | return doSomething() 239 | ->catch('handleError') 240 | ->finally('cleanup'); 241 | ``` 242 | 243 | #### PromiseInterface::cancel() 244 | 245 | ``` php 246 | $promise->cancel(); 247 | ``` 248 | 249 | The `cancel()` method notifies the creator of the promise that there is no 250 | further interest in the results of the operation. 251 | 252 | Once a promise is settled (either fulfilled or rejected), calling `cancel()` on 253 | a promise has no effect. 254 | 255 | #### ~~PromiseInterface::otherwise()~~ 256 | 257 | > Deprecated since v3.0.0, see [`catch()`](#promiseinterfacecatch) instead. 258 | 259 | The `otherwise()` method registers a rejection handler for a promise. 260 | 261 | This method continues to exist only for BC reasons and to ease upgrading 262 | between versions. It is an alias for: 263 | 264 | ```php 265 | $promise->catch($onRejected); 266 | ``` 267 | 268 | #### ~~PromiseInterface::always()~~ 269 | 270 | > Deprecated since v3.0.0, see [`finally()`](#promiseinterfacefinally) instead. 271 | 272 | The `always()` method allows you to execute "cleanup" type tasks in a promise chain. 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->finally($onFulfilledOrRejected); 279 | ``` 280 | 281 | ### Promise 282 | 283 | Creates a promise whose state is controlled by the functions passed to 284 | `$resolver`. 285 | 286 | ```php 287 | $resolver = function (callable $resolve, callable $reject) { 288 | // Do some work, possibly asynchronously, and then 289 | // resolve or reject. 290 | 291 | $resolve($awesomeResult); 292 | // or throw new Exception('Promise rejected'); 293 | // or $resolve($anotherPromise); 294 | // or $reject($nastyError); 295 | }; 296 | 297 | $canceller = function () { 298 | // Cancel/abort any running operations like network connections, streams etc. 299 | 300 | // Reject promise by throwing an exception 301 | throw new Exception('Promise cancelled'); 302 | }; 303 | 304 | $promise = new React\Promise\Promise($resolver, $canceller); 305 | ``` 306 | 307 | The promise constructor receives a resolver function and an optional canceller 308 | function which both will be called with two arguments: 309 | 310 | * `$resolve($value)` - Primary function that seals the fate of the 311 | returned promise. Accepts either a non-promise value, or another promise. 312 | When called with a non-promise value, fulfills promise with that value. 313 | When called with another promise, e.g. `$resolve($otherPromise)`, promise's 314 | fate will be equivalent to that of `$otherPromise`. 315 | * `$reject($reason)` - Function that rejects the promise. It is recommended to 316 | just throw an exception instead of using `$reject()`. 317 | 318 | If the resolver or canceller throw an exception, the promise will be rejected 319 | with that thrown exception as the rejection reason. 320 | 321 | The resolver function will be called immediately, the canceller function only 322 | once all consumers called the `cancel()` method of the promise. 323 | 324 | ### Functions 325 | 326 | Useful functions for creating and joining collections of promises. 327 | 328 | All functions working on promise collections (like `all()`, `race()`, 329 | etc.) support cancellation. This means, if you call `cancel()` on the returned 330 | promise, all promises in the collection are cancelled. 331 | 332 | #### resolve() 333 | 334 | ```php 335 | $promise = React\Promise\resolve(mixed $promiseOrValue); 336 | ``` 337 | 338 | Creates a promise for the supplied `$promiseOrValue`. 339 | 340 | If `$promiseOrValue` is a value, it will be the resolution value of the 341 | returned promise. 342 | 343 | If `$promiseOrValue` is a thenable (any object that provides a `then()` method), 344 | a trusted promise that follows the state of the thenable is returned. 345 | 346 | If `$promiseOrValue` is a promise, it will be returned as is. 347 | 348 | The resulting `$promise` implements the [`PromiseInterface`](#promiseinterface) 349 | and can be consumed like any other promise: 350 | 351 | ```php 352 | $promise = React\Promise\resolve(42); 353 | 354 | $promise->then(function (int $result): void { 355 | var_dump($result); 356 | }, function (\Throwable $e): void { 357 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 358 | }); 359 | ``` 360 | 361 | #### reject() 362 | 363 | ```php 364 | $promise = React\Promise\reject(\Throwable $reason); 365 | ``` 366 | 367 | Creates a rejected promise for the supplied `$reason`. 368 | 369 | Note that the [`\Throwable`](https://www.php.net/manual/en/class.throwable.php) interface introduced in PHP 7 covers 370 | both user land [`\Exception`](https://www.php.net/manual/en/class.exception.php)'s and 371 | [`\Error`](https://www.php.net/manual/en/class.error.php) internal PHP errors. By enforcing `\Throwable` as reason to 372 | reject a promise, any language error or user land exception can be used to reject a promise. 373 | 374 | The resulting `$promise` implements the [`PromiseInterface`](#promiseinterface) 375 | and can be consumed like any other promise: 376 | 377 | ```php 378 | $promise = React\Promise\reject(new RuntimeException('Request failed')); 379 | 380 | $promise->then(function (int $result): void { 381 | var_dump($result); 382 | }, function (\Throwable $e): void { 383 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 384 | }); 385 | ``` 386 | 387 | Note that rejected promises should always be handled similar to how any 388 | exceptions should always be caught in a `try` + `catch` block. If you remove the 389 | last reference to a rejected promise that has not been handled, it will 390 | report an unhandled promise rejection: 391 | 392 | ```php 393 | function incorrect(): int 394 | { 395 | $promise = React\Promise\reject(new RuntimeException('Request failed')); 396 | 397 | // Commented out: No rejection handler registered here. 398 | // $promise->then(null, function (\Throwable $e): void { /* ignore */ }); 399 | 400 | // Returning from a function will remove all local variable references, hence why 401 | // this will report an unhandled promise rejection here. 402 | return 42; 403 | } 404 | 405 | // Calling this function will log an error message plus its stack trace: 406 | // Unhandled promise rejection with RuntimeException: Request failed in example.php:10 407 | incorrect(); 408 | ``` 409 | 410 | A rejected promise will be considered "handled" if you catch the rejection 411 | reason with either the [`then()` method](#promiseinterfacethen), the 412 | [`catch()` method](#promiseinterfacecatch), or the 413 | [`finally()` method](#promiseinterfacefinally). Note that each of these methods 414 | return a new promise that may again be rejected if you re-throw an exception. 415 | 416 | A rejected promise will also be considered "handled" if you abort the operation 417 | with the [`cancel()` method](#promiseinterfacecancel) (which in turn would 418 | usually reject the promise if it is still pending). 419 | 420 | See also the [`set_rejection_handler()` function](#set_rejection_handler). 421 | 422 | #### all() 423 | 424 | ```php 425 | $promise = React\Promise\all(iterable $promisesOrValues); 426 | ``` 427 | 428 | Returns a promise that will resolve only once all the items in 429 | `$promisesOrValues` have resolved. The resolution value of the returned promise 430 | will be an array containing the resolution values of each of the items in 431 | `$promisesOrValues`. 432 | 433 | #### race() 434 | 435 | ```php 436 | $promise = React\Promise\race(iterable $promisesOrValues); 437 | ``` 438 | 439 | Initiates a competitive race that allows one winner. Returns a promise which is 440 | resolved in the same way the first settled promise resolves. 441 | 442 | The returned promise will become **infinitely pending** if `$promisesOrValues` 443 | contains 0 items. 444 | 445 | #### any() 446 | 447 | ```php 448 | $promise = React\Promise\any(iterable $promisesOrValues); 449 | ``` 450 | 451 | Returns a promise that will resolve when any one of the items in 452 | `$promisesOrValues` resolves. The resolution value of the returned promise 453 | will be the resolution value of the triggering item. 454 | 455 | The returned promise will only reject if *all* items in `$promisesOrValues` are 456 | rejected. The rejection value will be a `React\Promise\Exception\CompositeException` 457 | which holds all rejection reasons. The rejection reasons can be obtained with 458 | `CompositeException::getThrowables()`. 459 | 460 | The returned promise will also reject with a `React\Promise\Exception\LengthException` 461 | if `$promisesOrValues` contains 0 items. 462 | 463 | #### set_rejection_handler() 464 | 465 | ```php 466 | React\Promise\set_rejection_handler(?callable $callback): ?callable; 467 | ``` 468 | 469 | Sets the global rejection handler for unhandled promise rejections. 470 | 471 | Note that rejected promises should always be handled similar to how any 472 | exceptions should always be caught in a `try` + `catch` block. If you remove 473 | the last reference to a rejected promise that has not been handled, it will 474 | report an unhandled promise rejection. See also the [`reject()` function](#reject) 475 | for more details. 476 | 477 | The `?callable $callback` argument MUST be a valid callback function that 478 | accepts a single `Throwable` argument or a `null` value to restore the 479 | default promise rejection handler. The return value of the callback function 480 | will be ignored and has no effect, so you SHOULD return a `void` value. The 481 | callback function MUST NOT throw or the program will be terminated with a 482 | fatal error. 483 | 484 | The function returns the previous rejection handler or `null` if using the 485 | default promise rejection handler. 486 | 487 | The default promise rejection handler will log an error message plus its stack 488 | trace: 489 | 490 | ```php 491 | // Unhandled promise rejection with RuntimeException: Unhandled in example.php:2 492 | React\Promise\reject(new RuntimeException('Unhandled')); 493 | ``` 494 | 495 | The promise rejection handler may be used to use customize the log message or 496 | write to custom log targets. As a rule of thumb, this function should only be 497 | used as a last resort and promise rejections are best handled with either the 498 | [`then()` method](#promiseinterfacethen), the 499 | [`catch()` method](#promiseinterfacecatch), or the 500 | [`finally()` method](#promiseinterfacefinally). 501 | See also the [`reject()` function](#reject) for more details. 502 | 503 | ## Examples 504 | 505 | ### How to use Deferred 506 | 507 | ```php 508 | function getAwesomeResultPromise() 509 | { 510 | $deferred = new React\Promise\Deferred(); 511 | 512 | // Execute a Node.js-style function using the callback pattern 513 | computeAwesomeResultAsynchronously(function (\Throwable $error, $result) use ($deferred) { 514 | if ($error) { 515 | $deferred->reject($error); 516 | } else { 517 | $deferred->resolve($result); 518 | } 519 | }); 520 | 521 | // Return the promise 522 | return $deferred->promise(); 523 | } 524 | 525 | getAwesomeResultPromise() 526 | ->then( 527 | function ($value) { 528 | // Deferred resolved, do something with $value 529 | }, 530 | function (\Throwable $reason) { 531 | // Deferred rejected, do something with $reason 532 | } 533 | ); 534 | ``` 535 | 536 | ### How promise forwarding works 537 | 538 | A few simple examples to show how the mechanics of Promises/A forwarding works. 539 | These examples are contrived, of course, and in real usage, promise chains will 540 | typically be spread across several function calls, or even several levels of 541 | your application architecture. 542 | 543 | #### Resolution forwarding 544 | 545 | Resolved promises forward resolution values to the next promise. 546 | The first promise, `$deferred->promise()`, will resolve with the value passed 547 | to `$deferred->resolve()` below. 548 | 549 | Each call to `then()` returns a new promise that will resolve with the return 550 | value of the previous handler. This creates a promise "pipeline". 551 | 552 | ```php 553 | $deferred = new React\Promise\Deferred(); 554 | 555 | $deferred->promise() 556 | ->then(function ($x) { 557 | // $x will be the value passed to $deferred->resolve() below 558 | // and returns a *new promise* for $x + 1 559 | return $x + 1; 560 | }) 561 | ->then(function ($x) { 562 | // $x === 2 563 | // This handler receives the return value of the 564 | // previous handler. 565 | return $x + 1; 566 | }) 567 | ->then(function ($x) { 568 | // $x === 3 569 | // This handler receives the return value of the 570 | // previous handler. 571 | return $x + 1; 572 | }) 573 | ->then(function ($x) { 574 | // $x === 4 575 | // This handler receives the return value of the 576 | // previous handler. 577 | echo 'Resolve ' . $x; 578 | }); 579 | 580 | $deferred->resolve(1); // Prints "Resolve 4" 581 | ``` 582 | 583 | #### Rejection forwarding 584 | 585 | Rejected promises behave similarly, and also work similarly to try/catch: 586 | When you catch an exception, you must rethrow for it to propagate. 587 | 588 | Similarly, when you handle a rejected promise, to propagate the rejection, 589 | "rethrow" it by either returning a rejected promise, or actually throwing 590 | (since promise translates thrown exceptions into rejections) 591 | 592 | ```php 593 | $deferred = new React\Promise\Deferred(); 594 | 595 | $deferred->promise() 596 | ->then(function ($x) { 597 | throw new \Exception($x + 1); 598 | }) 599 | ->catch(function (\Exception $x) { 600 | // Propagate the rejection 601 | throw $x; 602 | }) 603 | ->catch(function (\Exception $x) { 604 | // Can also propagate by returning another rejection 605 | return React\Promise\reject( 606 | new \Exception($x->getMessage() + 1) 607 | ); 608 | }) 609 | ->catch(function ($x) { 610 | echo 'Reject ' . $x->getMessage(); // 3 611 | }); 612 | 613 | $deferred->resolve(1); // Prints "Reject 3" 614 | ``` 615 | 616 | #### Mixed resolution and rejection forwarding 617 | 618 | Just like try/catch, you can choose to propagate or not. Mixing resolutions and 619 | rejections will still forward handler results in a predictable way. 620 | 621 | ```php 622 | $deferred = new React\Promise\Deferred(); 623 | 624 | $deferred->promise() 625 | ->then(function ($x) { 626 | return $x + 1; 627 | }) 628 | ->then(function ($x) { 629 | throw new \Exception($x + 1); 630 | }) 631 | ->catch(function (\Exception $x) { 632 | // Handle the rejection, and don't propagate. 633 | // This is like catch without a rethrow 634 | return $x->getMessage() + 1; 635 | }) 636 | ->then(function ($x) { 637 | echo 'Mixed ' . $x; // 4 638 | }); 639 | 640 | $deferred->resolve(1); // Prints "Mixed 4" 641 | ``` 642 | --------------------------------------------------------------------------------