├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── AggregateException.php ├── CancellationException.php ├── Coroutine.php ├── Create.php ├── Each.php ├── EachPromise.php ├── FulfilledPromise.php ├── Is.php ├── Promise.php ├── PromiseInterface.php ├── PromisorInterface.php ├── RejectedPromise.php ├── RejectionException.php ├── TaskQueue.php ├── TaskQueueInterface.php └── Utils.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | 4 | ## 2.2.0 - 2025-03-27 5 | 6 | ### Fixed 7 | 8 | - Revert "Allow an empty EachPromise to be resolved by running the queue" 9 | 10 | 11 | ## 2.1.0 - 2025-03-27 12 | 13 | ### Added 14 | 15 | - Allow an empty EachPromise to be resolved by running the queue 16 | 17 | 18 | ## 2.0.4 - 2024-10-17 19 | 20 | ### Fixed 21 | 22 | - Once settled, don't allow further rejection of additional promises 23 | 24 | 25 | ## 2.0.3 - 2024-07-18 26 | 27 | ### Changed 28 | 29 | - PHP 8.4 support 30 | 31 | 32 | ## 2.0.2 - 2023-12-03 33 | 34 | ### Changed 35 | 36 | - Replaced `call_user_func*` with native calls 37 | 38 | 39 | ## 2.0.1 - 2023-08-03 40 | 41 | ### Changed 42 | 43 | - PHP 8.3 support 44 | 45 | 46 | ## 2.0.0 - 2023-05-21 47 | 48 | ### Added 49 | 50 | - Added PHP 7 type hints 51 | 52 | ### Changed 53 | 54 | - All previously non-final non-exception classes have been marked as soft-final 55 | 56 | ### Removed 57 | 58 | - Dropped PHP < 7.2 support 59 | - All functions in the `GuzzleHttp\Promise` namespace 60 | 61 | 62 | ## 1.5.3 - 2023-05-21 63 | 64 | ### Changed 65 | 66 | - Removed remaining usage of deprecated functions 67 | 68 | 69 | ## 1.5.2 - 2022-08-07 70 | 71 | ### Changed 72 | 73 | - Officially support PHP 8.2 74 | 75 | 76 | ## 1.5.1 - 2021-10-22 77 | 78 | ### Fixed 79 | 80 | - Revert "Call handler when waiting on fulfilled/rejected Promise" 81 | - Fix pool memory leak when empty array of promises provided 82 | 83 | 84 | ## 1.5.0 - 2021-10-07 85 | 86 | ### Changed 87 | 88 | - Call handler when waiting on fulfilled/rejected Promise 89 | - Officially support PHP 8.1 90 | 91 | ### Fixed 92 | 93 | - Fix manually settle promises generated with `Utils::task` 94 | 95 | 96 | ## 1.4.1 - 2021-02-18 97 | 98 | ### Fixed 99 | 100 | - Fixed `each_limit` skipping promises and failing 101 | 102 | 103 | ## 1.4.0 - 2020-09-30 104 | 105 | ### Added 106 | 107 | - Support for PHP 8 108 | - Optional `$recursive` flag to `all` 109 | - Replaced functions by static methods 110 | 111 | ### Fixed 112 | 113 | - Fix empty `each` processing 114 | - Fix promise handling for Iterators of non-unique keys 115 | - Fixed `method_exists` crashes on PHP 8 116 | - Memory leak on exceptions 117 | 118 | 119 | ## 1.3.1 - 2016-12-20 120 | 121 | ### Fixed 122 | 123 | - `wait()` foreign promise compatibility 124 | 125 | 126 | ## 1.3.0 - 2016-11-18 127 | 128 | ### Added 129 | 130 | - Adds support for custom task queues. 131 | 132 | ### Fixed 133 | 134 | - Fixed coroutine promise memory leak. 135 | 136 | 137 | ## 1.2.0 - 2016-05-18 138 | 139 | ### Changed 140 | 141 | - Update to now catch `\Throwable` on PHP 7+ 142 | 143 | 144 | ## 1.1.0 - 2016-03-07 145 | 146 | ### Changed 147 | 148 | - Update EachPromise to prevent recurring on a iterator when advancing, as this 149 | could trigger fatal generator errors. 150 | - Update Promise to allow recursive waiting without unwrapping exceptions. 151 | 152 | 153 | ## 1.0.3 - 2015-10-15 154 | 155 | ### Changed 156 | 157 | - Update EachPromise to immediately resolve when the underlying promise iterator 158 | is empty. Previously, such a promise would throw an exception when its `wait` 159 | function was called. 160 | 161 | 162 | ## 1.0.2 - 2015-05-15 163 | 164 | ### Changed 165 | 166 | - Conditionally require functions.php. 167 | 168 | 169 | ## 1.0.1 - 2015-06-24 170 | 171 | ### Changed 172 | 173 | - Updating EachPromise to call next on the underlying promise iterator as late 174 | as possible to ensure that generators that generate new requests based on 175 | callbacks are not iterated until after callbacks are invoked. 176 | 177 | 178 | ## 1.0.0 - 2015-05-12 179 | 180 | - Initial release 181 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michael Dowling 4 | Copyright (c) 2015 Graham Campbell 5 | Copyright (c) 2017 Tobias Schultze 6 | Copyright (c) 2020 Tobias Nyholm 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guzzle Promises 2 | 3 | [Promises/A+](https://promisesaplus.com/) implementation that handles promise 4 | chaining and resolution iteratively, allowing for "infinite" promise chaining 5 | while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/) 6 | for a general introduction to promises. 7 | 8 | - [Features](#features) 9 | - [Quick start](#quick-start) 10 | - [Synchronous wait](#synchronous-wait) 11 | - [Cancellation](#cancellation) 12 | - [API](#api) 13 | - [Promise](#promise) 14 | - [FulfilledPromise](#fulfilledpromise) 15 | - [RejectedPromise](#rejectedpromise) 16 | - [Promise interop](#promise-interop) 17 | - [Implementation notes](#implementation-notes) 18 | 19 | 20 | ## Features 21 | 22 | - [Promises/A+](https://promisesaplus.com/) implementation. 23 | - Promise resolution and chaining is handled iteratively, allowing for 24 | "infinite" promise chaining. 25 | - Promises have a synchronous `wait` method. 26 | - Promises can be cancelled. 27 | - Works with any object that has a `then` function. 28 | - C# style async/await coroutine promises using 29 | `GuzzleHttp\Promise\Coroutine::of()`. 30 | 31 | 32 | ## Installation 33 | 34 | ```shell 35 | composer require guzzlehttp/promises 36 | ``` 37 | 38 | 39 | ## Version Guidance 40 | 41 | | Version | Status | PHP Version | 42 | |---------|---------------------|--------------| 43 | | 1.x | Security fixes only | >=5.5,<8.3 | 44 | | 2.x | Latest | >=7.2.5,<8.5 | 45 | 46 | 47 | ## Quick Start 48 | 49 | A *promise* represents the eventual result of an asynchronous operation. The 50 | primary way of interacting with a promise is through its `then` method, which 51 | registers callbacks to receive either a promise's eventual value or the reason 52 | why the promise cannot be fulfilled. 53 | 54 | ### Callbacks 55 | 56 | Callbacks are registered with the `then` method by providing an optional 57 | `$onFulfilled` followed by an optional `$onRejected` function. 58 | 59 | 60 | ```php 61 | use GuzzleHttp\Promise\Promise; 62 | 63 | $promise = new Promise(); 64 | $promise->then( 65 | // $onFulfilled 66 | function ($value) { 67 | echo 'The promise was fulfilled.'; 68 | }, 69 | // $onRejected 70 | function ($reason) { 71 | echo 'The promise was rejected.'; 72 | } 73 | ); 74 | ``` 75 | 76 | *Resolving* a promise means that you either fulfill a promise with a *value* or 77 | reject a promise with a *reason*. Resolving a promise triggers callbacks 78 | registered with the promise's `then` method. These callbacks are triggered 79 | only once and in the order in which they were added. 80 | 81 | ### Resolving a Promise 82 | 83 | Promises are fulfilled using the `resolve($value)` method. Resolving a promise 84 | with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger 85 | all of the onFulfilled callbacks (resolving a promise with a rejected promise 86 | will reject the promise and trigger the `$onRejected` callbacks). 87 | 88 | ```php 89 | use GuzzleHttp\Promise\Promise; 90 | 91 | $promise = new Promise(); 92 | $promise 93 | ->then(function ($value) { 94 | // Return a value and don't break the chain 95 | return "Hello, " . $value; 96 | }) 97 | // This then is executed after the first then and receives the value 98 | // returned from the first then. 99 | ->then(function ($value) { 100 | echo $value; 101 | }); 102 | 103 | // Resolving the promise triggers the $onFulfilled callbacks and outputs 104 | // "Hello, reader." 105 | $promise->resolve('reader.'); 106 | ``` 107 | 108 | ### Promise Forwarding 109 | 110 | Promises can be chained one after the other. Each then in the chain is a new 111 | promise. The return value of a promise is what's forwarded to the next 112 | promise in the chain. Returning a promise in a `then` callback will cause the 113 | subsequent promises in the chain to only be fulfilled when the returned promise 114 | has been fulfilled. The next promise in the chain will be invoked with the 115 | resolved value of the promise. 116 | 117 | ```php 118 | use GuzzleHttp\Promise\Promise; 119 | 120 | $promise = new Promise(); 121 | $nextPromise = new Promise(); 122 | 123 | $promise 124 | ->then(function ($value) use ($nextPromise) { 125 | echo $value; 126 | return $nextPromise; 127 | }) 128 | ->then(function ($value) { 129 | echo $value; 130 | }); 131 | 132 | // Triggers the first callback and outputs "A" 133 | $promise->resolve('A'); 134 | // Triggers the second callback and outputs "B" 135 | $nextPromise->resolve('B'); 136 | ``` 137 | 138 | ### Promise Rejection 139 | 140 | When a promise is rejected, the `$onRejected` callbacks are invoked with the 141 | rejection reason. 142 | 143 | ```php 144 | use GuzzleHttp\Promise\Promise; 145 | 146 | $promise = new Promise(); 147 | $promise->then(null, function ($reason) { 148 | echo $reason; 149 | }); 150 | 151 | $promise->reject('Error!'); 152 | // Outputs "Error!" 153 | ``` 154 | 155 | ### Rejection Forwarding 156 | 157 | If an exception is thrown in an `$onRejected` callback, subsequent 158 | `$onRejected` callbacks are invoked with the thrown exception as the reason. 159 | 160 | ```php 161 | use GuzzleHttp\Promise\Promise; 162 | 163 | $promise = new Promise(); 164 | $promise->then(null, function ($reason) { 165 | throw new Exception($reason); 166 | })->then(null, function ($reason) { 167 | assert($reason->getMessage() === 'Error!'); 168 | }); 169 | 170 | $promise->reject('Error!'); 171 | ``` 172 | 173 | You can also forward a rejection down the promise chain by returning a 174 | `GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or 175 | `$onRejected` callback. 176 | 177 | ```php 178 | use GuzzleHttp\Promise\Promise; 179 | use GuzzleHttp\Promise\RejectedPromise; 180 | 181 | $promise = new Promise(); 182 | $promise->then(null, function ($reason) { 183 | return new RejectedPromise($reason); 184 | })->then(null, function ($reason) { 185 | assert($reason === 'Error!'); 186 | }); 187 | 188 | $promise->reject('Error!'); 189 | ``` 190 | 191 | If an exception is not thrown in a `$onRejected` callback and the callback 192 | does not return a rejected promise, downstream `$onFulfilled` callbacks are 193 | invoked using the value returned from the `$onRejected` callback. 194 | 195 | ```php 196 | use GuzzleHttp\Promise\Promise; 197 | 198 | $promise = new Promise(); 199 | $promise 200 | ->then(null, function ($reason) { 201 | return "It's ok"; 202 | }) 203 | ->then(function ($value) { 204 | assert($value === "It's ok"); 205 | }); 206 | 207 | $promise->reject('Error!'); 208 | ``` 209 | 210 | 211 | ## Synchronous Wait 212 | 213 | You can synchronously force promises to complete using a promise's `wait` 214 | method. When creating a promise, you can provide a wait function that is used 215 | to synchronously force a promise to complete. When a wait function is invoked 216 | it is expected to deliver a value to the promise or reject the promise. If the 217 | wait function does not deliver a value, then an exception is thrown. The wait 218 | function provided to a promise constructor is invoked when the `wait` function 219 | of the promise is called. 220 | 221 | ```php 222 | $promise = new Promise(function () use (&$promise) { 223 | $promise->resolve('foo'); 224 | }); 225 | 226 | // Calling wait will return the value of the promise. 227 | echo $promise->wait(); // outputs "foo" 228 | ``` 229 | 230 | If an exception is encountered while invoking the wait function of a promise, 231 | the promise is rejected with the exception and the exception is thrown. 232 | 233 | ```php 234 | $promise = new Promise(function () use (&$promise) { 235 | throw new Exception('foo'); 236 | }); 237 | 238 | $promise->wait(); // throws the exception. 239 | ``` 240 | 241 | Calling `wait` on a promise that has been fulfilled will not trigger the wait 242 | function. It will simply return the previously resolved value. 243 | 244 | ```php 245 | $promise = new Promise(function () { die('this is not called!'); }); 246 | $promise->resolve('foo'); 247 | echo $promise->wait(); // outputs "foo" 248 | ``` 249 | 250 | Calling `wait` on a promise that has been rejected will throw an exception. If 251 | the rejection reason is an instance of `\Exception` the reason is thrown. 252 | Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason 253 | can be obtained by calling the `getReason` method of the exception. 254 | 255 | ```php 256 | $promise = new Promise(); 257 | $promise->reject('foo'); 258 | $promise->wait(); 259 | ``` 260 | 261 | > PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo' 262 | 263 | ### Unwrapping a Promise 264 | 265 | When synchronously waiting on a promise, you are joining the state of the 266 | promise into the current state of execution (i.e., return the value of the 267 | promise if it was fulfilled or throw an exception if it was rejected). This is 268 | called "unwrapping" the promise. Waiting on a promise will by default unwrap 269 | the promise state. 270 | 271 | You can force a promise to resolve and *not* unwrap the state of the promise 272 | by passing `false` to the first argument of the `wait` function: 273 | 274 | ```php 275 | $promise = new Promise(); 276 | $promise->reject('foo'); 277 | // This will not throw an exception. It simply ensures the promise has 278 | // been resolved. 279 | $promise->wait(false); 280 | ``` 281 | 282 | When unwrapping a promise, the resolved value of the promise will be waited 283 | upon until the unwrapped value is not a promise. This means that if you resolve 284 | promise A with a promise B and unwrap promise A, the value returned by the 285 | wait function will be the value delivered to promise B. 286 | 287 | **Note**: when you do not unwrap the promise, no value is returned. 288 | 289 | 290 | ## Cancellation 291 | 292 | You can cancel a promise that has not yet been fulfilled using the `cancel()` 293 | method of a promise. When creating a promise you can provide an optional 294 | cancel function that when invoked cancels the action of computing a resolution 295 | of the promise. 296 | 297 | 298 | ## API 299 | 300 | ### Promise 301 | 302 | When creating a promise object, you can provide an optional `$waitFn` and 303 | `$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is 304 | expected to resolve the promise. `$cancelFn` is a function with no arguments 305 | that is expected to cancel the computation of a promise. It is invoked when the 306 | `cancel()` method of a promise is called. 307 | 308 | ```php 309 | use GuzzleHttp\Promise\Promise; 310 | 311 | $promise = new Promise( 312 | function () use (&$promise) { 313 | $promise->resolve('waited'); 314 | }, 315 | function () { 316 | // do something that will cancel the promise computation (e.g., close 317 | // a socket, cancel a database query, etc...) 318 | } 319 | ); 320 | 321 | assert('waited' === $promise->wait()); 322 | ``` 323 | 324 | A promise has the following methods: 325 | 326 | - `then(callable $onFulfilled, callable $onRejected) : PromiseInterface` 327 | 328 | Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler. 329 | 330 | - `otherwise(callable $onRejected) : PromiseInterface` 331 | 332 | Appends a rejection handler callback to the promise, and returns a new promise resolving to the return value of the callback if it is called, or to its original fulfillment value if the promise is instead fulfilled. 333 | 334 | - `wait($unwrap = true) : mixed` 335 | 336 | Synchronously waits on the promise to complete. 337 | 338 | `$unwrap` controls whether or not the value of the promise is returned for a 339 | fulfilled promise or if an exception is thrown if the promise is rejected. 340 | This is set to `true` by default. 341 | 342 | - `cancel()` 343 | 344 | Attempts to cancel the promise if possible. The promise being cancelled and 345 | the parent most ancestor that has not yet been resolved will also be 346 | cancelled. Any promises waiting on the cancelled promise to resolve will also 347 | be cancelled. 348 | 349 | - `getState() : string` 350 | 351 | Returns the state of the promise. One of `pending`, `fulfilled`, or 352 | `rejected`. 353 | 354 | - `resolve($value)` 355 | 356 | Fulfills the promise with the given `$value`. 357 | 358 | - `reject($reason)` 359 | 360 | Rejects the promise with the given `$reason`. 361 | 362 | 363 | ### FulfilledPromise 364 | 365 | A fulfilled promise can be created to represent a promise that has been 366 | fulfilled. 367 | 368 | ```php 369 | use GuzzleHttp\Promise\FulfilledPromise; 370 | 371 | $promise = new FulfilledPromise('value'); 372 | 373 | // Fulfilled callbacks are immediately invoked. 374 | $promise->then(function ($value) { 375 | echo $value; 376 | }); 377 | ``` 378 | 379 | 380 | ### RejectedPromise 381 | 382 | A rejected promise can be created to represent a promise that has been 383 | rejected. 384 | 385 | ```php 386 | use GuzzleHttp\Promise\RejectedPromise; 387 | 388 | $promise = new RejectedPromise('Error'); 389 | 390 | // Rejected callbacks are immediately invoked. 391 | $promise->then(null, function ($reason) { 392 | echo $reason; 393 | }); 394 | ``` 395 | 396 | 397 | ## Promise Interoperability 398 | 399 | This library works with foreign promises that have a `then` method. This means 400 | you can use Guzzle promises with [React promises](https://github.com/reactphp/promise) 401 | for example. When a foreign promise is returned inside of a then method 402 | callback, promise resolution will occur recursively. 403 | 404 | ```php 405 | // Create a React promise 406 | $deferred = new React\Promise\Deferred(); 407 | $reactPromise = $deferred->promise(); 408 | 409 | // Create a Guzzle promise that is fulfilled with a React promise. 410 | $guzzlePromise = new GuzzleHttp\Promise\Promise(); 411 | $guzzlePromise->then(function ($value) use ($reactPromise) { 412 | // Do something something with the value... 413 | // Return the React promise 414 | return $reactPromise; 415 | }); 416 | ``` 417 | 418 | Please note that wait and cancel chaining is no longer possible when forwarding 419 | a foreign promise. You will need to wrap a third-party promise with a Guzzle 420 | promise in order to utilize wait and cancel functions with foreign promises. 421 | 422 | 423 | ### Event Loop Integration 424 | 425 | In order to keep the stack size constant, Guzzle promises are resolved 426 | asynchronously using a task queue. When waiting on promises synchronously, the 427 | task queue will be automatically run to ensure that the blocking promise and 428 | any forwarded promises are resolved. When using promises asynchronously in an 429 | event loop, you will need to run the task queue on each tick of the loop. If 430 | you do not run the task queue, then promises will not be resolved. 431 | 432 | You can run the task queue using the `run()` method of the global task queue 433 | instance. 434 | 435 | ```php 436 | // Get the global task queue 437 | $queue = GuzzleHttp\Promise\Utils::queue(); 438 | $queue->run(); 439 | ``` 440 | 441 | For example, you could use Guzzle promises with React using a periodic timer: 442 | 443 | ```php 444 | $loop = React\EventLoop\Factory::create(); 445 | $loop->addPeriodicTimer(0, [$queue, 'run']); 446 | ``` 447 | 448 | 449 | ## Implementation Notes 450 | 451 | ### Promise Resolution and Chaining is Handled Iteratively 452 | 453 | By shuffling pending handlers from one owner to another, promises are 454 | resolved iteratively, allowing for "infinite" then chaining. 455 | 456 | ```php 457 | then(function ($v) { 467 | // The stack size remains constant (a good thing) 468 | echo xdebug_get_stack_depth() . ', '; 469 | return $v + 1; 470 | }); 471 | } 472 | 473 | $parent->resolve(0); 474 | var_dump($p->wait()); // int(1000) 475 | 476 | ``` 477 | 478 | When a promise is fulfilled or rejected with a non-promise value, the promise 479 | then takes ownership of the handlers of each child promise and delivers values 480 | down the chain without using recursion. 481 | 482 | When a promise is resolved with another promise, the original promise transfers 483 | all of its pending handlers to the new promise. When the new promise is 484 | eventually resolved, all of the pending handlers are delivered the forwarded 485 | value. 486 | 487 | ### A Promise is the Deferred 488 | 489 | Some promise libraries implement promises using a deferred object to represent 490 | a computation and a promise object to represent the delivery of the result of 491 | the computation. This is a nice separation of computation and delivery because 492 | consumers of the promise cannot modify the value that will be eventually 493 | delivered. 494 | 495 | One side effect of being able to implement promise resolution and chaining 496 | iteratively is that you need to be able for one promise to reach into the state 497 | of another promise to shuffle around ownership of handlers. In order to achieve 498 | this without making the handlers of a promise publicly mutable, a promise is 499 | also the deferred value, allowing promises of the same parent class to reach 500 | into and modify the private properties of promises of the same type. While this 501 | does allow consumers of the value to modify the resolution or rejection of the 502 | deferred, it is a small price to pay for keeping the stack size constant. 503 | 504 | ```php 505 | $promise = new Promise(); 506 | $promise->then(function ($value) { echo $value; }); 507 | // The promise is the deferred value, so you can deliver a value to it. 508 | $promise->resolve('foo'); 509 | // prints "foo" 510 | ``` 511 | 512 | 513 | ## Upgrading from Function API 514 | 515 | A static API was first introduced in 1.4.0, in order to mitigate problems with 516 | functions conflicting between global and local copies of the package. The 517 | function API was removed in 2.0.0. A migration table has been provided here for 518 | your convenience: 519 | 520 | | Original Function | Replacement Method | 521 | |----------------|----------------| 522 | | `queue` | `Utils::queue` | 523 | | `task` | `Utils::task` | 524 | | `promise_for` | `Create::promiseFor` | 525 | | `rejection_for` | `Create::rejectionFor` | 526 | | `exception_for` | `Create::exceptionFor` | 527 | | `iter_for` | `Create::iterFor` | 528 | | `inspect` | `Utils::inspect` | 529 | | `inspect_all` | `Utils::inspectAll` | 530 | | `unwrap` | `Utils::unwrap` | 531 | | `all` | `Utils::all` | 532 | | `some` | `Utils::some` | 533 | | `any` | `Utils::any` | 534 | | `settle` | `Utils::settle` | 535 | | `each` | `Each::of` | 536 | | `each_limit` | `Each::ofLimit` | 537 | | `each_limit_all` | `Each::ofLimitAll` | 538 | | `!is_fulfilled` | `Is::pending` | 539 | | `is_fulfilled` | `Is::fulfilled` | 540 | | `is_rejected` | `Is::rejected` | 541 | | `is_settled` | `Is::settled` | 542 | | `coroutine` | `Coroutine::of` | 543 | 544 | 545 | ## Security 546 | 547 | If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/promises/security/policy) for more information. 548 | 549 | 550 | ## License 551 | 552 | Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information. 553 | 554 | 555 | ## For Enterprise 556 | 557 | Available as part of the Tidelift Subscription 558 | 559 | The maintainers of Guzzle and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-promises?utm_source=packagist-guzzlehttp-promises&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) 560 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guzzlehttp/promises", 3 | "description": "Guzzle promises library", 4 | "keywords": ["promise"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Graham Campbell", 9 | "email": "hello@gjcampbell.co.uk", 10 | "homepage": "https://github.com/GrahamCampbell" 11 | }, 12 | { 13 | "name": "Michael Dowling", 14 | "email": "mtdowling@gmail.com", 15 | "homepage": "https://github.com/mtdowling" 16 | }, 17 | { 18 | "name": "Tobias Nyholm", 19 | "email": "tobias.nyholm@gmail.com", 20 | "homepage": "https://github.com/Nyholm" 21 | }, 22 | { 23 | "name": "Tobias Schultze", 24 | "email": "webmaster@tubo-world.de", 25 | "homepage": "https://github.com/Tobion" 26 | } 27 | ], 28 | "require": { 29 | "php": "^7.2.5 || ^8.0" 30 | }, 31 | "require-dev": { 32 | "bamarni/composer-bin-plugin": "^1.8.2", 33 | "phpunit/phpunit": "^8.5.39 || ^9.6.20" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "GuzzleHttp\\Promise\\": "src/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "GuzzleHttp\\Promise\\Tests\\": "tests/" 43 | } 44 | }, 45 | "extra": { 46 | "bamarni-bin": { 47 | "bin-links": true, 48 | "forward-command": false 49 | } 50 | }, 51 | "config": { 52 | "allow-plugins": { 53 | "bamarni/composer-bin-plugin": true 54 | }, 55 | "preferred-install": "dist", 56 | "sort-packages": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/AggregateException.php: -------------------------------------------------------------------------------- 1 | then(function ($v) { echo $v; }); 39 | * 40 | * @param callable $generatorFn Generator function to wrap into a promise. 41 | * 42 | * @return Promise 43 | * 44 | * @see https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration 45 | */ 46 | final class Coroutine implements PromiseInterface 47 | { 48 | /** 49 | * @var PromiseInterface|null 50 | */ 51 | private $currentPromise; 52 | 53 | /** 54 | * @var Generator 55 | */ 56 | private $generator; 57 | 58 | /** 59 | * @var Promise 60 | */ 61 | private $result; 62 | 63 | public function __construct(callable $generatorFn) 64 | { 65 | $this->generator = $generatorFn(); 66 | $this->result = new Promise(function (): void { 67 | while (isset($this->currentPromise)) { 68 | $this->currentPromise->wait(); 69 | } 70 | }); 71 | try { 72 | $this->nextCoroutine($this->generator->current()); 73 | } catch (Throwable $throwable) { 74 | $this->result->reject($throwable); 75 | } 76 | } 77 | 78 | /** 79 | * Create a new coroutine. 80 | */ 81 | public static function of(callable $generatorFn): self 82 | { 83 | return new self($generatorFn); 84 | } 85 | 86 | public function then( 87 | ?callable $onFulfilled = null, 88 | ?callable $onRejected = null 89 | ): PromiseInterface { 90 | return $this->result->then($onFulfilled, $onRejected); 91 | } 92 | 93 | public function otherwise(callable $onRejected): PromiseInterface 94 | { 95 | return $this->result->otherwise($onRejected); 96 | } 97 | 98 | public function wait(bool $unwrap = true) 99 | { 100 | return $this->result->wait($unwrap); 101 | } 102 | 103 | public function getState(): string 104 | { 105 | return $this->result->getState(); 106 | } 107 | 108 | public function resolve($value): void 109 | { 110 | $this->result->resolve($value); 111 | } 112 | 113 | public function reject($reason): void 114 | { 115 | $this->result->reject($reason); 116 | } 117 | 118 | public function cancel(): void 119 | { 120 | $this->currentPromise->cancel(); 121 | $this->result->cancel(); 122 | } 123 | 124 | private function nextCoroutine($yielded): void 125 | { 126 | $this->currentPromise = Create::promiseFor($yielded) 127 | ->then([$this, '_handleSuccess'], [$this, '_handleFailure']); 128 | } 129 | 130 | /** 131 | * @internal 132 | */ 133 | public function _handleSuccess($value): void 134 | { 135 | unset($this->currentPromise); 136 | try { 137 | $next = $this->generator->send($value); 138 | if ($this->generator->valid()) { 139 | $this->nextCoroutine($next); 140 | } else { 141 | $this->result->resolve($value); 142 | } 143 | } catch (Throwable $throwable) { 144 | $this->result->reject($throwable); 145 | } 146 | } 147 | 148 | /** 149 | * @internal 150 | */ 151 | public function _handleFailure($reason): void 152 | { 153 | unset($this->currentPromise); 154 | try { 155 | $nextYield = $this->generator->throw(Create::exceptionFor($reason)); 156 | // The throw was caught, so keep iterating on the coroutine 157 | $this->nextCoroutine($nextYield); 158 | } catch (Throwable $throwable) { 159 | $this->result->reject($throwable); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Create.php: -------------------------------------------------------------------------------- 1 | then([$promise, 'resolve'], [$promise, 'reject']); 26 | 27 | return $promise; 28 | } 29 | 30 | return new FulfilledPromise($value); 31 | } 32 | 33 | /** 34 | * Creates a rejected promise for a reason if the reason is not a promise. 35 | * If the provided reason is a promise, then it is returned as-is. 36 | * 37 | * @param mixed $reason Promise or reason. 38 | */ 39 | public static function rejectionFor($reason): PromiseInterface 40 | { 41 | if ($reason instanceof PromiseInterface) { 42 | return $reason; 43 | } 44 | 45 | return new RejectedPromise($reason); 46 | } 47 | 48 | /** 49 | * Create an exception for a rejected promise value. 50 | * 51 | * @param mixed $reason 52 | */ 53 | public static function exceptionFor($reason): \Throwable 54 | { 55 | if ($reason instanceof \Throwable) { 56 | return $reason; 57 | } 58 | 59 | return new RejectionException($reason); 60 | } 61 | 62 | /** 63 | * Returns an iterator for the given value. 64 | * 65 | * @param mixed $value 66 | */ 67 | public static function iterFor($value): \Iterator 68 | { 69 | if ($value instanceof \Iterator) { 70 | return $value; 71 | } 72 | 73 | if (is_array($value)) { 74 | return new \ArrayIterator($value); 75 | } 76 | 77 | return new \ArrayIterator([$value]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Each.php: -------------------------------------------------------------------------------- 1 | $onFulfilled, 31 | 'rejected' => $onRejected, 32 | ]))->promise(); 33 | } 34 | 35 | /** 36 | * Like of, but only allows a certain number of outstanding promises at any 37 | * given time. 38 | * 39 | * $concurrency may be an integer or a function that accepts the number of 40 | * pending promises and returns a numeric concurrency limit value to allow 41 | * for dynamic a concurrency size. 42 | * 43 | * @param mixed $iterable 44 | * @param int|callable $concurrency 45 | */ 46 | public static function ofLimit( 47 | $iterable, 48 | $concurrency, 49 | ?callable $onFulfilled = null, 50 | ?callable $onRejected = null 51 | ): PromiseInterface { 52 | return (new EachPromise($iterable, [ 53 | 'fulfilled' => $onFulfilled, 54 | 'rejected' => $onRejected, 55 | 'concurrency' => $concurrency, 56 | ]))->promise(); 57 | } 58 | 59 | /** 60 | * Like limit, but ensures that no promise in the given $iterable argument 61 | * is rejected. If any promise is rejected, then the aggregate promise is 62 | * rejected with the encountered rejection. 63 | * 64 | * @param mixed $iterable 65 | * @param int|callable $concurrency 66 | */ 67 | public static function ofLimitAll( 68 | $iterable, 69 | $concurrency, 70 | ?callable $onFulfilled = null 71 | ): PromiseInterface { 72 | return self::ofLimit( 73 | $iterable, 74 | $concurrency, 75 | $onFulfilled, 76 | function ($reason, $idx, PromiseInterface $aggregate): void { 77 | $aggregate->reject($reason); 78 | } 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/EachPromise.php: -------------------------------------------------------------------------------- 1 | iterable = Create::iterFor($iterable); 61 | 62 | if (isset($config['concurrency'])) { 63 | $this->concurrency = $config['concurrency']; 64 | } 65 | 66 | if (isset($config['fulfilled'])) { 67 | $this->onFulfilled = $config['fulfilled']; 68 | } 69 | 70 | if (isset($config['rejected'])) { 71 | $this->onRejected = $config['rejected']; 72 | } 73 | } 74 | 75 | /** @psalm-suppress InvalidNullableReturnType */ 76 | public function promise(): PromiseInterface 77 | { 78 | if ($this->aggregate) { 79 | return $this->aggregate; 80 | } 81 | 82 | try { 83 | $this->createPromise(); 84 | /** @psalm-assert Promise $this->aggregate */ 85 | $this->iterable->rewind(); 86 | $this->refillPending(); 87 | } catch (\Throwable $e) { 88 | $this->aggregate->reject($e); 89 | } 90 | 91 | /** 92 | * @psalm-suppress NullableReturnStatement 93 | */ 94 | return $this->aggregate; 95 | } 96 | 97 | private function createPromise(): void 98 | { 99 | $this->mutex = false; 100 | $this->aggregate = new Promise(function (): void { 101 | if ($this->checkIfFinished()) { 102 | return; 103 | } 104 | reset($this->pending); 105 | // Consume a potentially fluctuating list of promises while 106 | // ensuring that indexes are maintained (precluding array_shift). 107 | while ($promise = current($this->pending)) { 108 | next($this->pending); 109 | $promise->wait(); 110 | if (Is::settled($this->aggregate)) { 111 | return; 112 | } 113 | } 114 | }); 115 | 116 | // Clear the references when the promise is resolved. 117 | $clearFn = function (): void { 118 | $this->iterable = $this->concurrency = $this->pending = null; 119 | $this->onFulfilled = $this->onRejected = null; 120 | $this->nextPendingIndex = 0; 121 | }; 122 | 123 | $this->aggregate->then($clearFn, $clearFn); 124 | } 125 | 126 | private function refillPending(): void 127 | { 128 | if (!$this->concurrency) { 129 | // Add all pending promises. 130 | while ($this->addPending() && $this->advanceIterator()) { 131 | } 132 | 133 | return; 134 | } 135 | 136 | // Add only up to N pending promises. 137 | $concurrency = is_callable($this->concurrency) 138 | ? ($this->concurrency)(count($this->pending)) 139 | : $this->concurrency; 140 | $concurrency = max($concurrency - count($this->pending), 0); 141 | // Concurrency may be set to 0 to disallow new promises. 142 | if (!$concurrency) { 143 | return; 144 | } 145 | // Add the first pending promise. 146 | $this->addPending(); 147 | // Note this is special handling for concurrency=1 so that we do 148 | // not advance the iterator after adding the first promise. This 149 | // helps work around issues with generators that might not have the 150 | // next value to yield until promise callbacks are called. 151 | while (--$concurrency 152 | && $this->advanceIterator() 153 | && $this->addPending()) { 154 | } 155 | } 156 | 157 | private function addPending(): bool 158 | { 159 | if (!$this->iterable || !$this->iterable->valid()) { 160 | return false; 161 | } 162 | 163 | $promise = Create::promiseFor($this->iterable->current()); 164 | $key = $this->iterable->key(); 165 | 166 | // Iterable keys may not be unique, so we use a counter to 167 | // guarantee uniqueness 168 | $idx = $this->nextPendingIndex++; 169 | 170 | $this->pending[$idx] = $promise->then( 171 | function ($value) use ($idx, $key): void { 172 | if ($this->onFulfilled) { 173 | ($this->onFulfilled)( 174 | $value, 175 | $key, 176 | $this->aggregate 177 | ); 178 | } 179 | $this->step($idx); 180 | }, 181 | function ($reason) use ($idx, $key): void { 182 | if ($this->onRejected) { 183 | ($this->onRejected)( 184 | $reason, 185 | $key, 186 | $this->aggregate 187 | ); 188 | } 189 | $this->step($idx); 190 | } 191 | ); 192 | 193 | return true; 194 | } 195 | 196 | private function advanceIterator(): bool 197 | { 198 | // Place a lock on the iterator so that we ensure to not recurse, 199 | // preventing fatal generator errors. 200 | if ($this->mutex) { 201 | return false; 202 | } 203 | 204 | $this->mutex = true; 205 | 206 | try { 207 | $this->iterable->next(); 208 | $this->mutex = false; 209 | 210 | return true; 211 | } catch (\Throwable $e) { 212 | $this->aggregate->reject($e); 213 | $this->mutex = false; 214 | 215 | return false; 216 | } 217 | } 218 | 219 | private function step(int $idx): void 220 | { 221 | // If the promise was already resolved, then ignore this step. 222 | if (Is::settled($this->aggregate)) { 223 | return; 224 | } 225 | 226 | unset($this->pending[$idx]); 227 | 228 | // Only refill pending promises if we are not locked, preventing the 229 | // EachPromise to recursively invoke the provided iterator, which 230 | // cause a fatal error: "Cannot resume an already running generator" 231 | if ($this->advanceIterator() && !$this->checkIfFinished()) { 232 | // Add more pending promises if possible. 233 | $this->refillPending(); 234 | } 235 | } 236 | 237 | private function checkIfFinished(): bool 238 | { 239 | if (!$this->pending && !$this->iterable->valid()) { 240 | // Resolve the promise if there's nothing left to do. 241 | $this->aggregate->resolve(null); 242 | 243 | return true; 244 | } 245 | 246 | return false; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/FulfilledPromise.php: -------------------------------------------------------------------------------- 1 | value = $value; 31 | } 32 | 33 | public function then( 34 | ?callable $onFulfilled = null, 35 | ?callable $onRejected = null 36 | ): PromiseInterface { 37 | // Return itself if there is no onFulfilled function. 38 | if (!$onFulfilled) { 39 | return $this; 40 | } 41 | 42 | $queue = Utils::queue(); 43 | $p = new Promise([$queue, 'run']); 44 | $value = $this->value; 45 | $queue->add(static function () use ($p, $value, $onFulfilled): void { 46 | if (Is::pending($p)) { 47 | try { 48 | $p->resolve($onFulfilled($value)); 49 | } catch (\Throwable $e) { 50 | $p->reject($e); 51 | } 52 | } 53 | }); 54 | 55 | return $p; 56 | } 57 | 58 | public function otherwise(callable $onRejected): PromiseInterface 59 | { 60 | return $this->then(null, $onRejected); 61 | } 62 | 63 | public function wait(bool $unwrap = true) 64 | { 65 | return $unwrap ? $this->value : null; 66 | } 67 | 68 | public function getState(): string 69 | { 70 | return self::FULFILLED; 71 | } 72 | 73 | public function resolve($value): void 74 | { 75 | if ($value !== $this->value) { 76 | throw new \LogicException('Cannot resolve a fulfilled promise'); 77 | } 78 | } 79 | 80 | public function reject($reason): void 81 | { 82 | throw new \LogicException('Cannot reject a fulfilled promise'); 83 | } 84 | 85 | public function cancel(): void 86 | { 87 | // pass 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Is.php: -------------------------------------------------------------------------------- 1 | getState() === PromiseInterface::PENDING; 15 | } 16 | 17 | /** 18 | * Returns true if a promise is fulfilled or rejected. 19 | */ 20 | public static function settled(PromiseInterface $promise): bool 21 | { 22 | return $promise->getState() !== PromiseInterface::PENDING; 23 | } 24 | 25 | /** 26 | * Returns true if a promise is fulfilled. 27 | */ 28 | public static function fulfilled(PromiseInterface $promise): bool 29 | { 30 | return $promise->getState() === PromiseInterface::FULFILLED; 31 | } 32 | 33 | /** 34 | * Returns true if a promise is rejected. 35 | */ 36 | public static function rejected(PromiseInterface $promise): bool 37 | { 38 | return $promise->getState() === PromiseInterface::REJECTED; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Promise.php: -------------------------------------------------------------------------------- 1 | waitFn = $waitFn; 32 | $this->cancelFn = $cancelFn; 33 | } 34 | 35 | public function then( 36 | ?callable $onFulfilled = null, 37 | ?callable $onRejected = null 38 | ): PromiseInterface { 39 | if ($this->state === self::PENDING) { 40 | $p = new Promise(null, [$this, 'cancel']); 41 | $this->handlers[] = [$p, $onFulfilled, $onRejected]; 42 | $p->waitList = $this->waitList; 43 | $p->waitList[] = $this; 44 | 45 | return $p; 46 | } 47 | 48 | // Return a fulfilled promise and immediately invoke any callbacks. 49 | if ($this->state === self::FULFILLED) { 50 | $promise = Create::promiseFor($this->result); 51 | 52 | return $onFulfilled ? $promise->then($onFulfilled) : $promise; 53 | } 54 | 55 | // It's either cancelled or rejected, so return a rejected promise 56 | // and immediately invoke any callbacks. 57 | $rejection = Create::rejectionFor($this->result); 58 | 59 | return $onRejected ? $rejection->then(null, $onRejected) : $rejection; 60 | } 61 | 62 | public function otherwise(callable $onRejected): PromiseInterface 63 | { 64 | return $this->then(null, $onRejected); 65 | } 66 | 67 | public function wait(bool $unwrap = true) 68 | { 69 | $this->waitIfPending(); 70 | 71 | if ($this->result instanceof PromiseInterface) { 72 | return $this->result->wait($unwrap); 73 | } 74 | if ($unwrap) { 75 | if ($this->state === self::FULFILLED) { 76 | return $this->result; 77 | } 78 | // It's rejected so "unwrap" and throw an exception. 79 | throw Create::exceptionFor($this->result); 80 | } 81 | } 82 | 83 | public function getState(): string 84 | { 85 | return $this->state; 86 | } 87 | 88 | public function cancel(): void 89 | { 90 | if ($this->state !== self::PENDING) { 91 | return; 92 | } 93 | 94 | $this->waitFn = $this->waitList = null; 95 | 96 | if ($this->cancelFn) { 97 | $fn = $this->cancelFn; 98 | $this->cancelFn = null; 99 | try { 100 | $fn(); 101 | } catch (\Throwable $e) { 102 | $this->reject($e); 103 | } 104 | } 105 | 106 | // Reject the promise only if it wasn't rejected in a then callback. 107 | /** @psalm-suppress RedundantCondition */ 108 | if ($this->state === self::PENDING) { 109 | $this->reject(new CancellationException('Promise has been cancelled')); 110 | } 111 | } 112 | 113 | public function resolve($value): void 114 | { 115 | $this->settle(self::FULFILLED, $value); 116 | } 117 | 118 | public function reject($reason): void 119 | { 120 | $this->settle(self::REJECTED, $reason); 121 | } 122 | 123 | private function settle(string $state, $value): void 124 | { 125 | if ($this->state !== self::PENDING) { 126 | // Ignore calls with the same resolution. 127 | if ($state === $this->state && $value === $this->result) { 128 | return; 129 | } 130 | throw $this->state === $state 131 | ? new \LogicException("The promise is already {$state}.") 132 | : new \LogicException("Cannot change a {$this->state} promise to {$state}"); 133 | } 134 | 135 | if ($value === $this) { 136 | throw new \LogicException('Cannot fulfill or reject a promise with itself'); 137 | } 138 | 139 | // Clear out the state of the promise but stash the handlers. 140 | $this->state = $state; 141 | $this->result = $value; 142 | $handlers = $this->handlers; 143 | $this->handlers = null; 144 | $this->waitList = $this->waitFn = null; 145 | $this->cancelFn = null; 146 | 147 | if (!$handlers) { 148 | return; 149 | } 150 | 151 | // If the value was not a settled promise or a thenable, then resolve 152 | // it in the task queue using the correct ID. 153 | if (!is_object($value) || !method_exists($value, 'then')) { 154 | $id = $state === self::FULFILLED ? 1 : 2; 155 | // It's a success, so resolve the handlers in the queue. 156 | Utils::queue()->add(static function () use ($id, $value, $handlers): void { 157 | foreach ($handlers as $handler) { 158 | self::callHandler($id, $value, $handler); 159 | } 160 | }); 161 | } elseif ($value instanceof Promise && Is::pending($value)) { 162 | // We can just merge our handlers onto the next promise. 163 | $value->handlers = array_merge($value->handlers, $handlers); 164 | } else { 165 | // Resolve the handlers when the forwarded promise is resolved. 166 | $value->then( 167 | static function ($value) use ($handlers): void { 168 | foreach ($handlers as $handler) { 169 | self::callHandler(1, $value, $handler); 170 | } 171 | }, 172 | static function ($reason) use ($handlers): void { 173 | foreach ($handlers as $handler) { 174 | self::callHandler(2, $reason, $handler); 175 | } 176 | } 177 | ); 178 | } 179 | } 180 | 181 | /** 182 | * Call a stack of handlers using a specific callback index and value. 183 | * 184 | * @param int $index 1 (resolve) or 2 (reject). 185 | * @param mixed $value Value to pass to the callback. 186 | * @param array $handler Array of handler data (promise and callbacks). 187 | */ 188 | private static function callHandler(int $index, $value, array $handler): void 189 | { 190 | /** @var PromiseInterface $promise */ 191 | $promise = $handler[0]; 192 | 193 | // The promise may have been cancelled or resolved before placing 194 | // this thunk in the queue. 195 | if (Is::settled($promise)) { 196 | return; 197 | } 198 | 199 | try { 200 | if (isset($handler[$index])) { 201 | /* 202 | * If $f throws an exception, then $handler will be in the exception 203 | * stack trace. Since $handler contains a reference to the callable 204 | * itself we get a circular reference. We clear the $handler 205 | * here to avoid that memory leak. 206 | */ 207 | $f = $handler[$index]; 208 | unset($handler); 209 | $promise->resolve($f($value)); 210 | } elseif ($index === 1) { 211 | // Forward resolution values as-is. 212 | $promise->resolve($value); 213 | } else { 214 | // Forward rejections down the chain. 215 | $promise->reject($value); 216 | } 217 | } catch (\Throwable $reason) { 218 | $promise->reject($reason); 219 | } 220 | } 221 | 222 | private function waitIfPending(): void 223 | { 224 | if ($this->state !== self::PENDING) { 225 | return; 226 | } elseif ($this->waitFn) { 227 | $this->invokeWaitFn(); 228 | } elseif ($this->waitList) { 229 | $this->invokeWaitList(); 230 | } else { 231 | // If there's no wait function, then reject the promise. 232 | $this->reject('Cannot wait on a promise that has ' 233 | .'no internal wait function. You must provide a wait ' 234 | .'function when constructing the promise to be able to ' 235 | .'wait on a promise.'); 236 | } 237 | 238 | Utils::queue()->run(); 239 | 240 | /** @psalm-suppress RedundantCondition */ 241 | if ($this->state === self::PENDING) { 242 | $this->reject('Invoking the wait callback did not resolve the promise'); 243 | } 244 | } 245 | 246 | private function invokeWaitFn(): void 247 | { 248 | try { 249 | $wfn = $this->waitFn; 250 | $this->waitFn = null; 251 | $wfn(true); 252 | } catch (\Throwable $reason) { 253 | if ($this->state === self::PENDING) { 254 | // The promise has not been resolved yet, so reject the promise 255 | // with the exception. 256 | $this->reject($reason); 257 | } else { 258 | // The promise was already resolved, so there's a problem in 259 | // the application. 260 | throw $reason; 261 | } 262 | } 263 | } 264 | 265 | private function invokeWaitList(): void 266 | { 267 | $waitList = $this->waitList; 268 | $this->waitList = null; 269 | 270 | foreach ($waitList as $result) { 271 | do { 272 | $result->waitIfPending(); 273 | $result = $result->result; 274 | } while ($result instanceof Promise); 275 | 276 | if ($result instanceof PromiseInterface) { 277 | $result->wait(false); 278 | } 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/PromiseInterface.php: -------------------------------------------------------------------------------- 1 | reason = $reason; 31 | } 32 | 33 | public function then( 34 | ?callable $onFulfilled = null, 35 | ?callable $onRejected = null 36 | ): PromiseInterface { 37 | // If there's no onRejected callback then just return self. 38 | if (!$onRejected) { 39 | return $this; 40 | } 41 | 42 | $queue = Utils::queue(); 43 | $reason = $this->reason; 44 | $p = new Promise([$queue, 'run']); 45 | $queue->add(static function () use ($p, $reason, $onRejected): void { 46 | if (Is::pending($p)) { 47 | try { 48 | // Return a resolved promise if onRejected does not throw. 49 | $p->resolve($onRejected($reason)); 50 | } catch (\Throwable $e) { 51 | // onRejected threw, so return a rejected promise. 52 | $p->reject($e); 53 | } 54 | } 55 | }); 56 | 57 | return $p; 58 | } 59 | 60 | public function otherwise(callable $onRejected): PromiseInterface 61 | { 62 | return $this->then(null, $onRejected); 63 | } 64 | 65 | public function wait(bool $unwrap = true) 66 | { 67 | if ($unwrap) { 68 | throw Create::exceptionFor($this->reason); 69 | } 70 | 71 | return null; 72 | } 73 | 74 | public function getState(): string 75 | { 76 | return self::REJECTED; 77 | } 78 | 79 | public function resolve($value): void 80 | { 81 | throw new \LogicException('Cannot resolve a rejected promise'); 82 | } 83 | 84 | public function reject($reason): void 85 | { 86 | if ($reason !== $this->reason) { 87 | throw new \LogicException('Cannot reject a rejected promise'); 88 | } 89 | } 90 | 91 | public function cancel(): void 92 | { 93 | // pass 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/RejectionException.php: -------------------------------------------------------------------------------- 1 | reason = $reason; 24 | 25 | $message = 'The promise was rejected'; 26 | 27 | if ($description) { 28 | $message .= ' with reason: '.$description; 29 | } elseif (is_string($reason) 30 | || (is_object($reason) && method_exists($reason, '__toString')) 31 | ) { 32 | $message .= ' with reason: '.$this->reason; 33 | } elseif ($reason instanceof \JsonSerializable) { 34 | $message .= ' with reason: '.json_encode($this->reason, JSON_PRETTY_PRINT); 35 | } 36 | 37 | parent::__construct($message); 38 | } 39 | 40 | /** 41 | * Returns the rejection reason. 42 | * 43 | * @return mixed 44 | */ 45 | public function getReason() 46 | { 47 | return $this->reason; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/TaskQueue.php: -------------------------------------------------------------------------------- 1 | run(); 15 | * 16 | * @final 17 | */ 18 | class TaskQueue implements TaskQueueInterface 19 | { 20 | private $enableShutdown = true; 21 | private $queue = []; 22 | 23 | public function __construct(bool $withShutdown = true) 24 | { 25 | if ($withShutdown) { 26 | register_shutdown_function(function (): void { 27 | if ($this->enableShutdown) { 28 | // Only run the tasks if an E_ERROR didn't occur. 29 | $err = error_get_last(); 30 | if (!$err || ($err['type'] ^ E_ERROR)) { 31 | $this->run(); 32 | } 33 | } 34 | }); 35 | } 36 | } 37 | 38 | public function isEmpty(): bool 39 | { 40 | return !$this->queue; 41 | } 42 | 43 | public function add(callable $task): void 44 | { 45 | $this->queue[] = $task; 46 | } 47 | 48 | public function run(): void 49 | { 50 | while ($task = array_shift($this->queue)) { 51 | /** @var callable $task */ 52 | $task(); 53 | } 54 | } 55 | 56 | /** 57 | * The task queue will be run and exhausted by default when the process 58 | * exits IFF the exit is not the result of a PHP E_ERROR error. 59 | * 60 | * You can disable running the automatic shutdown of the queue by calling 61 | * this function. If you disable the task queue shutdown process, then you 62 | * MUST either run the task queue (as a result of running your event loop 63 | * or manually using the run() method) or wait on each outstanding promise. 64 | * 65 | * Note: This shutdown will occur before any destructors are triggered. 66 | */ 67 | public function disableShutdown(): void 68 | { 69 | $this->enableShutdown = false; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/TaskQueueInterface.php: -------------------------------------------------------------------------------- 1 | 17 | * while ($eventLoop->isRunning()) { 18 | * GuzzleHttp\Promise\Utils::queue()->run(); 19 | * } 20 | * 21 | * 22 | * @param TaskQueueInterface|null $assign Optionally specify a new queue instance. 23 | */ 24 | public static function queue(?TaskQueueInterface $assign = null): TaskQueueInterface 25 | { 26 | static $queue; 27 | 28 | if ($assign) { 29 | $queue = $assign; 30 | } elseif (!$queue) { 31 | $queue = new TaskQueue(); 32 | } 33 | 34 | return $queue; 35 | } 36 | 37 | /** 38 | * Adds a function to run in the task queue when it is next `run()` and 39 | * returns a promise that is fulfilled or rejected with the result. 40 | * 41 | * @param callable $task Task function to run. 42 | */ 43 | public static function task(callable $task): PromiseInterface 44 | { 45 | $queue = self::queue(); 46 | $promise = new Promise([$queue, 'run']); 47 | $queue->add(function () use ($task, $promise): void { 48 | try { 49 | if (Is::pending($promise)) { 50 | $promise->resolve($task()); 51 | } 52 | } catch (\Throwable $e) { 53 | $promise->reject($e); 54 | } 55 | }); 56 | 57 | return $promise; 58 | } 59 | 60 | /** 61 | * Synchronously waits on a promise to resolve and returns an inspection 62 | * state array. 63 | * 64 | * Returns a state associative array containing a "state" key mapping to a 65 | * valid promise state. If the state of the promise is "fulfilled", the 66 | * array will contain a "value" key mapping to the fulfilled value of the 67 | * promise. If the promise is rejected, the array will contain a "reason" 68 | * key mapping to the rejection reason of the promise. 69 | * 70 | * @param PromiseInterface $promise Promise or value. 71 | */ 72 | public static function inspect(PromiseInterface $promise): array 73 | { 74 | try { 75 | return [ 76 | 'state' => PromiseInterface::FULFILLED, 77 | 'value' => $promise->wait(), 78 | ]; 79 | } catch (RejectionException $e) { 80 | return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()]; 81 | } catch (\Throwable $e) { 82 | return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; 83 | } 84 | } 85 | 86 | /** 87 | * Waits on all of the provided promises, but does not unwrap rejected 88 | * promises as thrown exception. 89 | * 90 | * Returns an array of inspection state arrays. 91 | * 92 | * @see inspect for the inspection state array format. 93 | * 94 | * @param PromiseInterface[] $promises Traversable of promises to wait upon. 95 | */ 96 | public static function inspectAll($promises): array 97 | { 98 | $results = []; 99 | foreach ($promises as $key => $promise) { 100 | $results[$key] = self::inspect($promise); 101 | } 102 | 103 | return $results; 104 | } 105 | 106 | /** 107 | * Waits on all of the provided promises and returns the fulfilled values. 108 | * 109 | * Returns an array that contains the value of each promise (in the same 110 | * order the promises were provided). An exception is thrown if any of the 111 | * promises are rejected. 112 | * 113 | * @param iterable $promises Iterable of PromiseInterface objects to wait on. 114 | * 115 | * @throws \Throwable on error 116 | */ 117 | public static function unwrap($promises): array 118 | { 119 | $results = []; 120 | foreach ($promises as $key => $promise) { 121 | $results[$key] = $promise->wait(); 122 | } 123 | 124 | return $results; 125 | } 126 | 127 | /** 128 | * Given an array of promises, return a promise that is fulfilled when all 129 | * the items in the array are fulfilled. 130 | * 131 | * The promise's fulfillment value is an array with fulfillment values at 132 | * respective positions to the original array. If any promise in the array 133 | * rejects, the returned promise is rejected with the rejection reason. 134 | * 135 | * @param mixed $promises Promises or values. 136 | * @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution. 137 | */ 138 | public static function all($promises, bool $recursive = false): PromiseInterface 139 | { 140 | $results = []; 141 | $promise = Each::of( 142 | $promises, 143 | function ($value, $idx) use (&$results): void { 144 | $results[$idx] = $value; 145 | }, 146 | function ($reason, $idx, Promise $aggregate): void { 147 | if (Is::pending($aggregate)) { 148 | $aggregate->reject($reason); 149 | } 150 | } 151 | )->then(function () use (&$results) { 152 | ksort($results); 153 | 154 | return $results; 155 | }); 156 | 157 | if (true === $recursive) { 158 | $promise = $promise->then(function ($results) use ($recursive, &$promises) { 159 | foreach ($promises as $promise) { 160 | if (Is::pending($promise)) { 161 | return self::all($promises, $recursive); 162 | } 163 | } 164 | 165 | return $results; 166 | }); 167 | } 168 | 169 | return $promise; 170 | } 171 | 172 | /** 173 | * Initiate a competitive race between multiple promises or values (values 174 | * will become immediately fulfilled promises). 175 | * 176 | * When count amount of promises have been fulfilled, the returned promise 177 | * is fulfilled with an array that contains the fulfillment values of the 178 | * winners in order of resolution. 179 | * 180 | * This promise is rejected with a {@see AggregateException} if the number 181 | * of fulfilled promises is less than the desired $count. 182 | * 183 | * @param int $count Total number of promises. 184 | * @param mixed $promises Promises or values. 185 | */ 186 | public static function some(int $count, $promises): PromiseInterface 187 | { 188 | $results = []; 189 | $rejections = []; 190 | 191 | return Each::of( 192 | $promises, 193 | function ($value, $idx, PromiseInterface $p) use (&$results, $count): void { 194 | if (Is::settled($p)) { 195 | return; 196 | } 197 | $results[$idx] = $value; 198 | if (count($results) >= $count) { 199 | $p->resolve(null); 200 | } 201 | }, 202 | function ($reason) use (&$rejections): void { 203 | $rejections[] = $reason; 204 | } 205 | )->then( 206 | function () use (&$results, &$rejections, $count) { 207 | if (count($results) !== $count) { 208 | throw new AggregateException( 209 | 'Not enough promises to fulfill count', 210 | $rejections 211 | ); 212 | } 213 | ksort($results); 214 | 215 | return array_values($results); 216 | } 217 | ); 218 | } 219 | 220 | /** 221 | * Like some(), with 1 as count. However, if the promise fulfills, the 222 | * fulfillment value is not an array of 1 but the value directly. 223 | * 224 | * @param mixed $promises Promises or values. 225 | */ 226 | public static function any($promises): PromiseInterface 227 | { 228 | return self::some(1, $promises)->then(function ($values) { 229 | return $values[0]; 230 | }); 231 | } 232 | 233 | /** 234 | * Returns a promise that is fulfilled when all of the provided promises have 235 | * been fulfilled or rejected. 236 | * 237 | * The returned promise is fulfilled with an array of inspection state arrays. 238 | * 239 | * @see inspect for the inspection state array format. 240 | * 241 | * @param mixed $promises Promises or values. 242 | */ 243 | public static function settle($promises): PromiseInterface 244 | { 245 | $results = []; 246 | 247 | return Each::of( 248 | $promises, 249 | function ($value, $idx) use (&$results): void { 250 | $results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value]; 251 | }, 252 | function ($reason, $idx) use (&$results): void { 253 | $results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason]; 254 | } 255 | )->then(function () use (&$results) { 256 | ksort($results); 257 | 258 | return $results; 259 | }); 260 | } 261 | } 262 | --------------------------------------------------------------------------------