This commit is contained in:
Javier Casares 2026-08-18 07:00:18 +00:00
commit 77d0cea926
995 changed files with 28142 additions and 8603 deletions

View file

@ -1,6 +1,51 @@
# CHANGELOG
## 3.0.1 - 2026-08-05
### Changed
- Changed the default `TReason` of `FulfilledPromise` and `Create::promiseFor()` to `never`
- Changed the default `TValue` of `RejectedPromise` and `Create::rejectionFor()` to `never`
### Fixed
- Fixed `EachPromise` abandoning its aggregate when the pending window drains unsettled
- Fixed `EachPromise` admitting new work after its aggregate has settled
## 3.0.0 - 2026-07-20
### Added
- Added `concurrency` config support to `Utils::all()` and `Each::of()`
- Added generic PHPDoc annotations to promise APIs and collection callbacks
- Added recursive and `concurrency` config support to `Utils::settle()`
- Allowed promises to be resolved without passing a value
### Changed
- Changed `Utils::inspect()` to return actual rejection reasons
- Changed `Utils::inspect()` to prefer the settled state over late wait function exceptions
- Changed late rejection callbacks to follow rejected promises
- Reject native PHP serialization of in-flight runtime objects
- Made static helper classes non-instantiable
- Require iterable inputs for promise collection helpers and `EachPromise`
- Iterate `IteratorAggregate` inputs to collection helpers instead of treating them as a single value
- Improved recursive `Utils::all()` handling of dynamically-added settled values and raw values
### Removed
- Dropped support for PHP 7.2 and 7.3
## 2.5.1 - 2026-07-08
### Fixed
- Fixed recursive `Utils::all()` rejecting generator inputs
## 2.5.0 - 2026-06-02
### Deprecated

View file

@ -1,536 +1,93 @@
# Guzzle Promises
[Promises/A+](https://promisesaplus.com/) implementation that handles promise
chaining and resolution iteratively, allowing for "infinite" promise chaining
while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/)
for a general introduction to promises.
- [Features](#features)
- [Quick start](#quick-start)
- [Synchronous wait](#synchronous-wait)
- [Cancellation](#cancellation)
- [API](#api)
- [Promise](#promise)
- [FulfilledPromise](#fulfilledpromise)
- [RejectedPromise](#rejectedpromise)
- [Promise interop](#promise-interop)
- [Implementation notes](#implementation-notes)
## Features
- [Promises/A+](https://promisesaplus.com/) implementation.
- Promise resolution and chaining is handled iteratively, allowing for
"infinite" promise chaining.
- Promises have a synchronous `wait` method.
- Promises can be cancelled.
- Works with any object that has a `then` function.
- C# style async/await coroutine promises using
`GuzzleHttp\Promise\Coroutine::of()`.
`guzzlehttp/promises` is a small promise library used by Guzzle for asynchronous
operations. It implements promise chaining, synchronous waiting, cancellation,
and helpers for working with groups of promises.
Most application developers use this package through
[`guzzlehttp/guzzle`](https://github.com/guzzle/guzzle/blob/8.0/README.md) by
calling methods such as `requestAsync()`. Install this package directly when you
need promise composition without the full HTTP client.
## Installation
```shell
```bash
composer require guzzlehttp/promises
```
## Version Guidance
| Version | Status | PHP Version |
|---------|---------------------|--------------|
| 1.x | Security fixes only | >=5.5,<8.3 |
| 2.x | Latest | >=7.2.5,<8.6 |
| Version | Status | PHP Version |
|---------|--------------|--------------|
| 3.0 | Latest | >=7.4,<8.6 |
| 2.5 | Maintenance | >=7.2.5,<8.6 |
| 1.5 | End of Life | >=5.5,<8.3 |
## Quick Start
A *promise* represents the eventual result of an asynchronous operation. The
primary way of interacting with a promise is through its `then` method, which
registers callbacks to receive either a promise's eventual value or the reason
why the promise cannot be fulfilled.
### Callbacks
Callbacks are registered with the `then` method by providing an optional
`$onFulfilled` followed by an optional `$onRejected` function.
```php
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\Utils;
$promise = new Promise();
$promise->then(
// $onFulfilled
function ($value) {
echo 'The promise was fulfilled.';
echo 'Fulfilled: ' . $value;
},
// $onRejected
function ($reason) {
echo 'The promise was rejected.';
}
);
```
*Resolving* a promise means that you either fulfill a promise with a *value* or
reject a promise with a *reason*. Resolving a promise triggers callbacks
registered with the promise's `then` method. These callbacks are triggered
only once and in the order in which they were added.
### Resolving a Promise
Promises are fulfilled using the `resolve($value)` method. Resolving a promise
with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger
all of the onFulfilled callbacks (resolving a promise with a rejected promise
will reject the promise and trigger the `$onRejected` callbacks).
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$promise
->then(function ($value) {
// Return a value and don't break the chain
return "Hello, " . $value;
})
// This then is executed after the first then and receives the value
// returned from the first then.
->then(function ($value) {
echo $value;
});
// Resolving the promise triggers the $onFulfilled callbacks and outputs
// "Hello, reader."
$promise->resolve('reader.');
```
### Promise Forwarding
Promises can be chained one after the other. Each then in the chain is a new
promise. The return value of a promise is what's forwarded to the next
promise in the chain. Returning a promise in a `then` callback will cause the
subsequent promises in the chain to only be fulfilled when the returned promise
has been fulfilled. The next promise in the chain will be invoked with the
resolved value of the promise.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$nextPromise = new Promise();
$promise
->then(function ($value) use ($nextPromise) {
echo $value;
return $nextPromise;
})
->then(function ($value) {
echo $value;
});
// Triggers the first callback and outputs "A"
$promise->resolve('A');
// Triggers the second callback and outputs "B"
$nextPromise->resolve('B');
```
### Promise Rejection
When a promise is rejected, the `$onRejected` callbacks are invoked with the
rejection reason.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$promise->then(null, function ($reason) {
echo $reason;
});
$promise->reject('Error!');
// Outputs "Error!"
```
### Rejection Forwarding
If an exception is thrown in an `$onRejected` callback, subsequent
`$onRejected` callbacks are invoked with the thrown exception as the reason.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$promise->then(null, function ($reason) {
throw new Exception($reason);
})->then(null, function ($reason) {
assert($reason->getMessage() === 'Error!');
});
$promise->reject('Error!');
```
You can also forward a rejection down the promise chain by returning a
`GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or
`$onRejected` callback.
```php
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\RejectedPromise;
$promise = new Promise();
$promise->then(null, function ($reason) {
return new RejectedPromise($reason);
})->then(null, function ($reason) {
assert($reason === 'Error!');
});
$promise->reject('Error!');
```
If an exception is not thrown in a `$onRejected` callback and the callback
does not return a rejected promise, downstream `$onFulfilled` callbacks are
invoked using the value returned from the `$onRejected` callback.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise();
$promise
->then(null, function ($reason) {
return "It's ok";
})
->then(function ($value) {
assert($value === "It's ok");
});
$promise->reject('Error!');
```
## Synchronous Wait
You can synchronously force promises to complete using a promise's `wait`
method. When creating a promise, you can provide a wait function that is used
to synchronously force a promise to complete. When a wait function is invoked
it is expected to deliver a value to the promise or reject the promise. If the
wait function does not deliver a value, then an exception is thrown. The wait
function provided to a promise constructor is invoked when the `wait` function
of the promise is called.
```php
$promise = new Promise(function () use (&$promise) {
$promise->resolve('foo');
});
// Calling wait will return the value of the promise.
echo $promise->wait(); // outputs "foo"
```
If a throwable is encountered while invoking the wait function of a promise,
the promise is rejected with the throwable and the throwable is thrown.
```php
$promise = new Promise(function () use (&$promise) {
throw new Exception('foo');
});
$promise->wait(); // throws the exception.
```
Calling `wait` on a promise that has been fulfilled will not trigger the wait
function. It will simply return the previously resolved value.
```php
$promise = new Promise(function () { die('this is not called!'); });
$promise->resolve('foo');
echo $promise->wait(); // outputs "foo"
```
Calling `wait` on a promise that has been rejected will throw. If the rejection
reason is an instance of `\Throwable` the reason is thrown.
Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason
can be obtained by calling the `getReason` method of the exception.
```php
$promise = new Promise();
$promise->reject('foo');
$promise->wait();
```
> PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo'
### Unwrapping a Promise
When synchronously waiting on a promise, you are joining the state of the
promise into the current state of execution (i.e., return the value of the
promise if it was fulfilled or throw an exception if it was rejected). This is
called "unwrapping" the promise. Waiting on a promise will by default unwrap
the promise state.
You can force a promise to resolve and *not* unwrap the state of the promise
by passing `false` to the first argument of the `wait` function:
```php
$promise = new Promise();
$promise->reject('foo');
// This will not throw an exception. It simply ensures the promise has
// been resolved.
$promise->wait(false);
```
When unwrapping a promise, the resolved value of the promise will be waited
upon until the unwrapped value is not a promise. This means that if you resolve
promise A with a promise B and unwrap promise A, the value returned by the
wait function will be the value delivered to promise B.
**Note**: when you do not unwrap the promise, no value is returned.
## Cancellation
You can cancel a promise that has not yet been fulfilled using the `cancel()`
method of a promise. When creating a promise you can provide an optional
cancel function that when invoked cancels the action of computing a resolution
of the promise.
## API
### Promise
When creating a promise object, you can provide an optional `$waitFn` and
`$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is
expected to resolve the promise. `$cancelFn` is a function with no arguments
that is expected to cancel the computation of a promise. It is invoked when the
`cancel()` method of a promise is called.
```php
use GuzzleHttp\Promise\Promise;
$promise = new Promise(
function () use (&$promise) {
$promise->resolve('waited');
},
function () {
// do something that will cancel the promise computation (e.g., close
// a socket, cancel a database query, etc...)
echo 'Rejected: ' . $reason;
}
);
assert('waited' === $promise->wait());
$promise->resolve('done');
Utils::queue()->run();
```
A promise has the following methods:
- `then(?callable $onFulfilled = null, ?callable $onRejected = null) : PromiseInterface`
Appends fulfillment and rejection handlers to the promise, and returns a new
promise resolving to the return value of the called handler. If a handler is
omitted, the original fulfillment value or rejection reason is forwarded.
- `otherwise(callable $onRejected) : PromiseInterface`
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.
- `wait($unwrap = true) : mixed`
Synchronously waits on the promise to complete.
`$unwrap` controls whether or not the value of the promise is returned for a
fulfilled promise or if an exception is thrown if the promise is rejected.
This is set to `true` by default.
- `cancel()`
Attempts to cancel the promise if possible. The promise being cancelled and
the parent most ancestor that has not yet been resolved will also be
cancelled. Any promises waiting on the cancelled promise to resolve will also
be cancelled.
- `getState() : string`
Returns the state of the promise. One of `pending`, `fulfilled`, or
`rejected`.
- `resolve($value)`
Fulfills the promise with the given `$value`.
- `reject($reason)`
Rejects the promise with the given `$reason`.
### FulfilledPromise
A fulfilled promise can be created to represent a promise that has been
fulfilled.
You can wait for a promise to complete synchronously:
```php
use GuzzleHttp\Promise\FulfilledPromise;
$promise = new FulfilledPromise('value');
// Fulfilled callbacks are immediately invoked.
$promise->then(function ($value) {
echo $value;
});
$value = $promise->wait();
```
### RejectedPromise
A rejected promise can be created to represent a promise that has been
rejected.
When using Guzzle HTTP requests, asynchronous methods return
`GuzzleHttp\Promise\PromiseInterface` instances:
```php
use GuzzleHttp\Promise\RejectedPromise;
$promise = new RejectedPromise('Error');
// Rejected callbacks are immediately invoked.
$promise->then(null, function ($reason) {
echo $reason;
});
$promise = $client->requestAsync('GET', 'https://example.com');
$response = $promise->wait();
```
## Documentation
## Promise Interoperability
This library works with foreign promises that have a `then` method. This means
you can use Guzzle promises with [React promises](https://github.com/reactphp/promise)
for example. When a foreign promise is returned inside of a then method
callback, promise resolution will occur recursively.
```php
// Create a React promise
$deferred = new React\Promise\Deferred();
$reactPromise = $deferred->promise();
// Create a Guzzle promise that is fulfilled with a React promise.
$guzzlePromise = new GuzzleHttp\Promise\Promise();
$guzzlePromise->then(function ($value) use ($reactPromise) {
// Do something something with the value...
// Return the React promise
return $reactPromise;
});
```
Please note that wait and cancel chaining is no longer possible when forwarding
a foreign promise. You will need to wrap a third-party promise with a Guzzle
promise in order to utilize wait and cancel functions with foreign promises.
### Event Loop Integration
In order to keep the stack size constant, Guzzle promises are resolved
asynchronously using a task queue. When waiting on promises synchronously, the
task queue will be automatically run to ensure that the blocking promise and
any forwarded promises are resolved. When using promises asynchronously in an
event loop, you will need to run the task queue on each tick of the loop. If
you do not run the task queue, then promises will not be resolved.
You can run the task queue using the `run()` method of the global task queue
instance.
```php
// Get the global task queue
$queue = GuzzleHttp\Promise\Utils::queue();
$queue->run();
```
For example, you could use Guzzle promises with React using a short periodic
timer. Avoid zero-interval timers because they may keep the loop busy even when
there is no promise work to run.
```php
$loop = React\EventLoop\Factory::create();
$loop->addPeriodicTimer(0.01, [$queue, 'run']);
```
## Implementation Notes
### Promise Resolution and Chaining is Handled Iteratively
By shuffling pending handlers from one owner to another, promises are
resolved iteratively, allowing for "infinite" then chaining.
```php
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Promise\Promise;
$parent = new Promise();
$p = $parent;
for ($i = 0; $i < 1000; $i++) {
$p = $p->then(function ($v) {
// The stack size remains constant (a good thing)
echo xdebug_get_stack_depth() . ', ';
return $v + 1;
});
}
$parent->resolve(0);
var_dump($p->wait()); // int(1000)
```
When a promise is fulfilled or rejected with a non-promise value, the promise
then takes ownership of the handlers of each child promise and delivers values
down the chain without using recursion.
When a promise is resolved with another promise, the original promise transfers
all of its pending handlers to the new promise. When the new promise is
eventually resolved, all of the pending handlers are delivered the forwarded
value.
### A Promise is the Deferred
Some promise libraries implement promises using a deferred object to represent
a computation and a promise object to represent the delivery of the result of
the computation. This is a nice separation of computation and delivery because
consumers of the promise cannot modify the value that will be eventually
delivered.
One side effect of being able to implement promise resolution and chaining
iteratively is that you need to be able for one promise to reach into the state
of another promise to shuffle around ownership of handlers. In order to achieve
this without making the handlers of a promise publicly mutable, a promise is
also the deferred value, allowing promises of the same parent class to reach
into and modify the private properties of promises of the same type. While this
does allow consumers of the value to modify the resolution or rejection of the
deferred, it is a small price to pay for keeping the stack size constant.
```php
$promise = new Promise();
$promise->then(function ($value) { echo $value; });
// The promise is the deferred value, so you can deliver a value to it.
$promise->resolve('foo');
// prints "foo"
```
## Upgrading
See [UPGRADING.md](UPGRADING.md) for package upgrade notes.
- [Promise Quick Start](docs/promise-quick-start.md)
- [Promise API](docs/promise-api.md)
- [Promise Interoperability](docs/promise-interoperability.md)
- [Implementation Notes](docs/implementation-notes.md)
- [Upgrade Guide](UPGRADING.md)
- [Changelog](CHANGELOG.md)
## Security
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.
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.
## License
Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information.
Guzzle is made available under the MIT License (MIT). Please see
[License File](LICENSE) for more information.
## For Enterprise
Available as part of the Tidelift Subscription
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)
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)

View file

@ -1,6 +1,202 @@
Guzzle Promises Upgrade Guide
=============================
2.x to 3.0
----------
Guzzle Promises 3.0 is a major release that raises the minimum PHP version,
updates promise and collection helper signatures, adds generic PHPDoc types for
static analyzers, tightens collection helper inputs, improves recursive
collection behavior, and clarifies rejection inspection and late rejection
callback behavior.
#### PHP Version and Dependencies
Guzzle Promises 3.0 requires PHP `^7.4 || ^8.0`. Guzzle Promises 2.x supported
PHP `^7.2.5 || ^8.0`.
If your application still supports PHP 7.2 or 7.3, continue using Guzzle
Promises 2.x until your minimum PHP version is raised.
Guzzle Promises 3.0 has no runtime package dependencies beyond PHP. It removes
the 2.x runtime dependency on `symfony/deprecation-contracts`; require that
package directly if your application uses it.
#### Optional Promise Resolution Values
`PromiseInterface::resolve()` now accepts an optional value. Calling `resolve()`
without an argument fulfills the promise with `null`.
Custom implementations of `PromiseInterface`, and subclasses that override
`resolve()` on `Promise`, `FulfilledPromise`, or `RejectedPromise`, must update
their method signature from `resolve($value): void` to
`resolve($value = null): void`.
#### Generic PHPDoc Types
`PromiseInterface`, `PromisorInterface`, and the built-in promise classes now
include generic PHPDoc annotations for static analysis tools. The first template
type represents the fulfillment value and the second represents the rejection
reason. This is a static-analysis-only change and does not alter runtime
behavior, but projects with stricter static analysis may see new or different
diagnostics:
```php
use GuzzleHttp\Promise\PromiseInterface;
/** @var PromiseInterface<string, \Throwable> */
$promise = $factory->createPromise();
$value = $promise->wait();
```
Code that uses unparameterized promise types continues to work and is treated as
`PromiseInterface<mixed, mixed>`. If your project implements promise interfaces,
extends promise classes, or has stricter static analysis, you may need to update
your PHPDoc annotations to include the generic value and reason types. The
expanded PHPDoc also preserves promise-chain fulfillment and rejection types
more precisely through `then()` and `otherwise()`, and documents collection
callbacks with value or reason, key, and aggregate-promise arguments so
callbacks may declare only the arguments they use.
#### Collection Helper Inputs
Promise collection helpers now require iterable inputs. Passing a single promise
or scalar value directly now throws a `TypeError`.
Wrap single promises or values in an array before passing them to
`Create::iterFor()`, `Each::of()`, `Each::ofLimit()`, `Each::ofLimitAll()`,
`EachPromise`, or the `Utils` collection helpers.
```php
use GuzzleHttp\Promise\Each;
// 2.x
$promise = Each::ofLimit($singlePromise, 2);
// 3.0
$promise = Each::ofLimit([$singlePromise], 2);
```
`IteratorAggregate` inputs are now iterated via `getIterator()`. In 2.x an
aggregate was treated as a single value and produced one result; in 3.0 its
entries are consumed individually.
```php
use GuzzleHttp\Promise\Utils;
$aggregate = new \ArrayObject([$promiseA, $promiseB]);
// 2.x: one result — the ArrayObject itself
// 3.0: two results — the fulfillment values of $promiseA and $promiseB
$promise = Utils::all($aggregate);
```
#### Collection Helper Signatures
`Utils::all()`, `Utils::settle()`, and `Each::of()` now accept trailing optional
arguments. Direct calls using the 2.x argument lists continue to work, and the
affected helper classes are final so subclass signatures do not need to change.
Code that mirrors or reflects exact helper signatures may need to be updated.
Pass the recursive flag before the config array when using `Utils::all()` or
`Utils::settle()`:
```php
use GuzzleHttp\Promise\Utils;
$promise = Utils::all($promises, false, ['concurrency' => 5]);
$promise = Utils::settle($promises, false, ['concurrency' => 5]);
```
Only `concurrency` is honored by these helper config arrays. Callback config
keys such as `fulfilled` and `rejected` are ignored; pass callbacks to
`Each::of()` directly or use `EachPromise`.
#### Recursive Collection Helpers
Existing `Utils::all($promises, true)` calls may return different results in
3.0. Recursive mode now detects dynamically-added settled promises and raw
values. In 2.x, recursive mode only checked for pending promises.
If you previously worked around the lack of recursive `Utils::settle()` support,
you can replace that workaround with the new `$recursive` argument:
```php
use GuzzleHttp\Promise\Utils;
$promise = Utils::settle($promises, true);
```
When `$recursive` is true, collection helpers continue taking passes over the
collection until no new entries are found and no visible promises remain
pending. This is intended for rewindable mutable collections such as
`ArrayIterator`. Generators are safe to pass, but they cannot be traversed
again once consumed, so recursive mode degrades to a single pass; use a
rewindable mutable collection when recursion needs to observe added values.
#### Promise Inspection
`Utils::inspect()` and `Utils::inspectAll()` now return the actual rejection
reason delivered to rejection callbacks. They no longer unwrap
`RejectionException` instances to their inner reason.
For example, a promise rejected with a `RejectionException` now inspects with
that exception as the reason:
```php
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Promise\RejectionException;
use GuzzleHttp\Promise\Utils;
$reason = new RejectionException('reason');
$result = Utils::inspect(new RejectedPromise($reason));
assert($result['reason'] === $reason);
```
Cancelled promises now inspect with a `CancellationException` reason. If you
need the string reason from a `RejectionException` or subclass, call
`getReason()` on the exception.
`Utils::inspect()` still reports wait-function failures as rejection reasons
when the wait function does not settle the promise. If the wait function
settles the promise and then throws, `inspect()` reports the settled
fulfillment or rejection state; direct `Promise::wait()` calls continue to
throw that late exception.
#### Late Rejection Callbacks
Rejection callbacks registered after a promise was resolved with a rejected
promise are now invoked with the nested rejection reason.
```php
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Promise\Utils;
$promise = new Promise();
$promise->resolve(new RejectedPromise('reason'));
$promise->then(null, function ($reason): void {
assert($reason === 'reason');
});
Utils::queue()->run();
```
#### Non-instantiable Helper Classes
Static helper classes such as `Create`, `Each`, `Is`, and `Utils` now have
private constructors. Replace any accidental instantiation with static method
calls.
#### Native PHP Serialization of Runtime Objects
`Promise`, `TaskQueue`, `EachPromise`, and `Coroutine` no longer support native
PHP `serialize()` or `unserialize()`. Persist application values instead of
promise runtime state.
1.x to 2.0
----------

View file

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace GuzzleHttp\Promise;
use Generator;
use Throwable;
/**
* Creates a promise that is resolved using a generator that yields values or
@ -37,29 +36,35 @@ use Throwable;
* // Outputs "abc"
* $promise->then(function ($v) { echo $v; });
*
* @param callable $generatorFn Generator function to wrap into a promise.
* @template TValue = mixed
* @template TReason = mixed
*
* @return Promise
* @implements PromiseInterface<TValue, TReason>
*
* @see https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration
*/
final class Coroutine implements PromiseInterface
{
/**
* @var PromiseInterface|null
*/
private $currentPromise;
use NonSerializableTrait;
/**
* @var Generator
* @var PromiseInterface<mixed, mixed>|null
*/
private $generator;
private ?PromiseInterface $currentPromise = null;
/**
* @var Promise
* @var Generator<mixed, mixed, mixed, mixed>
*/
private $result;
private Generator $generator;
/**
* @var Promise<TValue, TReason>
*/
private PromiseInterface $result;
/**
* @param callable(): Generator<mixed, mixed, mixed, mixed> $generatorFn
*/
public function __construct(callable $generatorFn)
{
$this->generator = $generatorFn();
@ -70,19 +75,34 @@ final class Coroutine implements PromiseInterface
});
try {
$this->nextCoroutine($this->generator->current());
} catch (Throwable $throwable) {
} catch (\Throwable $throwable) {
$this->result->reject($throwable);
}
}
/**
* Create a new coroutine.
*
* @param callable(): Generator<mixed, mixed, mixed, mixed> $generatorFn
*
* @return self<mixed, mixed>
*/
public static function of(callable $generatorFn): self
{
return new self($generatorFn);
}
/**
* @template TFulfilledValue = never
* @template TFulfilledReason = never
* @template TRejectedValue = never
* @template TRejectedReason = never
*
* @param (callable(TValue): (TFulfilledValue|PromiseInterface<TFulfilledValue, TFulfilledReason>))|null $onFulfilled Invoked when the promise fulfills.
* @param (callable(TReason): (TRejectedValue|PromiseInterface<TRejectedValue, TRejectedReason>))|null $onRejected Invoked when the promise is rejected.
*
* @return PromiseInterface<($onFulfilled is null ? TValue : TFulfilledValue)|($onRejected is null ? never : TRejectedValue), ($onFulfilled is null ? never : TFulfilledReason|\Throwable)|($onRejected is null ? TReason : TRejectedReason|\Throwable)>
*/
public function then(
?callable $onFulfilled = null,
?callable $onRejected = null
@ -90,6 +110,14 @@ final class Coroutine implements PromiseInterface
return $this->result->then($onFulfilled, $onRejected);
}
/**
* @template TRejectedValue = never
* @template TRejectedReason = never
*
* @param callable(TReason): (TRejectedValue|PromiseInterface<TRejectedValue, TRejectedReason>) $onRejected Invoked when the promise is rejected.
*
* @return PromiseInterface<TValue|TRejectedValue, TRejectedReason|\Throwable>
*/
public function otherwise(callable $onRejected): PromiseInterface
{
return $this->result->otherwise($onRejected);
@ -105,7 +133,7 @@ final class Coroutine implements PromiseInterface
return $this->result->getState();
}
public function resolve($value): void
public function resolve($value = null): void
{
$this->result->resolve($value);
}
@ -135,7 +163,8 @@ final class Coroutine implements PromiseInterface
*/
public function _handleSuccess($value): void
{
unset($this->currentPromise);
$this->currentPromise = null;
try {
$next = $this->generator->send($value);
if ($this->generator->valid()) {
@ -143,7 +172,7 @@ final class Coroutine implements PromiseInterface
} else {
$this->result->resolve($value);
}
} catch (Throwable $throwable) {
} catch (\Throwable $throwable) {
$this->result->reject($throwable);
}
}
@ -153,12 +182,13 @@ final class Coroutine implements PromiseInterface
*/
public function _handleFailure($reason): void
{
unset($this->currentPromise);
$this->currentPromise = null;
try {
$nextYield = $this->generator->throw(Create::exceptionFor($reason));
// The throw was caught, so keep iterating on the coroutine
$this->nextCoroutine($nextYield);
} catch (Throwable $throwable) {
} catch (\Throwable $throwable) {
$this->result->reject($throwable);
}
}

View file

@ -6,10 +6,21 @@ namespace GuzzleHttp\Promise;
final class Create
{
private function __construct()
{
}
/**
* Creates a promise for a value if the value is not a promise.
* Returns `$value` when it is already a Guzzle promise, wraps foreign
* thenables in a Guzzle promise, or returns a fulfilled promise for plain
* values.
*
* @param mixed $value Promise or value.
* @template TValue
* @template TPromise of PromiseInterface<mixed, mixed> = PromiseInterface<mixed, mixed>
*
* @param TValue|TPromise $value Promise or value.
*
* @return ($value is PromiseInterface ? TPromise : FulfilledPromise<TValue, never>)
*/
public static function promiseFor($value): PromiseInterface
{
@ -31,10 +42,16 @@ final class Create
}
/**
* Creates a rejected promise for a reason if the reason is not a promise.
* If the provided reason is a promise, then it is returned as-is.
* Returns `$reason` when it is already a promise, or returns a rejected
* promise for plain reasons.
*
* @param mixed $reason Promise or reason.
* @template TReason
* @template TValue = never
* @template TPromise of PromiseInterface<mixed, mixed> = PromiseInterface<mixed, mixed>
*
* @param TReason|TPromise $reason Promise or reason.
*
* @return ($reason is PromiseInterface ? TPromise : RejectedPromise<TValue, TReason>)
*/
public static function rejectionFor($reason): PromiseInterface
{
@ -46,9 +63,12 @@ final class Create
}
/**
* Create an exception for a rejected promise value.
* Returns throwable reasons as-is, or wraps non-throwable reasons in
* `RejectionException`.
*
* @param mixed $reason
* @template TReason
*
* @param TReason $reason
*/
public static function exceptionFor($reason): \Throwable
{
@ -60,11 +80,17 @@ final class Create
}
/**
* Returns an iterator for the given value.
* Returns an iterator for arrays, iterators, iterator aggregates, and
* traversables.
*
* @param mixed $value
* @template TKey of array-key
* @template TValue
*
* @param iterable<TKey, TValue> $value
*
* @return \Iterator<TKey, TValue>
*/
public static function iterFor($value): \Iterator
public static function iterFor(iterable $value): \Iterator
{
if ($value instanceof \Iterator) {
return $value;
@ -74,16 +100,10 @@ final class Create
return new \ArrayIterator($value);
}
if (!is_iterable($value)) {
\trigger_deprecation(
'guzzlehttp/promises',
'2.5',
'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
__CLASS__,
__FUNCTION__
);
if ($value instanceof \IteratorAggregate) {
return self::iterFor($value->getIterator());
}
return new \ArrayIterator([$value]);
return new \IteratorIterator($value);
}
}

View file

@ -6,32 +6,58 @@ namespace GuzzleHttp\Promise;
final class Each
{
private function __construct()
{
}
/**
* Given an iterator that yields promises or values, returns a promise that
* is fulfilled with a null value when the iterator has been consumed or
* the aggregate promise has been fulfilled or rejected.
*
* $onFulfilled is a function that accepts the fulfilled value, iterator
* index, and the aggregate promise. The callback can invoke any necessary
* $onFulfilled is a function that accepts the fulfilled value, iterable
* key, and the aggregate promise. The callback can invoke any necessary
* side effects and choose to resolve or reject the aggregate if needed.
*
* $onRejected is a function that accepts the rejection reason, iterator
* index, and the aggregate promise. The callback can invoke any necessary
* $onRejected is a function that accepts the rejection reason, iterable
* key, and the aggregate promise. The callback can invoke any necessary
* side effects and choose to resolve or reject the aggregate if needed.
*
* @param mixed $iterable Iterator or array to iterate over.
* The config array accepts a concurrency option matching {@see ofLimit}.
* Other config keys are ignored by this wrapper.
*
* @template TKey of array-key
* @template TValue
* @template TReason
*
* @param iterable<TKey, TValue|PromiseInterface<TValue, TReason>> $iterable Iterator or array to iterate over.
* @param (callable(TValue, TKey, PromiseInterface<mixed, mixed>): mixed)|null $onFulfilled
* @param (callable(TReason, TKey, PromiseInterface<mixed, mixed>): mixed)|null $onRejected
* @param array{concurrency?: int|(callable(int): int)} $config Configuration options.
*
* @return PromiseInterface<mixed, mixed>
*/
public static function of(
$iterable,
iterable $iterable,
?callable $onFulfilled = null,
?callable $onRejected = null
?callable $onRejected = null,
array $config = []
): PromiseInterface {
$iterable = self::prepareIterable($iterable, __FUNCTION__);
$eachConfig = [];
return (new EachPromise($iterable, [
'fulfilled' => $onFulfilled,
'rejected' => $onRejected,
]))->promise();
if (null !== $onFulfilled) {
$eachConfig['fulfilled'] = $onFulfilled;
}
if (null !== $onRejected) {
$eachConfig['rejected'] = $onRejected;
}
if (isset($config['concurrency'])) {
$eachConfig['concurrency'] = $config['concurrency'];
}
return (new EachPromise($iterable, $eachConfig))->promise();
}
/**
@ -40,41 +66,46 @@ final class Each
*
* $concurrency may be an integer or a function that accepts the number of
* pending promises and returns a numeric concurrency limit value to allow
* for dynamic a concurrency size.
* for a dynamic concurrency size.
*
* @param mixed $iterable
* @param int|callable $concurrency
* @template TKey of array-key
* @template TValue
* @template TReason
*
* @param iterable<TKey, TValue|PromiseInterface<TValue, TReason>> $iterable
* @param int|(callable(int): int) $concurrency
* @param (callable(TValue, TKey, PromiseInterface<mixed, mixed>): mixed)|null $onFulfilled
* @param (callable(TReason, TKey, PromiseInterface<mixed, mixed>): mixed)|null $onRejected
*
* @return PromiseInterface<mixed, mixed>
*/
public static function ofLimit(
$iterable,
iterable $iterable,
$concurrency,
?callable $onFulfilled = null,
?callable $onRejected = null
): PromiseInterface {
$iterable = self::prepareIterable($iterable, __FUNCTION__);
return (new EachPromise($iterable, [
'fulfilled' => $onFulfilled,
'rejected' => $onRejected,
'concurrency' => $concurrency,
]))->promise();
return self::of($iterable, $onFulfilled, $onRejected, ['concurrency' => $concurrency]);
}
/**
* Like limit, but ensures that no promise in the given $iterable argument
* is rejected. If any promise is rejected, then the aggregate promise is
* rejected with the encountered rejection.
* Like ofLimit, but rejects the aggregate promise on the first rejection.
*
* @param mixed $iterable
* @param int|callable $concurrency
* @template TKey of array-key
* @template TValue
* @template TReason
*
* @param iterable<TKey, TValue|PromiseInterface<TValue, TReason>> $iterable
* @param int|(callable(int): int) $concurrency
* @param (callable(TValue, TKey, PromiseInterface<mixed, mixed>): mixed)|null $onFulfilled
*
* @return PromiseInterface<mixed, mixed>
*/
public static function ofLimitAll(
$iterable,
iterable $iterable,
$concurrency,
?callable $onFulfilled = null
): PromiseInterface {
$iterable = self::prepareIterable($iterable, __FUNCTION__);
return self::ofLimit(
$iterable,
$concurrency,
@ -84,21 +115,4 @@ final class Each
}
);
}
private static function prepareIterable($iterable, string $method): iterable
{
if (is_iterable($iterable)) {
return $iterable;
}
\trigger_deprecation(
'guzzlehttp/promises',
'2.5',
'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
self::class,
$method
);
return [$iterable];
}
}

View file

@ -8,31 +8,41 @@ namespace GuzzleHttp\Promise;
* Represents a promise that iterates over many promises and invokes
* side-effect functions in the process.
*
* @template TKey of array-key
* @template TValue
* @template TReason
*
* @implements PromisorInterface<mixed, mixed>
*
* @final
*/
class EachPromise implements PromisorInterface
{
private $pending = [];
use NonSerializableTrait;
private $nextPendingIndex = 0;
/** @var array<int, PromiseInterface<mixed, mixed>>|null */
private ?array $pending = [];
/** @var \Iterator|null */
private $iterable;
private int $nextPendingIndex = 0;
/** @var callable|int|null */
/** @var \Iterator<TKey, TValue|PromiseInterface<TValue, TReason>>|null */
private ?\Iterator $iterable;
/** @var (callable(int): int)|int|null */
private $concurrency;
/** @var callable|null */
/** @var (callable(TValue, TKey, PromiseInterface<mixed, mixed>): mixed)|null */
private $onFulfilled;
/** @var callable|null */
/** @var (callable(TReason, TKey, PromiseInterface<mixed, mixed>): mixed)|null */
private $onRejected;
/** @var Promise|null */
private $aggregate;
/** @var Promise<mixed, mixed>|null */
private ?Promise $aggregate = null;
/** @var bool|null */
private $mutex;
private ?bool $mutex = null;
private bool $stepWhileLocked = false;
/**
* Configuration hash can include the following key value pairs:
@ -52,23 +62,15 @@ class EachPromise implements PromisorInterface
* allowed number of outstanding concurrently executing promises,
* creating a capped pool of promises. There is no limit by default.
*
* @param mixed $iterable Promises or values to iterate.
* @param array $config Configuration options
* @param iterable<TKey, TValue|PromiseInterface<TValue, TReason>> $iterable Promises or values to iterate.
* @param array{
* fulfilled?: callable(TValue, TKey, PromiseInterface<mixed, mixed>): mixed,
* rejected?: callable(TReason, TKey, PromiseInterface<mixed, mixed>): mixed,
* concurrency?: int|(callable(int): int)
* } $config Configuration options
*/
public function __construct($iterable, array $config = [])
public function __construct(iterable $iterable, array $config = [])
{
if (!is_iterable($iterable)) {
\trigger_deprecation(
'guzzlehttp/promises',
'2.5',
'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
__CLASS__,
__FUNCTION__
);
$iterable = [$iterable];
}
$this->iterable = Create::iterFor($iterable);
if (isset($config['concurrency'])) {
@ -84,7 +86,9 @@ class EachPromise implements PromisorInterface
}
}
/** @psalm-suppress InvalidNullableReturnType */
/**
* @return PromiseInterface<mixed, mixed>
*/
public function promise(): PromiseInterface
{
if ($this->aggregate) {
@ -93,7 +97,6 @@ class EachPromise implements PromisorInterface
try {
$this->createPromise();
/** @psalm-assert Promise $this->aggregate */
$this->iterable->rewind();
$this->refillPending();
if (!$this->pending) {
@ -113,9 +116,6 @@ class EachPromise implements PromisorInterface
$this->aggregate->reject($e);
}
/**
* @psalm-suppress NullableReturnStatement
*/
return $this->aggregate;
}
@ -123,16 +123,23 @@ class EachPromise implements PromisorInterface
{
$this->mutex = false;
$this->aggregate = new Promise(function (): void {
if ($this->checkIfFinished()) {
return;
}
reset($this->pending);
// Consume a potentially fluctuating list of promises while
// ensuring that indexes are maintained (precluding array_shift).
while ($promise = current($this->pending)) {
next($this->pending);
$promise->wait();
if (Is::settled($this->aggregate)) {
while (true) {
if ($this->checkIfFinished()) {
return;
}
reset($this->pending);
// Consume a potentially fluctuating list of promises while
// ensuring that indexes are maintained (precluding array_shift).
while ($promise = current($this->pending)) {
next($this->pending);
$promise->wait();
if (Is::settled($this->aggregate)) {
return;
}
}
// Refill and re-sweep; give up only when nothing remains.
$this->refillPending();
if (Is::settled($this->aggregate) || !$this->pending) {
return;
}
}
@ -162,6 +169,10 @@ class EachPromise implements PromisorInterface
$concurrency = is_callable($this->concurrency)
? ($this->concurrency)(count($this->pending))
: $this->concurrency;
// The callable can settle the aggregate; admit nothing more.
if (Is::settled($this->aggregate)) {
return;
}
$concurrency = max($concurrency - count($this->pending), 0);
// Concurrency may be set to 0 to disallow new promises.
if (!$concurrency) {
@ -223,6 +234,8 @@ class EachPromise implements PromisorInterface
// Place a lock on the iterator so that we ensure to not recurse,
// preventing fatal generator errors.
if ($this->mutex) {
$this->stepWhileLocked = true;
return false;
}
@ -231,14 +244,22 @@ class EachPromise implements PromisorInterface
try {
$this->iterable->next();
$this->mutex = false;
return true;
} catch (\Throwable $e) {
$this->aggregate->reject($e);
$this->mutex = false;
return false;
}
// Run the completion check that locked steps skipped.
if ($this->stepWhileLocked) {
$this->stepWhileLocked = false;
if (!Is::settled($this->aggregate)) {
$this->checkIfFinished();
}
}
return true;
}
private function step(int $idx): void
@ -259,6 +280,7 @@ class EachPromise implements PromisorInterface
}
}
/** @phpstan-impure */
private function checkIfFinished(): bool
{
if (!$this->pending && !$this->iterable->valid()) {

View file

@ -10,14 +10,20 @@ namespace GuzzleHttp\Promise;
* Thenning off of this promise will invoke the onFulfilled callback
* immediately and ignore other callbacks.
*
* @template TValue = mixed
* @template TReason = never
*
* @implements PromiseInterface<TValue, TReason>
*
* @final
*/
class FulfilledPromise implements PromiseInterface
{
/** @var TValue */
private $value;
/**
* @param mixed $value
* @param TValue $value
*/
public function __construct($value)
{
@ -30,6 +36,17 @@ class FulfilledPromise implements PromiseInterface
$this->value = $value;
}
/**
* @template TFulfilledValue = never
* @template TFulfilledReason = never
* @template TRejectedValue = never
* @template TRejectedReason = never
*
* @param (callable(TValue): (TFulfilledValue|PromiseInterface<TFulfilledValue, TFulfilledReason>))|null $onFulfilled Invoked when the promise fulfills.
* @param (callable(TReason): (TRejectedValue|PromiseInterface<TRejectedValue, TRejectedReason>))|null $onRejected Invoked when the promise is rejected.
*
* @return ($onFulfilled is null ? self<TValue, TReason> : PromiseInterface<TFulfilledValue, TFulfilledReason|\Throwable>)
*/
public function then(
?callable $onFulfilled = null,
?callable $onRejected = null
@ -55,6 +72,11 @@ class FulfilledPromise implements PromiseInterface
return $p;
}
/**
* @param callable(TReason): mixed $onRejected Invoked when the promise is rejected.
*
* @return self<TValue, TReason>
*/
public function otherwise(callable $onRejected): PromiseInterface
{
return $this->then(null, $onRejected);
@ -70,7 +92,7 @@ class FulfilledPromise implements PromiseInterface
return self::FULFILLED;
}
public function resolve($value): void
public function resolve($value = null): void
{
if ($value !== $this->value) {
throw new \LogicException('Cannot resolve a fulfilled promise');

View file

@ -6,6 +6,10 @@ namespace GuzzleHttp\Promise;
final class Is
{
private function __construct()
{
}
/**
* Returns true if a promise is pending.
*/

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace GuzzleHttp\Promise;
/**
* @internal
*/
trait NonSerializableTrait
{
public function __serialize(): array
{
throw new \LogicException(static::class.' should never be serialized');
}
public function __unserialize(array $data): void
{
throw new \LogicException(static::class.' should never be unserialized');
}
}

View file

@ -7,22 +7,40 @@ namespace GuzzleHttp\Promise;
/**
* Promises/A+ implementation that avoids recursion when possible.
*
* @template TValue = mixed
* @template TReason = mixed
*
* @implements PromiseInterface<TValue, TReason>
*
* @see https://promisesaplus.com/
*
* @final
*/
class Promise implements PromiseInterface
{
private $state = self::PENDING;
use NonSerializableTrait;
/** @var self::PENDING|self::FULFILLED|self::REJECTED */
private string $state = self::PENDING;
/** @var TValue|TReason|PromiseInterface<TValue, TReason>|null */
private $result;
/** @var (callable(): void)|null */
private $cancelFn;
/** @var (callable(bool): void)|null */
private $waitFn;
private $waitList;
private $handlers = [];
/** @var list<Promise<mixed, mixed>>|null */
private ?array $waitList = null;
/** @var list<array{0: PromiseInterface<mixed, mixed>, 1: callable|null, 2: callable|null}>|null */
private ?array $handlers = [];
/**
* @param callable $waitFn Fn that when invoked resolves the promise.
* @param callable $cancelFn Fn that when invoked cancels the promise.
* @param (callable(bool): void)|null $waitFn Fn that when invoked resolves the promise.
* @param (callable(): void)|null $cancelFn Fn that when invoked cancels the promise.
*/
public function __construct(
?callable $waitFn = null,
@ -32,6 +50,17 @@ class Promise implements PromiseInterface
$this->cancelFn = $cancelFn;
}
/**
* @template TFulfilledValue = never
* @template TFulfilledReason = never
* @template TRejectedValue = never
* @template TRejectedReason = never
*
* @param (callable(TValue): (TFulfilledValue|PromiseInterface<TFulfilledValue, TFulfilledReason>))|null $onFulfilled Invoked when the promise fulfills.
* @param (callable(TReason): (TRejectedValue|PromiseInterface<TRejectedValue, TRejectedReason>))|null $onRejected Invoked when the promise is rejected.
*
* @return PromiseInterface<($onFulfilled is null ? TValue : TFulfilledValue)|($onRejected is null ? never : TRejectedValue), ($onFulfilled is null ? never : TFulfilledReason|\Throwable)|($onRejected is null ? TReason : TRejectedReason|\Throwable)>
*/
public function then(
?callable $onFulfilled = null,
?callable $onRejected = null
@ -49,16 +78,27 @@ class Promise implements PromiseInterface
if ($this->state === self::FULFILLED) {
$promise = Create::promiseFor($this->result);
return $onFulfilled ? $promise->then($onFulfilled) : $promise;
return $promise->then($onFulfilled, $onRejected);
}
// It's either cancelled or rejected, so return a rejected promise
// and immediately invoke any callbacks.
$rejection = Create::rejectionFor($this->result);
return $onRejected ? $rejection->then(null, $onRejected) : $rejection;
/** @var PromiseInterface<($onFulfilled is null ? TValue : TFulfilledValue)|($onRejected is null ? never : TRejectedValue), ($onFulfilled is null ? never : TFulfilledReason|\Throwable)|($onRejected is null ? TReason : TRejectedReason|\Throwable)> $promise */
$promise = $onRejected ? $rejection->then(null, $onRejected) : $rejection;
return $promise;
}
/**
* @template TRejectedValue = never
* @template TRejectedReason = never
*
* @param callable(TReason): (TRejectedValue|PromiseInterface<TRejectedValue, TRejectedReason>) $onRejected Invoked when the promise is rejected.
*
* @return PromiseInterface<TValue|TRejectedValue, TRejectedReason|\Throwable>
*/
public function otherwise(callable $onRejected): PromiseInterface
{
return $this->then(null, $onRejected);
@ -78,6 +118,8 @@ class Promise implements PromiseInterface
// It's rejected so "unwrap" and throw an exception.
throw Create::exceptionFor($this->result);
}
return null;
}
public function getState(): string
@ -110,7 +152,7 @@ class Promise implements PromiseInterface
}
}
public function resolve($value): void
public function resolve($value = null): void
{
$this->settle(self::FULFILLED, $value);
}
@ -187,7 +229,7 @@ class Promise implements PromiseInterface
*/
private static function callHandler(int $index, $value, array $handler): void
{
/** @var PromiseInterface $promise */
/** @var PromiseInterface<mixed, mixed> $promise */
$promise = $handler[0];
// The promise may have been cancelled or resolved before placing

View file

@ -11,6 +11,9 @@ namespace GuzzleHttp\Promise;
* which registers callbacks to receive either a promises eventual value or
* the reason why the promise cannot be fulfilled.
*
* @template TValue = mixed
* @template TReason = mixed
*
* @see https://promisesaplus.com/
*/
interface PromiseInterface
@ -23,8 +26,15 @@ interface PromiseInterface
* Appends fulfillment and rejection handlers to the promise, and returns
* a new promise resolving to the return value of the called handler.
*
* @param callable $onFulfilled Invoked when the promise fulfills.
* @param callable $onRejected Invoked when the promise is rejected.
* @template TFulfilledValue = never
* @template TFulfilledReason = never
* @template TRejectedValue = never
* @template TRejectedReason = never
*
* @param (callable(TValue): (TFulfilledValue|PromiseInterface<TFulfilledValue, TFulfilledReason>))|null $onFulfilled Invoked when the promise fulfills.
* @param (callable(TReason): (TRejectedValue|PromiseInterface<TRejectedValue, TRejectedReason>))|null $onRejected Invoked when the promise is rejected.
*
* @return PromiseInterface<($onFulfilled is null ? TValue : TFulfilledValue)|($onRejected is null ? never : TRejectedValue), ($onFulfilled is null ? never : TFulfilledReason|\Throwable)|($onRejected is null ? TReason : TRejectedReason|\Throwable)>
*/
public function then(
?callable $onFulfilled = null,
@ -37,7 +47,12 @@ interface PromiseInterface
* or to its original fulfillment value if the promise is instead
* fulfilled.
*
* @param callable $onRejected Invoked when the promise is rejected.
* @template TRejectedValue = never
* @template TRejectedReason = never
*
* @param callable(TReason): (TRejectedValue|PromiseInterface<TRejectedValue, TRejectedReason>) $onRejected Invoked when the promise is rejected.
*
* @return PromiseInterface<TValue|TRejectedValue, TRejectedReason|\Throwable>
*/
public function otherwise(callable $onRejected): PromiseInterface;
@ -46,22 +61,24 @@ interface PromiseInterface
*
* The three states can be checked against the constants defined on
* PromiseInterface: PENDING, FULFILLED, and REJECTED.
*
* @return self::PENDING|self::FULFILLED|self::REJECTED
*/
public function getState(): string;
/**
* Resolve the promise with the given value.
* Resolve the promise with the given value, or with null if no value is given.
*
* @param mixed $value
* @param TValue|PromiseInterface<TValue, TReason>|null $value
*
* @throws \RuntimeException if the promise is already resolved.
*/
public function resolve($value): void;
public function resolve($value = null): void;
/**
* Reject the promise with the given reason.
*
* @param mixed $reason
* @param TReason $reason
*
* @throws \RuntimeException if the promise is already resolved.
*/
@ -82,7 +99,7 @@ interface PromiseInterface
*
* If the promise cannot be waited on, then the promise will be rejected.
*
* @return mixed
* @return ($unwrap is true ? TValue : null)
*
* @throws \LogicException if the promise has no wait function or if the
* promise does not settle after waiting.

View file

@ -6,11 +6,16 @@ namespace GuzzleHttp\Promise;
/**
* Interface used with classes that return a promise.
*
* @template TValue = mixed
* @template TReason = mixed
*/
interface PromisorInterface
{
/**
* Returns a promise.
*
* @return PromiseInterface<TValue, TReason>
*/
public function promise(): PromiseInterface;
}

View file

@ -10,14 +10,20 @@ namespace GuzzleHttp\Promise;
* Thenning off of this promise will invoke the onRejected callback
* immediately and ignore other callbacks.
*
* @template TValue = never
* @template TReason = mixed
*
* @implements PromiseInterface<TValue, TReason>
*
* @final
*/
class RejectedPromise implements PromiseInterface
{
/** @var TReason */
private $reason;
/**
* @param mixed $reason
* @param TReason $reason
*/
public function __construct($reason)
{
@ -30,6 +36,17 @@ class RejectedPromise implements PromiseInterface
$this->reason = $reason;
}
/**
* @template TFulfilledValue = never
* @template TFulfilledReason = never
* @template TRejectedValue = never
* @template TRejectedReason = never
*
* @param (callable(TValue): (TFulfilledValue|PromiseInterface<TFulfilledValue, TFulfilledReason>))|null $onFulfilled Invoked when the promise fulfills.
* @param (callable(TReason): (TRejectedValue|PromiseInterface<TRejectedValue, TRejectedReason>))|null $onRejected Invoked when the promise is rejected.
*
* @return ($onRejected is null ? self<TValue, TReason> : PromiseInterface<TRejectedValue, TRejectedReason|\Throwable>)
*/
public function then(
?callable $onFulfilled = null,
?callable $onRejected = null
@ -57,6 +74,14 @@ class RejectedPromise implements PromiseInterface
return $p;
}
/**
* @template TRejectedValue = never
* @template TRejectedReason = never
*
* @param callable(TReason): (TRejectedValue|PromiseInterface<TRejectedValue, TRejectedReason>) $onRejected Invoked when the promise is rejected.
*
* @return PromiseInterface<TRejectedValue, TRejectedReason|\Throwable>
*/
public function otherwise(callable $onRejected): PromiseInterface
{
return $this->then(null, $onRejected);
@ -76,7 +101,7 @@ class RejectedPromise implements PromiseInterface
return self::REJECTED;
}
public function resolve($value): void
public function resolve($value = null): void
{
throw new \LogicException('Cannot resolve a rejected promise');
}

View file

@ -17,8 +17,11 @@ namespace GuzzleHttp\Promise;
*/
class TaskQueue implements TaskQueueInterface
{
private $enableShutdown = true;
private $queue = [];
use NonSerializableTrait;
private bool $enableShutdown = true;
/** @var list<callable(): void> */
private array $queue = [];
public function __construct(bool $withShutdown = true)
{
@ -40,6 +43,9 @@ class TaskQueue implements TaskQueueInterface
return !$this->queue;
}
/**
* @param callable(): void $task
*/
public function add(callable $task): void
{
$this->queue[] = $task;
@ -48,7 +54,7 @@ class TaskQueue implements TaskQueueInterface
public function run(): void
{
while ($task = array_shift($this->queue)) {
/** @var callable $task */
/** @var callable(): void $task */
$task();
}
}

View file

@ -14,6 +14,8 @@ interface TaskQueueInterface
/**
* Adds a task to the queue that will be executed the next time run is
* called.
*
* @param callable(): void $task
*/
public function add(callable $task): void;

View file

@ -6,6 +6,10 @@ namespace GuzzleHttp\Promise;
final class Utils
{
private function __construct()
{
}
/**
* Get the global task queue used for promise resolution.
*
@ -35,10 +39,14 @@ final class Utils
}
/**
* Adds a function to run in the task queue when it is next `run()` and
* returns a promise that is fulfilled or rejected with the result.
* Adds a task to the global queue and returns a promise that is fulfilled
* or rejected with the task result.
*
* @param callable $task Task function to run.
* @template TValue
*
* @param callable(): TValue $task Task function to run.
*
* @return PromiseInterface<TValue, \Throwable>
*/
public static function task(callable $task): PromiseInterface
{
@ -58,7 +66,7 @@ final class Utils
}
/**
* Synchronously waits on a promise to resolve and returns an inspection
* Synchronously waits on a promise to settle and returns an inspection
* state array.
*
* Returns a state associative array containing a "state" key mapping to a
@ -67,42 +75,75 @@ final class Utils
* promise. If the promise is rejected, the array will contain a "reason"
* key mapping to the rejection reason of the promise.
*
* @param PromiseInterface $promise Promise or value.
* @template TValue
* @template TReason
*
* @param PromiseInterface<TValue, TReason> $promise Promise to inspect.
*
* @return array{state: PromiseInterface::FULFILLED, value: TValue}|array{state: PromiseInterface::REJECTED, reason: TReason|\Throwable}|array{state: PromiseInterface::PENDING}
*/
public static function inspect(PromiseInterface $promise): array
{
$result = null;
$getResult = static function () use (&$result): ?array {
return $result;
};
$inspection = $promise->then(
static function ($value) use (&$result): void {
$result = ['state' => PromiseInterface::FULFILLED, 'value' => $value];
},
static function ($reason) use (&$result): void {
$result = ['state' => PromiseInterface::REJECTED, 'reason' => $reason];
}
);
try {
return [
'state' => PromiseInterface::FULFILLED,
'value' => $promise->wait(),
];
$inspection->wait(false);
} catch (\Throwable $e) {
if ($e instanceof AggregateException) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
$settled = $getResult();
if (null !== $settled) {
return $settled;
}
if ($e instanceof RejectionException) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()];
if (Is::settled($promise)) {
try {
self::queue()->run();
} catch (\Throwable $queueError) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $queueError];
}
$settled = $getResult();
if (null !== $settled) {
return $settled;
}
}
return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
}
return $getResult() ?? ['state' => $promise->getState()];
}
/**
* Waits on all of the provided promises, but does not unwrap rejected
* promises as thrown exception.
* promises as a thrown exception.
*
* Returns an array of inspection state arrays.
* Returns an array of inspection state arrays keyed like the input
* iterable.
*
* @see inspect for the inspection state array format.
*
* @param PromiseInterface[] $promises Traversable of promises to wait upon.
* @template TKey of array-key
* @template TValue
* @template TReason
*
* @param iterable<TKey, PromiseInterface<TValue, TReason>> $promises Traversable of promises to wait upon.
*
* @return array<TKey, array{state: PromiseInterface::FULFILLED, value: TValue}|array{state: PromiseInterface::REJECTED, reason: TReason|\Throwable}|array{state: PromiseInterface::PENDING}>
*/
public static function inspectAll($promises): array
public static function inspectAll(iterable $promises): array
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
foreach ($promises as $key => $promise) {
$results[$key] = self::inspect($promise);
@ -118,14 +159,18 @@ final class Utils
* order the promises were provided). An exception is thrown if any of the
* promises are rejected.
*
* @param iterable<PromiseInterface> $promises Iterable of PromiseInterface objects to wait on.
* @template TKey of array-key
* @template TValue
* @template TReason
*
* @param iterable<TKey, PromiseInterface<TValue, TReason>> $promises Iterable of PromiseInterface objects to wait on.
*
* @return array<TKey, TValue>
*
* @throws \Throwable on error
*/
public static function unwrap($promises): array
public static function unwrap(iterable $promises): array
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
foreach ($promises as $key => $promise) {
$results[$key] = $promise->wait();
@ -142,24 +187,33 @@ final class Utils
* respective positions to the original array. If any promise in the array
* rejects, the returned promise is rejected with the rejection reason.
*
* @param mixed $promises Promises or values.
* @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution.
* The config array accepts a concurrency option for lazy iterables. Other
* config keys are ignored by this wrapper.
*
* @template TKey of array-key
* @template TValue
* @template TReason
*
* @param iterable<TKey, TValue|PromiseInterface<TValue, TReason>> $promises Promises or values.
* @param bool $recursive If true, resolves newly-added entries until no unprocessed entries or pending promises remain.
* @param array{concurrency?: int|(callable(int): int)} $config Configuration options.
*
* @return PromiseInterface<array<TKey, TValue>, TReason|\Throwable>
*/
public static function all($promises, bool $recursive = false): PromiseInterface
public static function all(iterable $promises, bool $recursive = false, array $config = []): PromiseInterface
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
$promise = Each::of(
$promises,
function ($value, $idx) use (&$results): void {
$results[$idx] = $value;
},
function ($reason, $idx, Promise $aggregate): void {
function ($reason, $idx, PromiseInterface $aggregate): void {
if (Is::pending($aggregate)) {
$aggregate->reject($reason);
}
}
},
$config
)->then(function () use (&$results) {
ksort($results);
@ -167,11 +221,9 @@ final class Utils
});
if (true === $recursive) {
$promise = $promise->then(function ($results) use ($recursive, &$promises) {
foreach ($promises as $promise) {
if (Is::pending($promise)) {
return self::all($promises, $recursive);
}
$promise = $promise->then(function ($results) use (&$promises, $config) {
if (self::shouldRecurse($promises, $results)) {
return self::all($promises, true, $config);
}
return $results;
@ -187,22 +239,25 @@ final class Utils
*
* When count amount of promises have been fulfilled, the returned promise
* is fulfilled with an array that contains the fulfillment values of the
* winners in order of resolution.
* winners, in the order they appear in the input.
*
* This promise is rejected with a {@see AggregateException} if the number
* of fulfilled promises is less than the desired $count.
*
* @param int $count Total number of promises.
* @param mixed $promises Promises or values.
* @template TValue
* @template TReason
*
* @param int $count Total number of promises.
* @param iterable<TValue|PromiseInterface<TValue, TReason>> $promises Promises or values.
*
* @return PromiseInterface<list<TValue>, \Throwable>
*/
public static function some(int $count, $promises): PromiseInterface
public static function some(int $count, iterable $promises): PromiseInterface
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
$rejections = [];
return Each::of(
$promise = Each::of(
$promises,
function ($value, $idx, PromiseInterface $p) use (&$results, $count): void {
if (Is::settled($p)) {
@ -229,77 +284,107 @@ final class Utils
return array_values($results);
}
);
/** @var PromiseInterface<list<TValue>, \Throwable> $promise */
return $promise;
}
/**
* Like some(), with 1 as count. However, if the promise fulfills, the
* fulfillment value is not an array of 1 but the value directly.
*
* @param mixed $promises Promises or values.
* @template TValue
* @template TReason
*
* @param iterable<TValue|PromiseInterface<TValue, TReason>> $promises Promises or values.
*
* @return PromiseInterface<TValue, \Throwable>
*/
public static function any($promises): PromiseInterface
public static function any(iterable $promises): PromiseInterface
{
$promises = self::prepareIterable($promises, __FUNCTION__);
return self::some(1, $promises)->then(function ($values) {
return self::some(1, $promises)->then(function (array $values) {
return $values[0];
});
}
/**
* Returns a promise that is fulfilled when all of the provided promises have
* been fulfilled or rejected.
* Returns a promise that is fulfilled when all of the provided promises
* have been fulfilled or rejected.
*
* The returned promise is fulfilled with an array of inspection state arrays.
* The returned promise is fulfilled with an array of inspection state
* arrays.
*
* The config array accepts a concurrency option for lazy iterables. Other
* config keys are ignored by this wrapper.
*
* @see inspect for the inspection state array format.
*
* @param mixed $promises Promises or values.
* @template TKey of array-key
* @template TValue
* @template TReason
*
* @param iterable<TKey, TValue|PromiseInterface<TValue, TReason>> $promises Promises or values.
* @param bool $recursive If true, settles newly-added entries until no unprocessed entries or pending promises remain.
* @param array{concurrency?: int|(callable(int): int)} $config Configuration options.
*
* @return PromiseInterface<array<TKey, array{state: PromiseInterface::FULFILLED, value: TValue}|array{state: PromiseInterface::REJECTED, reason: TReason|\Throwable}>, \Throwable>
*/
public static function settle($promises): PromiseInterface
public static function settle(iterable $promises, bool $recursive = false, array $config = []): PromiseInterface
{
$promises = self::prepareIterable($promises, __FUNCTION__);
$results = [];
return Each::of(
$promise = Each::of(
$promises,
function ($value, $idx) use (&$results): void {
$results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value];
},
function ($reason, $idx) use (&$results): void {
$results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason];
}
},
$config
)->then(function () use (&$results) {
ksort($results);
return $results;
});
}
private static function prepareIterable($promises, string $method): iterable
{
if (is_iterable($promises)) {
return $promises;
if (true === $recursive) {
$promise = $promise->then(function ($results) use (&$promises, $config) {
if (self::shouldRecurse($promises, $results)) {
return self::settle($promises, true, $config);
}
return $results;
});
}
self::triggerNonIterableDeprecation($promises, $method);
return [$promises];
return $promise;
}
private static function triggerNonIterableDeprecation($promises, string $method): void
/**
* @template TKey of array-key
*
* @param iterable<TKey, mixed> $promises Promises or values.
* @param array<TKey, mixed> $results Results already collected for a pass.
*/
private static function shouldRecurse(iterable $promises, array $results): bool
{
if (is_iterable($promises)) {
return;
// A consumed generator cannot be traversed again, so a recursive
// pass has nothing further to observe.
if ($promises instanceof \Generator) {
return false;
}
\trigger_deprecation(
'guzzlehttp/promises',
'2.5',
'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
self::class,
$method
);
foreach ($promises as $key => $promise) {
if (!array_key_exists($key, $results)) {
return true;
}
if ($promise instanceof PromiseInterface && Is::pending($promise)) {
return true;
}
}
return false;
}
}