friendship ended with social-app. php is my new best friend
1<?php
2
3namespace React\Promise;
4
5use React\Promise\Internal\RejectedPromise;
6
7/**
8 * @template T
9 * @template-implements PromiseInterface<T>
10 */
11final class Promise implements PromiseInterface
12{
13 /** @var (callable(callable(T):void,callable(\Throwable):void):void)|null */
14 private $canceller;
15
16 /** @var ?PromiseInterface<T> */
17 private $result;
18
19 /** @var list<callable(PromiseInterface<T>):void> */
20 private $handlers = [];
21
22 /** @var int */
23 private $requiredCancelRequests = 0;
24
25 /** @var bool */
26 private $cancelled = false;
27
28 /**
29 * @param callable(callable(T):void,callable(\Throwable):void):void $resolver
30 * @param (callable(callable(T):void,callable(\Throwable):void):void)|null $canceller
31 */
32 public function __construct(callable $resolver, ?callable $canceller = null)
33 {
34 $this->canceller = $canceller;
35
36 // Explicitly overwrite arguments with null values before invoking
37 // resolver function. This ensure that these arguments do not show up
38 // in the stack trace in PHP 7+ only.
39 $cb = $resolver;
40 $resolver = $canceller = null;
41 $this->call($cb);
42 }
43
44 public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface
45 {
46 if (null !== $this->result) {
47 return $this->result->then($onFulfilled, $onRejected);
48 }
49
50 if (null === $this->canceller) {
51 return new static($this->resolver($onFulfilled, $onRejected));
52 }
53
54 // This promise has a canceller, so we create a new child promise which
55 // has a canceller that invokes the parent canceller if all other
56 // followers are also cancelled. We keep a reference to this promise
57 // instance for the static canceller function and clear this to avoid
58 // keeping a cyclic reference between parent and follower.
59 $parent = $this;
60 ++$parent->requiredCancelRequests;
61
62 return new static(
63 $this->resolver($onFulfilled, $onRejected),
64 static function () use (&$parent): void {
65 assert($parent instanceof self);
66 --$parent->requiredCancelRequests;
67
68 if ($parent->requiredCancelRequests <= 0) {
69 $parent->cancel();
70 }
71
72 $parent = null;
73 }
74 );
75 }
76
77 /**
78 * @template TThrowable of \Throwable
79 * @template TRejected
80 * @param callable(TThrowable): (PromiseInterface<TRejected>|TRejected) $onRejected
81 * @return PromiseInterface<T|TRejected>
82 */
83 public function catch(callable $onRejected): PromiseInterface
84 {
85 return $this->then(null, static function (\Throwable $reason) use ($onRejected) {
86 if (!_checkTypehint($onRejected, $reason)) {
87 return new RejectedPromise($reason);
88 }
89
90 /**
91 * @var callable(\Throwable):(PromiseInterface<TRejected>|TRejected) $onRejected
92 */
93 return $onRejected($reason);
94 });
95 }
96
97 public function finally(callable $onFulfilledOrRejected): PromiseInterface
98 {
99 return $this->then(static function ($value) use ($onFulfilledOrRejected): PromiseInterface {
100 /** @var T $value */
101 return resolve($onFulfilledOrRejected())->then(function () use ($value) {
102 return $value;
103 });
104 }, static function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface {
105 return resolve($onFulfilledOrRejected())->then(function () use ($reason): RejectedPromise {
106 return new RejectedPromise($reason);
107 });
108 });
109 }
110
111 public function cancel(): void
112 {
113 $this->cancelled = true;
114 $canceller = $this->canceller;
115 $this->canceller = null;
116
117 $parentCanceller = null;
118
119 if (null !== $this->result) {
120 // Forward cancellation to rejected promise to avoid reporting unhandled rejection
121 if ($this->result instanceof RejectedPromise) {
122 $this->result->cancel();
123 }
124
125 // Go up the promise chain and reach the top most promise which is
126 // itself not following another promise
127 $root = $this->unwrap($this->result);
128
129 // Return if the root promise is already resolved or a
130 // FulfilledPromise or RejectedPromise
131 if (!$root instanceof self || null !== $root->result) {
132 return;
133 }
134
135 $root->requiredCancelRequests--;
136
137 if ($root->requiredCancelRequests <= 0) {
138 $parentCanceller = [$root, 'cancel'];
139 }
140 }
141
142 if (null !== $canceller) {
143 $this->call($canceller);
144 }
145
146 // For BC, we call the parent canceller after our own canceller
147 if ($parentCanceller) {
148 $parentCanceller();
149 }
150 }
151
152 /**
153 * @deprecated 3.0.0 Use `catch()` instead
154 * @see self::catch()
155 */
156 public function otherwise(callable $onRejected): PromiseInterface
157 {
158 return $this->catch($onRejected);
159 }
160
161 /**
162 * @deprecated 3.0.0 Use `finally()` instead
163 * @see self::finally()
164 */
165 public function always(callable $onFulfilledOrRejected): PromiseInterface
166 {
167 return $this->finally($onFulfilledOrRejected);
168 }
169
170 private function resolver(?callable $onFulfilled = null, ?callable $onRejected = null): callable
171 {
172 return function (callable $resolve, callable $reject) use ($onFulfilled, $onRejected): void {
173 $this->handlers[] = static function (PromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject): void {
174 $promise = $promise->then($onFulfilled, $onRejected);
175
176 if ($promise instanceof self && $promise->result === null) {
177 $promise->handlers[] = static function (PromiseInterface $promise) use ($resolve, $reject): void {
178 $promise->then($resolve, $reject);
179 };
180 } else {
181 $promise->then($resolve, $reject);
182 }
183 };
184 };
185 }
186
187 private function reject(\Throwable $reason): void
188 {
189 if (null !== $this->result) {
190 return;
191 }
192
193 $this->settle(reject($reason));
194 }
195
196 /**
197 * @param PromiseInterface<T> $result
198 */
199 private function settle(PromiseInterface $result): void
200 {
201 $result = $this->unwrap($result);
202
203 if ($result === $this) {
204 $result = new RejectedPromise(
205 new \LogicException('Cannot resolve a promise with itself.')
206 );
207 }
208
209 if ($result instanceof self) {
210 $result->requiredCancelRequests++;
211 } else {
212 // Unset canceller only when not following a pending promise
213 $this->canceller = null;
214 }
215
216 $handlers = $this->handlers;
217
218 $this->handlers = [];
219 $this->result = $result;
220
221 foreach ($handlers as $handler) {
222 $handler($result);
223 }
224
225 // Forward cancellation to rejected promise to avoid reporting unhandled rejection
226 if ($this->cancelled && $result instanceof RejectedPromise) {
227 $result->cancel();
228 }
229 }
230
231 /**
232 * @param PromiseInterface<T> $promise
233 * @return PromiseInterface<T>
234 */
235 private function unwrap(PromiseInterface $promise): PromiseInterface
236 {
237 while ($promise instanceof self && null !== $promise->result) {
238 /** @var PromiseInterface<T> $promise */
239 $promise = $promise->result;
240 }
241
242 return $promise;
243 }
244
245 /**
246 * @param callable(callable(mixed):void,callable(\Throwable):void):void $cb
247 */
248 private function call(callable $cb): void
249 {
250 // Explicitly overwrite argument with null value. This ensure that this
251 // argument does not show up in the stack trace in PHP 7+ only.
252 $callback = $cb;
253 $cb = null;
254
255 // Use reflection to inspect number of arguments expected by this callback.
256 // We did some careful benchmarking here: Using reflection to avoid unneeded
257 // function arguments is actually faster than blindly passing them.
258 // Also, this helps avoiding unnecessary function arguments in the call stack
259 // if the callback creates an Exception (creating garbage cycles).
260 if (\is_array($callback)) {
261 $ref = new \ReflectionMethod($callback[0], $callback[1]);
262 } elseif (\is_object($callback) && !$callback instanceof \Closure) {
263 $ref = new \ReflectionMethod($callback, '__invoke');
264 } else {
265 assert($callback instanceof \Closure || \is_string($callback));
266 $ref = new \ReflectionFunction($callback);
267 }
268 $args = $ref->getNumberOfParameters();
269
270 try {
271 if ($args === 0) {
272 $callback();
273 } else {
274 // Keep references to this promise instance for the static resolve/reject functions.
275 // By using static callbacks that are not bound to this instance
276 // and passing the target promise instance by reference, we can
277 // still execute its resolving logic and still clear this
278 // reference when settling the promise. This helps avoiding
279 // garbage cycles if any callback creates an Exception.
280 // These assumptions are covered by the test suite, so if you ever feel like
281 // refactoring this, go ahead, any alternative suggestions are welcome!
282 $target =& $this;
283
284 $callback(
285 static function ($value) use (&$target): void {
286 if ($target !== null) {
287 $target->settle(resolve($value));
288 $target = null;
289 }
290 },
291 static function (\Throwable $reason) use (&$target): void {
292 if ($target !== null) {
293 $target->reject($reason);
294 $target = null;
295 }
296 }
297 );
298 }
299 } catch (\Throwable $e) {
300 $target = null;
301 $this->reject($e);
302 }
303 }
304}