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