Promise.php 10 KB

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