1<?php
2
3namespace Sabre\Event;
4
5use Exception;
6
7/**
8 * An implementation of the Promise pattern.
9 *
10 * Promises basically allow you to avoid what is commonly called 'callback
11 * hell'. It allows for easily chaining of asynchronous operations.
12 *
13 * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/).
14 * @author Evert Pot (http://evertpot.com/)
15 * @license http://sabre.io/license/ Modified BSD License
16 */
17class Promise {
18
19    /**
20     * Pending promise. No result yet.
21     */
22    const PENDING = 0;
23
24    /**
25     * The promise has been fulfilled. It was successful.
26     */
27    const FULFILLED = 1;
28
29    /**
30     * The promise was rejected. The operation failed.
31     */
32    const REJECTED = 2;
33
34    /**
35     * The current state of this promise.
36     *
37     * @var int
38     */
39    protected $state = self::PENDING;
40
41    /**
42     * A list of subscribers. Subscribers are the callbacks that want us to let
43     * them know if the callback was fulfilled or rejected.
44     *
45     * @var array
46     */
47    protected $subscribers = [];
48
49    /**
50     * The result of the promise.
51     *
52     * If the promise was fulfilled, this will be the result value. If the
53     * promise was rejected, this is most commonly an exception.
54     *
55     * @var mixed
56     */
57    protected $value = null;
58
59    /**
60     * Creates the promise.
61     *
62     * The passed argument is the executor. The executor is automatically
63     * called with two arguments.
64     *
65     * Each are callbacks that map to $this->fulfill and $this->reject.
66     * Using the executor is optional.
67     *
68     * @param callable $executor
69     */
70    function __construct(callable $executor = null) {
71
72        if ($executor) {
73            $executor(
74                [$this, 'fulfill'],
75                [$this, 'reject']
76            );
77        }
78
79    }
80
81    /**
82     * This method allows you to specify the callback that will be called after
83     * the promise has been fulfilled or rejected.
84     *
85     * Both arguments are optional.
86     *
87     * This method returns a new promise, which can be used for chaining.
88     * If either the onFulfilled or onRejected callback is called, you may
89     * return a result from this callback.
90     *
91     * If the result of this callback is yet another promise, the result of
92     * _that_ promise will be used to set the result of the returned promise.
93     *
94     * If either of the callbacks return any other value, the returned promise
95     * is automatically fulfilled with that value.
96     *
97     * If either of the callbacks throw an exception, the returned promise will
98     * be rejected and the exception will be passed back.
99     *
100     * @param callable $onFulfilled
101     * @param callable $onRejected
102     * @return Promise
103     */
104    function then(callable $onFulfilled = null, callable $onRejected = null) {
105
106        $subPromise = new self();
107        switch ($this->state) {
108            case self::PENDING :
109                $this->subscribers[] = [$subPromise, $onFulfilled, $onRejected];
110                break;
111            case self::FULFILLED :
112                $this->invokeCallback($subPromise, $onFulfilled);
113                break;
114            case self::REJECTED :
115                $this->invokeCallback($subPromise, $onRejected);
116                break;
117        }
118        return $subPromise;
119
120    }
121
122    /**
123     * Add a callback for when this promise is rejected.
124     *
125     * I would have used the word 'catch', but it's a reserved word in PHP, so
126     * we're not allowed to call our function that.
127     *
128     * @param callable $onRejected
129     * @return Promise
130     */
131    function error(callable $onRejected) {
132
133        return $this->then(null, $onRejected);
134
135    }
136
137    /**
138     * Marks this promise as fulfilled and sets its return value.
139     *
140     * @param mixed $value
141     * @return void
142     */
143    function fulfill($value = null) {
144        if ($this->state !== self::PENDING) {
145            throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once');
146        }
147        $this->state = self::FULFILLED;
148        $this->value = $value;
149        foreach ($this->subscribers as $subscriber) {
150            $this->invokeCallback($subscriber[0], $subscriber[1]);
151        }
152    }
153
154    /**
155     * Marks this promise as rejected, and set it's rejection reason.
156     *
157     * @param mixed $reason
158     * @return void
159     */
160    function reject($reason = null) {
161        if ($this->state !== self::PENDING) {
162            throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once');
163        }
164        $this->state = self::REJECTED;
165        $this->value = $reason;
166        foreach ($this->subscribers as $subscriber) {
167            $this->invokeCallback($subscriber[0], $subscriber[2]);
168        }
169
170    }
171
172    /**
173     * It's possible to send an array of promises to the all method. This
174     * method returns a promise that will be fulfilled, only if all the passed
175     * promises are fulfilled.
176     *
177     * @param Promise[] $promises
178     * @return Promise
179     */
180    static function all(array $promises) {
181
182        return new self(function($success, $fail) use ($promises) {
183
184            $successCount = 0;
185            $completeResult = [];
186
187            foreach ($promises as $promiseIndex => $subPromise) {
188
189                $subPromise->then(
190                    function($result) use ($promiseIndex, &$completeResult, &$successCount, $success, $promises) {
191                        $completeResult[$promiseIndex] = $result;
192                        $successCount++;
193                        if ($successCount === count($promises)) {
194                            $success($completeResult);
195                        }
196                        return $result;
197                    }
198                )->error(
199                    function($reason) use ($fail) {
200                        $fail($reason);
201                    }
202                );
203
204            }
205        });
206
207    }
208
209    /**
210     * This method is used to call either an onFulfilled or onRejected callback.
211     *
212     * This method makes sure that the result of these callbacks are handled
213     * correctly, and any chained promises are also correctly fulfilled or
214     * rejected.
215     *
216     * @param Promise $subPromise
217     * @param callable $callBack
218     * @return void
219     */
220    protected function invokeCallback(Promise $subPromise, callable $callBack = null) {
221
222        if (is_callable($callBack)) {
223            try {
224                $result = $callBack($this->value);
225                if ($result instanceof self) {
226                    $result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']);
227                } else {
228                    $subPromise->fulfill($result);
229                }
230            } catch (Exception $e) {
231                $subPromise->reject($e);
232            }
233        } else {
234            if ($this->state === self::FULFILLED) {
235                $subPromise->fulfill($this->value);
236            } else {
237                $subPromise->reject($this->value);
238            }
239        }
240    }
241
242
243}
244