Subversion Repositories cheapmusic

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
103 - 1
# Guzzle Promises
2
 
3
[Promises/A+](https://promisesaplus.com/) implementation that handles promise
4
chaining and resolution iteratively, allowing for "infinite" promise chaining
5
while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/)
6
for a general introduction to promises.
7
 
8
- [Features](#features)
9
- [Quick start](#quick-start)
10
- [Synchronous wait](#synchronous-wait)
11
- [Cancellation](#cancellation)
12
- [API](#api)
13
  - [Promise](#promise)
14
  - [FulfilledPromise](#fulfilledpromise)
15
  - [RejectedPromise](#rejectedpromise)
16
- [Promise interop](#promise-interop)
17
- [Implementation notes](#implementation-notes)
18
 
19
 
20
# Features
21
 
22
- [Promises/A+](https://promisesaplus.com/) implementation.
23
- Promise resolution and chaining is handled iteratively, allowing for
24
  "infinite" promise chaining.
25
- Promises have a synchronous `wait` method.
26
- Promises can be cancelled.
27
- Works with any object that has a `then` function.
28
- C# style async/await coroutine promises using
29
  `GuzzleHttp\Promise\coroutine()`.
30
 
31
 
32
# Quick start
33
 
34
A *promise* represents the eventual result of an asynchronous operation. The
35
primary way of interacting with a promise is through its `then` method, which
36
registers callbacks to receive either a promise's eventual value or the reason
37
why the promise cannot be fulfilled.
38
 
39
 
40
## Callbacks
41
 
42
Callbacks are registered with the `then` method by providing an optional
43
`$onFulfilled` followed by an optional `$onRejected` function.
44
 
45
 
46
```php
47
use GuzzleHttp\Promise\Promise;
48
 
49
$promise = new Promise();
50
$promise->then(
51
    // $onFulfilled
52
    function ($value) {
53
        echo 'The promise was fulfilled.';
54
    },
55
    // $onRejected
56
    function ($reason) {
57
        echo 'The promise was rejected.';
58
    }
59
);
60
```
61
 
62
*Resolving* a promise means that you either fulfill a promise with a *value* or
63
reject a promise with a *reason*. Resolving a promises triggers callbacks
64
registered with the promises's `then` method. These callbacks are triggered
65
only once and in the order in which they were added.
66
 
67
 
68
## Resolving a promise
69
 
70
Promises are fulfilled using the `resolve($value)` method. Resolving a promise
71
with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger
72
all of the onFulfilled callbacks (resolving a promise with a rejected promise
73
will reject the promise and trigger the `$onRejected` callbacks).
74
 
75
```php
76
use GuzzleHttp\Promise\Promise;
77
 
78
$promise = new Promise();
79
$promise
80
    ->then(function ($value) {
81
        // Return a value and don't break the chain
82
        return "Hello, " . $value;
83
    })
84
    // This then is executed after the first then and receives the value
85
    // returned from the first then.
86
    ->then(function ($value) {
87
        echo $value;
88
    });
89
 
90
// Resolving the promise triggers the $onFulfilled callbacks and outputs
91
// "Hello, reader".
92
$promise->resolve('reader.');
93
```
94
 
95
 
96
## Promise forwarding
97
 
98
Promises can be chained one after the other. Each then in the chain is a new
99
promise. The return value of a promise is what's forwarded to the next
100
promise in the chain. Returning a promise in a `then` callback will cause the
101
subsequent promises in the chain to only be fulfilled when the returned promise
102
has been fulfilled. The next promise in the chain will be invoked with the
103
resolved value of the promise.
104
 
105
```php
106
use GuzzleHttp\Promise\Promise;
107
 
108
$promise = new Promise();
109
$nextPromise = new Promise();
110
 
111
$promise
112
    ->then(function ($value) use ($nextPromise) {
113
        echo $value;
114
        return $nextPromise;
115
    })
116
    ->then(function ($value) {
117
        echo $value;
118
    });
119
 
120
// Triggers the first callback and outputs "A"
121
$promise->resolve('A');
122
// Triggers the second callback and outputs "B"
123
$nextPromise->resolve('B');
124
```
125
 
126
## Promise rejection
127
 
128
When a promise is rejected, the `$onRejected` callbacks are invoked with the
129
rejection reason.
130
 
131
```php
132
use GuzzleHttp\Promise\Promise;
133
 
134
$promise = new Promise();
135
$promise->then(null, function ($reason) {
136
    echo $reason;
137
});
138
 
139
$promise->reject('Error!');
140
// Outputs "Error!"
141
```
142
 
143
## Rejection forwarding
144
 
145
If an exception is thrown in an `$onRejected` callback, subsequent
146
`$onRejected` callbacks are invoked with the thrown exception as the reason.
147
 
148
```php
149
use GuzzleHttp\Promise\Promise;
150
 
151
$promise = new Promise();
152
$promise->then(null, function ($reason) {
153
    throw new \Exception($reason);
154
})->then(null, function ($reason) {
155
    assert($reason->getMessage() === 'Error!');
156
});
157
 
158
$promise->reject('Error!');
159
```
160
 
161
You can also forward a rejection down the promise chain by returning a
162
`GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or
163
`$onRejected` callback.
164
 
165
```php
166
use GuzzleHttp\Promise\Promise;
167
use GuzzleHttp\Promise\RejectedPromise;
168
 
169
$promise = new Promise();
170
$promise->then(null, function ($reason) {
171
    return new RejectedPromise($reason);
172
})->then(null, function ($reason) {
173
    assert($reason === 'Error!');
174
});
175
 
176
$promise->reject('Error!');
177
```
178
 
179
If an exception is not thrown in a `$onRejected` callback and the callback
180
does not return a rejected promise, downstream `$onFulfilled` callbacks are
181
invoked using the value returned from the `$onRejected` callback.
182
 
183
```php
184
use GuzzleHttp\Promise\Promise;
185
use GuzzleHttp\Promise\RejectedPromise;
186
 
187
$promise = new Promise();
188
$promise
189
    ->then(null, function ($reason) {
190
        return "It's ok";
191
    })
192
    ->then(function ($value) {
193
        assert($value === "It's ok");
194
    });
195
 
196
$promise->reject('Error!');
197
```
198
 
199
# Synchronous wait
200
 
201
You can synchronously force promises to complete using a promise's `wait`
202
method. When creating a promise, you can provide a wait function that is used
203
to synchronously force a promise to complete. When a wait function is invoked
204
it is expected to deliver a value to the promise or reject the promise. If the
205
wait function does not deliver a value, then an exception is thrown. The wait
206
function provided to a promise constructor is invoked when the `wait` function
207
of the promise is called.
208
 
209
```php
210
$promise = new Promise(function () use (&$promise) {
211
    $promise->resolve('foo');
212
});
213
 
214
// Calling wait will return the value of the promise.
215
echo $promise->wait(); // outputs "foo"
216
```
217
 
218
If an exception is encountered while invoking the wait function of a promise,
219
the promise is rejected with the exception and the exception is thrown.
220
 
221
```php
222
$promise = new Promise(function () use (&$promise) {
223
    throw new \Exception('foo');
224
});
225
 
226
$promise->wait(); // throws the exception.
227
```
228
 
229
Calling `wait` on a promise that has been fulfilled will not trigger the wait
230
function. It will simply return the previously resolved value.
231
 
232
```php
233
$promise = new Promise(function () { die('this is not called!'); });
234
$promise->resolve('foo');
235
echo $promise->wait(); // outputs "foo"
236
```
237
 
238
Calling `wait` on a promise that has been rejected will throw an exception. If
239
the rejection reason is an instance of `\Exception` the reason is thrown.
240
Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason
241
can be obtained by calling the `getReason` method of the exception.
242
 
243
```php
244
$promise = new Promise();
245
$promise->reject('foo');
246
$promise->wait();
247
```
248
 
249
> PHP Fatal error:  Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo'
250
 
251
 
252
## Unwrapping a promise
253
 
254
When synchronously waiting on a promise, you are joining the state of the
255
promise into the current state of execution (i.e., return the value of the
256
promise if it was fulfilled or throw an exception if it was rejected). This is
257
called "unwrapping" the promise. Waiting on a promise will by default unwrap
258
the promise state.
259
 
260
You can force a promise to resolve and *not* unwrap the state of the promise
261
by passing `false` to the first argument of the `wait` function:
262
 
263
```php
264
$promise = new Promise();
265
$promise->reject('foo');
266
// This will not throw an exception. It simply ensures the promise has
267
// been resolved.
268
$promise->wait(false);
269
```
270
 
271
When unwrapping a promise, the resolved value of the promise will be waited
272
upon until the unwrapped value is not a promise. This means that if you resolve
273
promise A with a promise B and unwrap promise A, the value returned by the
274
wait function will be the value delivered to promise B.
275
 
276
**Note**: when you do not unwrap the promise, no value is returned.
277
 
278
 
279
# Cancellation
280
 
281
You can cancel a promise that has not yet been fulfilled using the `cancel()`
282
method of a promise. When creating a promise you can provide an optional
283
cancel function that when invoked cancels the action of computing a resolution
284
of the promise.
285
 
286
 
287
# API
288
 
289
 
290
## Promise
291
 
292
When creating a promise object, you can provide an optional `$waitFn` and
293
`$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is
294
expected to resolve the promise. `$cancelFn` is a function with no arguments
295
that is expected to cancel the computation of a promise. It is invoked when the
296
`cancel()` method of a promise is called.
297
 
298
```php
299
use GuzzleHttp\Promise\Promise;
300
 
301
$promise = new Promise(
302
    function () use (&$promise) {
303
        $promise->resolve('waited');
304
    },
305
    function () {
306
        // do something that will cancel the promise computation (e.g., close
307
        // a socket, cancel a database query, etc...)
308
    }
309
);
310
 
311
assert('waited' === $promise->wait());
312
```
313
 
314
A promise has the following methods:
315
 
316
- `then(callable $onFulfilled, callable $onRejected) : PromiseInterface`
317
 
318
  Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler.
319
 
320
- `otherwise(callable $onRejected) : PromiseInterface`
321
 
322
  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.
323
 
324
- `wait($unwrap = true) : mixed`
325
 
326
  Synchronously waits on the promise to complete.
327
 
328
  `$unwrap` controls whether or not the value of the promise is returned for a
329
  fulfilled promise or if an exception is thrown if the promise is rejected.
330
  This is set to `true` by default.
331
 
332
- `cancel()`
333
 
334
  Attempts to cancel the promise if possible. The promise being cancelled and
335
  the parent most ancestor that has not yet been resolved will also be
336
  cancelled. Any promises waiting on the cancelled promise to resolve will also
337
  be cancelled.
338
 
339
- `getState() : string`
340
 
341
  Returns the state of the promise. One of `pending`, `fulfilled`, or
342
  `rejected`.
343
 
344
- `resolve($value)`
345
 
346
  Fulfills the promise with the given `$value`.
347
 
348
- `reject($reason)`
349
 
350
  Rejects the promise with the given `$reason`.
351
 
352
 
353
## FulfilledPromise
354
 
355
A fulfilled promise can be created to represent a promise that has been
356
fulfilled.
357
 
358
```php
359
use GuzzleHttp\Promise\FulfilledPromise;
360
 
361
$promise = new FulfilledPromise('value');
362
 
363
// Fulfilled callbacks are immediately invoked.
364
$promise->then(function ($value) {
365
    echo $value;
366
});
367
```
368
 
369
 
370
## RejectedPromise
371
 
372
A rejected promise can be created to represent a promise that has been
373
rejected.
374
 
375
```php
376
use GuzzleHttp\Promise\RejectedPromise;
377
 
378
$promise = new RejectedPromise('Error');
379
 
380
// Rejected callbacks are immediately invoked.
381
$promise->then(null, function ($reason) {
382
    echo $reason;
383
});
384
```
385
 
386
 
387
# Promise interop
388
 
389
This library works with foreign promises that have a `then` method. This means
390
you can use Guzzle promises with [React promises](https://github.com/reactphp/promise)
391
for example. When a foreign promise is returned inside of a then method
392
callback, promise resolution will occur recursively.
393
 
394
```php
395
// Create a React promise
396
$deferred = new React\Promise\Deferred();
397
$reactPromise = $deferred->promise();
398
 
399
// Create a Guzzle promise that is fulfilled with a React promise.
400
$guzzlePromise = new \GuzzleHttp\Promise\Promise();
401
$guzzlePromise->then(function ($value) use ($reactPromise) {
402
    // Do something something with the value...
403
    // Return the React promise
404
    return $reactPromise;
405
});
406
```
407
 
408
Please note that wait and cancel chaining is no longer possible when forwarding
409
a foreign promise. You will need to wrap a third-party promise with a Guzzle
410
promise in order to utilize wait and cancel functions with foreign promises.
411
 
412
 
413
## Event Loop Integration
414
 
415
In order to keep the stack size constant, Guzzle promises are resolved
416
asynchronously using a task queue. When waiting on promises synchronously, the
417
task queue will be automatically run to ensure that the blocking promise and
418
any forwarded promises are resolved. When using promises asynchronously in an
419
event loop, you will need to run the task queue on each tick of the loop. If
420
you do not run the task queue, then promises will not be resolved.
421
 
422
You can run the task queue using the `run()` method of the global task queue
423
instance.
424
 
425
```php
426
// Get the global task queue
427
$queue = \GuzzleHttp\Promise\queue();
428
$queue->run();
429
```
430
 
431
For example, you could use Guzzle promises with React using a periodic timer:
432
 
433
```php
434
$loop = React\EventLoop\Factory::create();
435
$loop->addPeriodicTimer(0, [$queue, 'run']);
436
```
437
 
438
*TODO*: Perhaps adding a `futureTick()` on each tick would be faster?
439
 
440
 
441
# Implementation notes
442
 
443
 
444
## Promise resolution and chaining is handled iteratively
445
 
446
By shuffling pending handlers from one owner to another, promises are
447
resolved iteratively, allowing for "infinite" then chaining.
448
 
449
```php
450
<?php
451
require 'vendor/autoload.php';
452
 
453
use GuzzleHttp\Promise\Promise;
454
 
455
$parent = new Promise();
456
$p = $parent;
457
 
458
for ($i = 0; $i < 1000; $i++) {
459
    $p = $p->then(function ($v) {
460
        // The stack size remains constant (a good thing)
461
        echo xdebug_get_stack_depth() . ', ';
462
        return $v + 1;
463
    });
464
}
465
 
466
$parent->resolve(0);
467
var_dump($p->wait()); // int(1000)
468
 
469
```
470
 
471
When a promise is fulfilled or rejected with a non-promise value, the promise
472
then takes ownership of the handlers of each child promise and delivers values
473
down the chain without using recursion.
474
 
475
When a promise is resolved with another promise, the original promise transfers
476
all of its pending handlers to the new promise. When the new promise is
477
eventually resolved, all of the pending handlers are delivered the forwarded
478
value.
479
 
480
 
481
## A promise is the deferred.
482
 
483
Some promise libraries implement promises using a deferred object to represent
484
a computation and a promise object to represent the delivery of the result of
485
the computation. This is a nice separation of computation and delivery because
486
consumers of the promise cannot modify the value that will be eventually
487
delivered.
488
 
489
One side effect of being able to implement promise resolution and chaining
490
iteratively is that you need to be able for one promise to reach into the state
491
of another promise to shuffle around ownership of handlers. In order to achieve
492
this without making the handlers of a promise publicly mutable, a promise is
493
also the deferred value, allowing promises of the same parent class to reach
494
into and modify the private properties of promises of the same type. While this
495
does allow consumers of the value to modify the resolution or rejection of the
496
deferred, it is a small price to pay for keeping the stack size constant.
497
 
498
```php
499
$promise = new Promise();
500
$promise->then(function ($value) { echo $value; });
501
// The promise is the deferred value, so you can deliver a value to it.
502
$promise->resolve('foo');
503
// prints "foo"
504
```