Chris@0: # Guzzle Promises Chris@0: Chris@0: [Promises/A+](https://promisesaplus.com/) implementation that handles promise Chris@0: chaining and resolution iteratively, allowing for "infinite" promise chaining Chris@0: while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/) Chris@0: for a general introduction to promises. Chris@0: Chris@0: - [Features](#features) Chris@0: - [Quick start](#quick-start) Chris@0: - [Synchronous wait](#synchronous-wait) Chris@0: - [Cancellation](#cancellation) Chris@0: - [API](#api) Chris@0: - [Promise](#promise) Chris@0: - [FulfilledPromise](#fulfilledpromise) Chris@0: - [RejectedPromise](#rejectedpromise) Chris@0: - [Promise interop](#promise-interop) Chris@0: - [Implementation notes](#implementation-notes) Chris@0: Chris@0: Chris@0: # Features Chris@0: Chris@0: - [Promises/A+](https://promisesaplus.com/) implementation. Chris@0: - Promise resolution and chaining is handled iteratively, allowing for Chris@0: "infinite" promise chaining. Chris@0: - Promises have a synchronous `wait` method. Chris@0: - Promises can be cancelled. Chris@0: - Works with any object that has a `then` function. Chris@0: - C# style async/await coroutine promises using Chris@0: `GuzzleHttp\Promise\coroutine()`. Chris@0: Chris@0: Chris@0: # Quick start Chris@0: Chris@0: A *promise* represents the eventual result of an asynchronous operation. The Chris@0: primary way of interacting with a promise is through its `then` method, which Chris@0: registers callbacks to receive either a promise's eventual value or the reason Chris@0: why the promise cannot be fulfilled. Chris@0: Chris@0: Chris@0: ## Callbacks Chris@0: Chris@0: Callbacks are registered with the `then` method by providing an optional Chris@0: `$onFulfilled` followed by an optional `$onRejected` function. Chris@0: Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\Promise; Chris@0: Chris@0: $promise = new Promise(); Chris@0: $promise->then( Chris@0: // $onFulfilled Chris@0: function ($value) { Chris@0: echo 'The promise was fulfilled.'; Chris@0: }, Chris@0: // $onRejected Chris@0: function ($reason) { Chris@0: echo 'The promise was rejected.'; Chris@0: } Chris@0: ); Chris@0: ``` Chris@0: Chris@0: *Resolving* a promise means that you either fulfill a promise with a *value* or Chris@0: reject a promise with a *reason*. Resolving a promises triggers callbacks Chris@0: registered with the promises's `then` method. These callbacks are triggered Chris@0: only once and in the order in which they were added. Chris@0: Chris@0: Chris@0: ## Resolving a promise Chris@0: Chris@0: Promises are fulfilled using the `resolve($value)` method. Resolving a promise Chris@0: with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger Chris@0: all of the onFulfilled callbacks (resolving a promise with a rejected promise Chris@0: will reject the promise and trigger the `$onRejected` callbacks). Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\Promise; Chris@0: Chris@0: $promise = new Promise(); Chris@0: $promise Chris@0: ->then(function ($value) { Chris@0: // Return a value and don't break the chain Chris@0: return "Hello, " . $value; Chris@0: }) Chris@0: // This then is executed after the first then and receives the value Chris@0: // returned from the first then. Chris@0: ->then(function ($value) { Chris@0: echo $value; Chris@0: }); Chris@0: Chris@0: // Resolving the promise triggers the $onFulfilled callbacks and outputs Chris@0: // "Hello, reader". Chris@0: $promise->resolve('reader.'); Chris@0: ``` Chris@0: Chris@0: Chris@0: ## Promise forwarding Chris@0: Chris@0: Promises can be chained one after the other. Each then in the chain is a new Chris@0: promise. The return value of a promise is what's forwarded to the next Chris@0: promise in the chain. Returning a promise in a `then` callback will cause the Chris@0: subsequent promises in the chain to only be fulfilled when the returned promise Chris@0: has been fulfilled. The next promise in the chain will be invoked with the Chris@0: resolved value of the promise. Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\Promise; Chris@0: Chris@0: $promise = new Promise(); Chris@0: $nextPromise = new Promise(); Chris@0: Chris@0: $promise Chris@0: ->then(function ($value) use ($nextPromise) { Chris@0: echo $value; Chris@0: return $nextPromise; Chris@0: }) Chris@0: ->then(function ($value) { Chris@0: echo $value; Chris@0: }); Chris@0: Chris@0: // Triggers the first callback and outputs "A" Chris@0: $promise->resolve('A'); Chris@0: // Triggers the second callback and outputs "B" Chris@0: $nextPromise->resolve('B'); Chris@0: ``` Chris@0: Chris@0: ## Promise rejection Chris@0: Chris@0: When a promise is rejected, the `$onRejected` callbacks are invoked with the Chris@0: rejection reason. Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\Promise; Chris@0: Chris@0: $promise = new Promise(); Chris@0: $promise->then(null, function ($reason) { Chris@0: echo $reason; Chris@0: }); Chris@0: Chris@0: $promise->reject('Error!'); Chris@0: // Outputs "Error!" Chris@0: ``` Chris@0: Chris@0: ## Rejection forwarding Chris@0: Chris@0: If an exception is thrown in an `$onRejected` callback, subsequent Chris@0: `$onRejected` callbacks are invoked with the thrown exception as the reason. Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\Promise; Chris@0: Chris@0: $promise = new Promise(); Chris@0: $promise->then(null, function ($reason) { Chris@0: throw new \Exception($reason); Chris@0: })->then(null, function ($reason) { Chris@0: assert($reason->getMessage() === 'Error!'); Chris@0: }); Chris@0: Chris@0: $promise->reject('Error!'); Chris@0: ``` Chris@0: Chris@0: You can also forward a rejection down the promise chain by returning a Chris@0: `GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or Chris@0: `$onRejected` callback. Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\Promise; Chris@0: use GuzzleHttp\Promise\RejectedPromise; Chris@0: Chris@0: $promise = new Promise(); Chris@0: $promise->then(null, function ($reason) { Chris@0: return new RejectedPromise($reason); Chris@0: })->then(null, function ($reason) { Chris@0: assert($reason === 'Error!'); Chris@0: }); Chris@0: Chris@0: $promise->reject('Error!'); Chris@0: ``` Chris@0: Chris@0: If an exception is not thrown in a `$onRejected` callback and the callback Chris@0: does not return a rejected promise, downstream `$onFulfilled` callbacks are Chris@0: invoked using the value returned from the `$onRejected` callback. Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\Promise; Chris@0: use GuzzleHttp\Promise\RejectedPromise; Chris@0: Chris@0: $promise = new Promise(); Chris@0: $promise Chris@0: ->then(null, function ($reason) { Chris@0: return "It's ok"; Chris@0: }) Chris@0: ->then(function ($value) { Chris@0: assert($value === "It's ok"); Chris@0: }); Chris@0: Chris@0: $promise->reject('Error!'); Chris@0: ``` Chris@0: Chris@0: # Synchronous wait Chris@0: Chris@0: You can synchronously force promises to complete using a promise's `wait` Chris@0: method. When creating a promise, you can provide a wait function that is used Chris@0: to synchronously force a promise to complete. When a wait function is invoked Chris@0: it is expected to deliver a value to the promise or reject the promise. If the Chris@0: wait function does not deliver a value, then an exception is thrown. The wait Chris@0: function provided to a promise constructor is invoked when the `wait` function Chris@0: of the promise is called. Chris@0: Chris@0: ```php Chris@0: $promise = new Promise(function () use (&$promise) { Chris@0: $promise->resolve('foo'); Chris@0: }); Chris@0: Chris@0: // Calling wait will return the value of the promise. Chris@0: echo $promise->wait(); // outputs "foo" Chris@0: ``` Chris@0: Chris@0: If an exception is encountered while invoking the wait function of a promise, Chris@0: the promise is rejected with the exception and the exception is thrown. Chris@0: Chris@0: ```php Chris@0: $promise = new Promise(function () use (&$promise) { Chris@0: throw new \Exception('foo'); Chris@0: }); Chris@0: Chris@0: $promise->wait(); // throws the exception. Chris@0: ``` Chris@0: Chris@0: Calling `wait` on a promise that has been fulfilled will not trigger the wait Chris@0: function. It will simply return the previously resolved value. Chris@0: Chris@0: ```php Chris@0: $promise = new Promise(function () { die('this is not called!'); }); Chris@0: $promise->resolve('foo'); Chris@0: echo $promise->wait(); // outputs "foo" Chris@0: ``` Chris@0: Chris@0: Calling `wait` on a promise that has been rejected will throw an exception. If Chris@0: the rejection reason is an instance of `\Exception` the reason is thrown. Chris@0: Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason Chris@0: can be obtained by calling the `getReason` method of the exception. Chris@0: Chris@0: ```php Chris@0: $promise = new Promise(); Chris@0: $promise->reject('foo'); Chris@0: $promise->wait(); Chris@0: ``` Chris@0: Chris@0: > PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo' Chris@0: Chris@0: Chris@0: ## Unwrapping a promise Chris@0: Chris@0: When synchronously waiting on a promise, you are joining the state of the Chris@0: promise into the current state of execution (i.e., return the value of the Chris@0: promise if it was fulfilled or throw an exception if it was rejected). This is Chris@0: called "unwrapping" the promise. Waiting on a promise will by default unwrap Chris@0: the promise state. Chris@0: Chris@0: You can force a promise to resolve and *not* unwrap the state of the promise Chris@0: by passing `false` to the first argument of the `wait` function: Chris@0: Chris@0: ```php Chris@0: $promise = new Promise(); Chris@0: $promise->reject('foo'); Chris@0: // This will not throw an exception. It simply ensures the promise has Chris@0: // been resolved. Chris@0: $promise->wait(false); Chris@0: ``` Chris@0: Chris@0: When unwrapping a promise, the resolved value of the promise will be waited Chris@0: upon until the unwrapped value is not a promise. This means that if you resolve Chris@0: promise A with a promise B and unwrap promise A, the value returned by the Chris@0: wait function will be the value delivered to promise B. Chris@0: Chris@0: **Note**: when you do not unwrap the promise, no value is returned. Chris@0: Chris@0: Chris@0: # Cancellation Chris@0: Chris@0: You can cancel a promise that has not yet been fulfilled using the `cancel()` Chris@0: method of a promise. When creating a promise you can provide an optional Chris@0: cancel function that when invoked cancels the action of computing a resolution Chris@0: of the promise. Chris@0: Chris@0: Chris@0: # API Chris@0: Chris@0: Chris@0: ## Promise Chris@0: Chris@0: When creating a promise object, you can provide an optional `$waitFn` and Chris@0: `$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is Chris@0: expected to resolve the promise. `$cancelFn` is a function with no arguments Chris@0: that is expected to cancel the computation of a promise. It is invoked when the Chris@0: `cancel()` method of a promise is called. Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\Promise; Chris@0: Chris@0: $promise = new Promise( Chris@0: function () use (&$promise) { Chris@0: $promise->resolve('waited'); Chris@0: }, Chris@0: function () { Chris@0: // do something that will cancel the promise computation (e.g., close Chris@0: // a socket, cancel a database query, etc...) Chris@0: } Chris@0: ); Chris@0: Chris@0: assert('waited' === $promise->wait()); Chris@0: ``` Chris@0: Chris@0: A promise has the following methods: Chris@0: Chris@0: - `then(callable $onFulfilled, callable $onRejected) : PromiseInterface` Chris@0: Chris@0: Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler. Chris@0: Chris@0: - `otherwise(callable $onRejected) : PromiseInterface` Chris@0: Chris@0: 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. Chris@0: Chris@0: - `wait($unwrap = true) : mixed` Chris@0: Chris@0: Synchronously waits on the promise to complete. Chris@0: Chris@0: `$unwrap` controls whether or not the value of the promise is returned for a Chris@0: fulfilled promise or if an exception is thrown if the promise is rejected. Chris@0: This is set to `true` by default. Chris@0: Chris@0: - `cancel()` Chris@0: Chris@0: Attempts to cancel the promise if possible. The promise being cancelled and Chris@0: the parent most ancestor that has not yet been resolved will also be Chris@0: cancelled. Any promises waiting on the cancelled promise to resolve will also Chris@0: be cancelled. Chris@0: Chris@0: - `getState() : string` Chris@0: Chris@0: Returns the state of the promise. One of `pending`, `fulfilled`, or Chris@0: `rejected`. Chris@0: Chris@0: - `resolve($value)` Chris@0: Chris@0: Fulfills the promise with the given `$value`. Chris@0: Chris@0: - `reject($reason)` Chris@0: Chris@0: Rejects the promise with the given `$reason`. Chris@0: Chris@0: Chris@0: ## FulfilledPromise Chris@0: Chris@0: A fulfilled promise can be created to represent a promise that has been Chris@0: fulfilled. Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\FulfilledPromise; Chris@0: Chris@0: $promise = new FulfilledPromise('value'); Chris@0: Chris@0: // Fulfilled callbacks are immediately invoked. Chris@0: $promise->then(function ($value) { Chris@0: echo $value; Chris@0: }); Chris@0: ``` Chris@0: Chris@0: Chris@0: ## RejectedPromise Chris@0: Chris@0: A rejected promise can be created to represent a promise that has been Chris@0: rejected. Chris@0: Chris@0: ```php Chris@0: use GuzzleHttp\Promise\RejectedPromise; Chris@0: Chris@0: $promise = new RejectedPromise('Error'); Chris@0: Chris@0: // Rejected callbacks are immediately invoked. Chris@0: $promise->then(null, function ($reason) { Chris@0: echo $reason; Chris@0: }); Chris@0: ``` Chris@0: Chris@0: Chris@0: # Promise interop Chris@0: Chris@0: This library works with foreign promises that have a `then` method. This means Chris@0: you can use Guzzle promises with [React promises](https://github.com/reactphp/promise) Chris@0: for example. When a foreign promise is returned inside of a then method Chris@0: callback, promise resolution will occur recursively. Chris@0: Chris@0: ```php Chris@0: // Create a React promise Chris@0: $deferred = new React\Promise\Deferred(); Chris@0: $reactPromise = $deferred->promise(); Chris@0: Chris@0: // Create a Guzzle promise that is fulfilled with a React promise. Chris@0: $guzzlePromise = new \GuzzleHttp\Promise\Promise(); Chris@0: $guzzlePromise->then(function ($value) use ($reactPromise) { Chris@0: // Do something something with the value... Chris@0: // Return the React promise Chris@0: return $reactPromise; Chris@0: }); Chris@0: ``` Chris@0: Chris@0: Please note that wait and cancel chaining is no longer possible when forwarding Chris@0: a foreign promise. You will need to wrap a third-party promise with a Guzzle Chris@0: promise in order to utilize wait and cancel functions with foreign promises. Chris@0: Chris@0: Chris@0: ## Event Loop Integration Chris@0: Chris@0: In order to keep the stack size constant, Guzzle promises are resolved Chris@0: asynchronously using a task queue. When waiting on promises synchronously, the Chris@0: task queue will be automatically run to ensure that the blocking promise and Chris@0: any forwarded promises are resolved. When using promises asynchronously in an Chris@0: event loop, you will need to run the task queue on each tick of the loop. If Chris@0: you do not run the task queue, then promises will not be resolved. Chris@0: Chris@0: You can run the task queue using the `run()` method of the global task queue Chris@0: instance. Chris@0: Chris@0: ```php Chris@0: // Get the global task queue Chris@0: $queue = \GuzzleHttp\Promise\queue(); Chris@0: $queue->run(); Chris@0: ``` Chris@0: Chris@0: For example, you could use Guzzle promises with React using a periodic timer: Chris@0: Chris@0: ```php Chris@0: $loop = React\EventLoop\Factory::create(); Chris@0: $loop->addPeriodicTimer(0, [$queue, 'run']); Chris@0: ``` Chris@0: Chris@0: *TODO*: Perhaps adding a `futureTick()` on each tick would be faster? Chris@0: Chris@0: Chris@0: # Implementation notes Chris@0: Chris@0: Chris@0: ## Promise resolution and chaining is handled iteratively Chris@0: Chris@0: By shuffling pending handlers from one owner to another, promises are Chris@0: resolved iteratively, allowing for "infinite" then chaining. Chris@0: Chris@0: ```php Chris@0: then(function ($v) { Chris@0: // The stack size remains constant (a good thing) Chris@0: echo xdebug_get_stack_depth() . ', '; Chris@0: return $v + 1; Chris@0: }); Chris@0: } Chris@0: Chris@0: $parent->resolve(0); Chris@0: var_dump($p->wait()); // int(1000) Chris@0: Chris@0: ``` Chris@0: Chris@0: When a promise is fulfilled or rejected with a non-promise value, the promise Chris@0: then takes ownership of the handlers of each child promise and delivers values Chris@0: down the chain without using recursion. Chris@0: Chris@0: When a promise is resolved with another promise, the original promise transfers Chris@0: all of its pending handlers to the new promise. When the new promise is Chris@0: eventually resolved, all of the pending handlers are delivered the forwarded Chris@0: value. Chris@0: Chris@0: Chris@0: ## A promise is the deferred. Chris@0: Chris@0: Some promise libraries implement promises using a deferred object to represent Chris@0: a computation and a promise object to represent the delivery of the result of Chris@0: the computation. This is a nice separation of computation and delivery because Chris@0: consumers of the promise cannot modify the value that will be eventually Chris@0: delivered. Chris@0: Chris@0: One side effect of being able to implement promise resolution and chaining Chris@0: iteratively is that you need to be able for one promise to reach into the state Chris@0: of another promise to shuffle around ownership of handlers. In order to achieve Chris@0: this without making the handlers of a promise publicly mutable, a promise is Chris@0: also the deferred value, allowing promises of the same parent class to reach Chris@0: into and modify the private properties of promises of the same type. While this Chris@0: does allow consumers of the value to modify the resolution or rejection of the Chris@0: deferred, it is a small price to pay for keeping the stack size constant. Chris@0: Chris@0: ```php Chris@0: $promise = new Promise(); Chris@0: $promise->then(function ($value) { echo $value; }); Chris@0: // The promise is the deferred value, so you can deliver a value to it. Chris@0: $promise->resolve('foo'); Chris@0: // prints "foo" Chris@0: ```