502 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			502 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # 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()`.
 | |
| 
 | |
| 
 | |
| # 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;
 | |
| 
 | |
| $promise = new Promise();
 | |
| $promise->then(
 | |
|     // $onFulfilled
 | |
|     function ($value) {
 | |
|         echo 'The promise was fulfilled.';
 | |
|     },
 | |
|     // $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 promises triggers callbacks
 | |
| registered with the promises'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 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;
 | |
| use GuzzleHttp\Promise\RejectedPromise;
 | |
| 
 | |
| $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 an exception is encountered while invoking the wait function of a promise,
 | |
| the promise is rejected with the exception and the exception 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 an exception. If
 | |
| the rejection reason is an instance of `\Exception` 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...)
 | |
|     }
 | |
| );
 | |
| 
 | |
| assert('waited' === $promise->wait());
 | |
| ```
 | |
| 
 | |
| A promise has the following methods:
 | |
| 
 | |
| - `then(callable $onFulfilled, callable $onRejected) : PromiseInterface`
 | |
|   
 | |
|   Creates a new promise that is fulfilled or rejected when the promise is
 | |
|   resolved.
 | |
| 
 | |
| - `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.
 | |
| 
 | |
| ```php
 | |
| use GuzzleHttp\Promise\FulfilledPromise;
 | |
| 
 | |
| $promise = new FulfilledPromise('value');
 | |
| 
 | |
| // Fulfilled callbacks are immediately invoked.
 | |
| $promise->then(function ($value) {
 | |
|     echo $value;
 | |
| });
 | |
| ```
 | |
| 
 | |
| 
 | |
| ## RejectedPromise
 | |
| 
 | |
| A rejected promise can be created to represent a promise that has been
 | |
| rejected.
 | |
| 
 | |
| ```php
 | |
| use GuzzleHttp\Promise\RejectedPromise;
 | |
| 
 | |
| $promise = new RejectedPromise('Error');
 | |
| 
 | |
| // Rejected callbacks are immediately invoked.
 | |
| $promise->then(null, function ($reason) {
 | |
|     echo $reason;
 | |
| });
 | |
| ```
 | |
| 
 | |
| 
 | |
| # Promise interop
 | |
| 
 | |
| 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\queue();
 | |
| $queue->run();
 | |
| ```
 | |
| 
 | |
| For example, you could use Guzzle promises with React using a periodic timer:
 | |
| 
 | |
| ```php
 | |
| $loop = React\EventLoop\Factory::create();
 | |
| $loop->addPeriodicTimer(0, [$queue, 'run']);
 | |
| ```
 | |
| 
 | |
| *TODO*: Perhaps adding a `futureTick()` on each tick would be faster?
 | |
| 
 | |
| 
 | |
| # 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"
 | |
| ```
 | 
