1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
|
<?php
declare(strict_types=1);
namespace Sabre\Event;
use Exception;
use Throwable;
/**
* An implementation of the Promise pattern.
*
* A promise represents the result of an asynchronous operation.
* At any given point a promise can be in one of three states:
*
* 1. Pending (the promise does not have a result yet).
* 2. Fulfilled (the asynchronous operation has completed with a result).
* 3. Rejected (the asynchronous operation has completed with an error).
*
* To get a callback when the operation has finished, use the `then` method.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*
* @psalm-template TReturn
*/
class Promise
{
/**
* The asynchronous operation is pending.
*/
const PENDING = 0;
/**
* The asynchronous operation has completed, and has a result.
*/
const FULFILLED = 1;
/**
* The asynchronous operation has completed with an error.
*/
const REJECTED = 2;
/**
* The current state of this promise.
*
* @var int
*/
public $state = self::PENDING;
/**
* Creates the promise.
*
* The passed argument is the executor. The executor is automatically
* called with two arguments.
*
* Each are callbacks that map to $this->fulfill and $this->reject.
* Using the executor is optional.
*/
public function __construct(callable $executor = null)
{
if ($executor) {
$executor(
[$this, 'fulfill'],
[$this, 'reject']
);
}
}
/**
* This method allows you to specify the callback that will be called after
* the promise has been fulfilled or rejected.
*
* Both arguments are optional.
*
* This method returns a new promise, which can be used for chaining.
* If either the onFulfilled or onRejected callback is called, you may
* return a result from this callback.
*
* If the result of this callback is yet another promise, the result of
* _that_ promise will be used to set the result of the returned promise.
*
* If either of the callbacks return any other value, the returned promise
* is automatically fulfilled with that value.
*
* If either of the callbacks throw an exception, the returned promise will
* be rejected and the exception will be passed back.
*/
public function then(callable $onFulfilled = null, callable $onRejected = null): Promise
{
// This new subPromise will be returned from this function, and will
// be fulfilled with the result of the onFulfilled or onRejected event
// handlers.
$subPromise = new self();
switch ($this->state) {
case self::PENDING:
// The operation is pending, so we keep a reference to the
// event handlers so we can call them later.
$this->subscribers[] = [$subPromise, $onFulfilled, $onRejected];
break;
case self::FULFILLED:
// The async operation is already fulfilled, so we trigger the
// onFulfilled callback asap.
$this->invokeCallback($subPromise, $onFulfilled);
break;
case self::REJECTED:
// The async operation failed, so we call the onRejected
// callback asap.
$this->invokeCallback($subPromise, $onRejected);
break;
}
return $subPromise;
}
/**
* Add a callback for when this promise is rejected.
*
* Its usage is identical to then(). However, the otherwise() function is
* preferred.
*/
public function otherwise(callable $onRejected): Promise
{
return $this->then(null, $onRejected);
}
/**
* Marks this promise as fulfilled and sets its return value.
*
* @param mixed $value
*/
public function fulfill($value = null)
{
if (self::PENDING !== $this->state) {
throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once');
}
$this->state = self::FULFILLED;
$this->value = $value;
foreach ($this->subscribers as $subscriber) {
$this->invokeCallback($subscriber[0], $subscriber[1]);
}
}
/**
* Marks this promise as rejected, and set it's rejection reason.
*/
public function reject(Throwable $reason)
{
if (self::PENDING !== $this->state) {
throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once');
}
$this->state = self::REJECTED;
$this->value = $reason;
foreach ($this->subscribers as $subscriber) {
$this->invokeCallback($subscriber[0], $subscriber[2]);
}
}
/**
* Stops execution until this promise is resolved.
*
* This method stops execution completely. If the promise is successful with
* a value, this method will return this value. If the promise was
* rejected, this method will throw an exception.
*
* This effectively turns the asynchronous operation into a synchronous
* one. In PHP it might be useful to call this on the last promise in a
* chain.
*
* @return mixed
* @psalm-return TReturn
*/
public function wait()
{
$hasEvents = true;
while (self::PENDING === $this->state) {
if (!$hasEvents) {
throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.');
}
// As long as the promise is not fulfilled, we tell the event loop
// to handle events, and to block.
$hasEvents = Loop\tick(true);
}
if (self::FULFILLED === $this->state) {
// If the state of this promise is fulfilled, we can return the value.
return $this->value;
} else {
// If we got here, it means that the asynchronous operation
// errored. Therefore we need to throw an exception.
throw $this->value;
}
}
/**
* A list of subscribers. Subscribers are the callbacks that want us to let
* them know if the callback was fulfilled or rejected.
*
* @var array
*/
protected $subscribers = [];
/**
* The result of the promise.
*
* If the promise was fulfilled, this will be the result value. If the
* promise was rejected, this property hold the rejection reason.
*
* @var mixed
*/
protected $value = null;
/**
* This method is used to call either an onFulfilled or onRejected callback.
*
* This method makes sure that the result of these callbacks are handled
* correctly, and any chained promises are also correctly fulfilled or
* rejected.
*
* @param callable $callBack
*/
private function invokeCallback(Promise $subPromise, callable $callBack = null)
{
// We use 'nextTick' to ensure that the event handlers are always
// triggered outside of the calling stack in which they were originally
// passed to 'then'.
//
// This makes the order of execution more predictable.
Loop\nextTick(function () use ($callBack, $subPromise) {
if (is_callable($callBack)) {
try {
$result = $callBack($this->value);
if ($result instanceof self) {
// If the callback (onRejected or onFulfilled)
// returned a promise, we only fulfill or reject the
// chained promise once that promise has also been
// resolved.
$result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']);
} else {
// If the callback returned any other value, we
// immediately fulfill the chained promise.
$subPromise->fulfill($result);
}
} catch (Throwable $e) {
// If the event handler threw an exception, we need to make sure that
// the chained promise is rejected as well.
$subPromise->reject($e);
}
} else {
if (self::FULFILLED === $this->state) {
$subPromise->fulfill($this->value);
} else {
$subPromise->reject($this->value);
}
}
});
}
}
|