1<?php
2
3namespace React\Promise;
4
5class Promise implements ExtendedPromiseInterface, CancellablePromiseInterface
6{
7    private $canceller;
8    private $result;
9
10    private $handlers = [];
11    private $progressHandlers = [];
12
13    private $requiredCancelRequests = 0;
14    private $cancelRequests = 0;
15
16    public function __construct(callable $resolver, callable $canceller = null)
17    {
18        $this->canceller = $canceller;
19
20        // Explicitly overwrite arguments with null values before invoking
21        // resolver function. This ensure that these arguments do not show up
22        // in the stack trace in PHP 7+ only.
23        $cb = $resolver;
24        $resolver = $canceller = null;
25        $this->call($cb);
26    }
27
28    public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null)
29    {
30        if (null !== $this->result) {
31            return $this->result->then($onFulfilled, $onRejected, $onProgress);
32        }
33
34        if (null === $this->canceller) {
35            return new static($this->resolver($onFulfilled, $onRejected, $onProgress));
36        }
37
38        // This promise has a canceller, so we create a new child promise which
39        // has a canceller that invokes the parent canceller if all other
40        // followers are also cancelled. We keep a reference to this promise
41        // instance for the static canceller function and clear this to avoid
42        // keeping a cyclic reference between parent and follower.
43        $parent = $this;
44        ++$parent->requiredCancelRequests;
45
46        return new static(
47            $this->resolver($onFulfilled, $onRejected, $onProgress),
48            static function () use (&$parent) {
49                if (++$parent->cancelRequests >= $parent->requiredCancelRequests) {
50                    $parent->cancel();
51                }
52
53                $parent = null;
54            }
55        );
56    }
57
58    public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null)
59    {
60        if (null !== $this->result) {
61            return $this->result->done($onFulfilled, $onRejected, $onProgress);
62        }
63
64        $this->handlers[] = static function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected) {
65            $promise
66                ->done($onFulfilled, $onRejected);
67        };
68
69        if ($onProgress) {
70            $this->progressHandlers[] = $onProgress;
71        }
72    }
73
74    public function otherwise(callable $onRejected)
75    {
76        return $this->then(null, static function ($reason) use ($onRejected) {
77            if (!_checkTypehint($onRejected, $reason)) {
78                return new RejectedPromise($reason);
79            }
80
81            return $onRejected($reason);
82        });
83    }
84
85    public function always(callable $onFulfilledOrRejected)
86    {
87        return $this->then(static function ($value) use ($onFulfilledOrRejected) {
88            return resolve($onFulfilledOrRejected())->then(function () use ($value) {
89                return $value;
90            });
91        }, static function ($reason) use ($onFulfilledOrRejected) {
92            return resolve($onFulfilledOrRejected())->then(function () use ($reason) {
93                return new RejectedPromise($reason);
94            });
95        });
96    }
97
98    public function progress(callable $onProgress)
99    {
100        return $this->then(null, null, $onProgress);
101    }
102
103    public function cancel()
104    {
105        if (null === $this->canceller || null !== $this->result) {
106            return;
107        }
108
109        $canceller = $this->canceller;
110        $this->canceller = null;
111
112        $this->call($canceller);
113    }
114
115    private function resolver(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null)
116    {
117        return function ($resolve, $reject, $notify) use ($onFulfilled, $onRejected, $onProgress) {
118            if ($onProgress) {
119                $progressHandler = static function ($update) use ($notify, $onProgress) {
120                    try {
121                        $notify($onProgress($update));
122                    } catch (\Throwable $e) {
123                        $notify($e);
124                    } catch (\Exception $e) {
125                        $notify($e);
126                    }
127                };
128            } else {
129                $progressHandler = $notify;
130            }
131
132            $this->handlers[] = static function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject, $progressHandler) {
133                $promise
134                    ->then($onFulfilled, $onRejected)
135                    ->done($resolve, $reject, $progressHandler);
136            };
137
138            $this->progressHandlers[] = $progressHandler;
139        };
140    }
141
142    private function reject($reason = null)
143    {
144        if (null !== $this->result) {
145            return;
146        }
147
148        $this->settle(reject($reason));
149    }
150
151    private function settle(ExtendedPromiseInterface $promise)
152    {
153        $promise = $this->unwrap($promise);
154
155        if ($promise === $this) {
156            $promise = new RejectedPromise(
157                new \LogicException('Cannot resolve a promise with itself.')
158            );
159        }
160
161        $handlers = $this->handlers;
162
163        $this->progressHandlers = $this->handlers = [];
164        $this->result = $promise;
165        $this->canceller = null;
166
167        foreach ($handlers as $handler) {
168            $handler($promise);
169        }
170    }
171
172    private function unwrap($promise)
173    {
174        $promise = $this->extract($promise);
175
176        while ($promise instanceof self && null !== $promise->result) {
177            $promise = $this->extract($promise->result);
178        }
179
180        return $promise;
181    }
182
183    private function extract($promise)
184    {
185        if ($promise instanceof LazyPromise) {
186            $promise = $promise->promise();
187        }
188
189        return $promise;
190    }
191
192    private function call(callable $cb)
193    {
194        // Explicitly overwrite argument with null value. This ensure that this
195        // argument does not show up in the stack trace in PHP 7+ only.
196        $callback = $cb;
197        $cb = null;
198
199        // Use reflection to inspect number of arguments expected by this callback.
200        // We did some careful benchmarking here: Using reflection to avoid unneeded
201        // function arguments is actually faster than blindly passing them.
202        // Also, this helps avoiding unnecessary function arguments in the call stack
203        // if the callback creates an Exception (creating garbage cycles).
204        if (\is_array($callback)) {
205            $ref = new \ReflectionMethod($callback[0], $callback[1]);
206        } elseif (\is_object($callback) && !$callback instanceof \Closure) {
207            $ref = new \ReflectionMethod($callback, '__invoke');
208        } else {
209            $ref = new \ReflectionFunction($callback);
210        }
211        $args = $ref->getNumberOfParameters();
212
213        try {
214            if ($args === 0) {
215                $callback();
216            } else {
217                // Keep references to this promise instance for the static resolve/reject functions.
218                // By using static callbacks that are not bound to this instance
219                // and passing the target promise instance by reference, we can
220                // still execute its resolving logic and still clear this
221                // reference when settling the promise. This helps avoiding
222                // garbage cycles if any callback creates an Exception.
223                // These assumptions are covered by the test suite, so if you ever feel like
224                // refactoring this, go ahead, any alternative suggestions are welcome!
225                $target =& $this;
226                $progressHandlers =& $this->progressHandlers;
227
228                $callback(
229                    static function ($value = null) use (&$target) {
230                        if ($target !== null) {
231                            $target->settle(resolve($value));
232                            $target = null;
233                        }
234                    },
235                    static function ($reason = null) use (&$target) {
236                        if ($target !== null) {
237                            $target->reject($reason);
238                            $target = null;
239                        }
240                    },
241                    static function ($update = null) use (&$progressHandlers) {
242                        foreach ($progressHandlers as $handler) {
243                            $handler($update);
244                        }
245                    }
246                );
247            }
248        } catch (\Throwable $e) {
249            $target = null;
250            $this->reject($e);
251        } catch (\Exception $e) {
252            $target = null;
253            $this->reject($e);
254        }
255    }
256}
257