functions.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <?php
  2. namespace React\Promise;
  3. use React\Promise\Exception\CompositeException;
  4. use React\Promise\Internal\FulfilledPromise;
  5. use React\Promise\Internal\RejectedPromise;
  6. /**
  7. * Creates a promise for the supplied `$promiseOrValue`.
  8. *
  9. * If `$promiseOrValue` is a value, it will be the resolution value of the
  10. * returned promise.
  11. *
  12. * If `$promiseOrValue` is a thenable (any object that provides a `then()` method),
  13. * a trusted promise that follows the state of the thenable is returned.
  14. *
  15. * If `$promiseOrValue` is a promise, it will be returned as is.
  16. *
  17. * @template T
  18. * @param PromiseInterface<T>|T $promiseOrValue
  19. * @return PromiseInterface<T>
  20. */
  21. function resolve($promiseOrValue): PromiseInterface
  22. {
  23. if ($promiseOrValue instanceof PromiseInterface) {
  24. return $promiseOrValue;
  25. }
  26. if (\is_object($promiseOrValue) && \method_exists($promiseOrValue, 'then')) {
  27. $canceller = null;
  28. if (\method_exists($promiseOrValue, 'cancel')) {
  29. $canceller = [$promiseOrValue, 'cancel'];
  30. assert(\is_callable($canceller));
  31. }
  32. /** @var Promise<T> */
  33. return new Promise(function (callable $resolve, callable $reject) use ($promiseOrValue): void {
  34. $promiseOrValue->then($resolve, $reject);
  35. }, $canceller);
  36. }
  37. return new FulfilledPromise($promiseOrValue);
  38. }
  39. /**
  40. * Creates a rejected promise for the supplied `$reason`.
  41. *
  42. * If `$reason` is a value, it will be the rejection value of the
  43. * returned promise.
  44. *
  45. * If `$reason` is a promise, its completion value will be the rejected
  46. * value of the returned promise.
  47. *
  48. * This can be useful in situations where you need to reject a promise without
  49. * throwing an exception. For example, it allows you to propagate a rejection with
  50. * the value of another promise.
  51. *
  52. * @return PromiseInterface<never>
  53. */
  54. function reject(\Throwable $reason): PromiseInterface
  55. {
  56. return new RejectedPromise($reason);
  57. }
  58. /**
  59. * Returns a promise that will resolve only once all the items in
  60. * `$promisesOrValues` have resolved. The resolution value of the returned promise
  61. * will be an array containing the resolution values of each of the items in
  62. * `$promisesOrValues`.
  63. *
  64. * @template T
  65. * @param iterable<PromiseInterface<T>|T> $promisesOrValues
  66. * @return PromiseInterface<array<T>>
  67. */
  68. function all(iterable $promisesOrValues): PromiseInterface
  69. {
  70. $cancellationQueue = new Internal\CancellationQueue();
  71. /** @var Promise<array<T>> */
  72. return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
  73. $toResolve = 0;
  74. /** @var bool */
  75. $continue = true;
  76. $values = [];
  77. foreach ($promisesOrValues as $i => $promiseOrValue) {
  78. $cancellationQueue->enqueue($promiseOrValue);
  79. $values[$i] = null;
  80. ++$toResolve;
  81. resolve($promiseOrValue)->then(
  82. function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void {
  83. $values[$i] = $value;
  84. if (0 === --$toResolve && !$continue) {
  85. $resolve($values);
  86. }
  87. },
  88. function (\Throwable $reason) use (&$continue, $reject): void {
  89. $continue = false;
  90. $reject($reason);
  91. }
  92. );
  93. if (!$continue && !\is_array($promisesOrValues)) {
  94. break;
  95. }
  96. }
  97. $continue = false;
  98. if ($toResolve === 0) {
  99. $resolve($values);
  100. }
  101. }, $cancellationQueue);
  102. }
  103. /**
  104. * Initiates a competitive race that allows one winner. Returns a promise which is
  105. * resolved in the same way the first settled promise resolves.
  106. *
  107. * The returned promise will become **infinitely pending** if `$promisesOrValues`
  108. * contains 0 items.
  109. *
  110. * @template T
  111. * @param iterable<PromiseInterface<T>|T> $promisesOrValues
  112. * @return PromiseInterface<T>
  113. */
  114. function race(iterable $promisesOrValues): PromiseInterface
  115. {
  116. $cancellationQueue = new Internal\CancellationQueue();
  117. /** @var Promise<T> */
  118. return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
  119. $continue = true;
  120. foreach ($promisesOrValues as $promiseOrValue) {
  121. $cancellationQueue->enqueue($promiseOrValue);
  122. resolve($promiseOrValue)->then($resolve, $reject)->finally(function () use (&$continue): void {
  123. $continue = false;
  124. });
  125. if (!$continue && !\is_array($promisesOrValues)) {
  126. break;
  127. }
  128. }
  129. }, $cancellationQueue);
  130. }
  131. /**
  132. * Returns a promise that will resolve when any one of the items in
  133. * `$promisesOrValues` resolves. The resolution value of the returned promise
  134. * will be the resolution value of the triggering item.
  135. *
  136. * The returned promise will only reject if *all* items in `$promisesOrValues` are
  137. * rejected. The rejection value will be an array of all rejection reasons.
  138. *
  139. * The returned promise will also reject with a `React\Promise\Exception\LengthException`
  140. * if `$promisesOrValues` contains 0 items.
  141. *
  142. * @template T
  143. * @param iterable<PromiseInterface<T>|T> $promisesOrValues
  144. * @return PromiseInterface<T>
  145. */
  146. function any(iterable $promisesOrValues): PromiseInterface
  147. {
  148. $cancellationQueue = new Internal\CancellationQueue();
  149. /** @var Promise<T> */
  150. return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
  151. $toReject = 0;
  152. $continue = true;
  153. $reasons = [];
  154. foreach ($promisesOrValues as $i => $promiseOrValue) {
  155. $cancellationQueue->enqueue($promiseOrValue);
  156. ++$toReject;
  157. resolve($promiseOrValue)->then(
  158. function ($value) use ($resolve, &$continue): void {
  159. $continue = false;
  160. $resolve($value);
  161. },
  162. function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continue): void {
  163. $reasons[$i] = $reason;
  164. if (0 === --$toReject && !$continue) {
  165. $reject(new CompositeException(
  166. $reasons,
  167. 'All promises rejected.'
  168. ));
  169. }
  170. }
  171. );
  172. if (!$continue && !\is_array($promisesOrValues)) {
  173. break;
  174. }
  175. }
  176. $continue = false;
  177. if ($toReject === 0 && !$reasons) {
  178. $reject(new Exception\LengthException(
  179. 'Must contain at least 1 item but contains only 0 items.'
  180. ));
  181. } elseif ($toReject === 0) {
  182. $reject(new CompositeException(
  183. $reasons,
  184. 'All promises rejected.'
  185. ));
  186. }
  187. }, $cancellationQueue);
  188. }
  189. /**
  190. * Sets the global rejection handler for unhandled promise rejections.
  191. *
  192. * Note that rejected promises should always be handled similar to how any
  193. * exceptions should always be caught in a `try` + `catch` block. If you remove
  194. * the last reference to a rejected promise that has not been handled, it will
  195. * report an unhandled promise rejection. See also the [`reject()` function](#reject)
  196. * for more details.
  197. *
  198. * The `?callable $callback` argument MUST be a valid callback function that
  199. * accepts a single `Throwable` argument or a `null` value to restore the
  200. * default promise rejection handler. The return value of the callback function
  201. * will be ignored and has no effect, so you SHOULD return a `void` value. The
  202. * callback function MUST NOT throw or the program will be terminated with a
  203. * fatal error.
  204. *
  205. * The function returns the previous rejection handler or `null` if using the
  206. * default promise rejection handler.
  207. *
  208. * The default promise rejection handler will log an error message plus its
  209. * stack trace:
  210. *
  211. * ```php
  212. * // Unhandled promise rejection with RuntimeException: Unhandled in example.php:2
  213. * React\Promise\reject(new RuntimeException('Unhandled'));
  214. * ```
  215. *
  216. * The promise rejection handler may be used to use customize the log message or
  217. * write to custom log targets. As a rule of thumb, this function should only be
  218. * used as a last resort and promise rejections are best handled with either the
  219. * [`then()` method](#promiseinterfacethen), the
  220. * [`catch()` method](#promiseinterfacecatch), or the
  221. * [`finally()` method](#promiseinterfacefinally).
  222. * See also the [`reject()` function](#reject) for more details.
  223. *
  224. * @param callable(\Throwable):void|null $callback
  225. * @return callable(\Throwable):void|null
  226. */
  227. function set_rejection_handler(?callable $callback): ?callable
  228. {
  229. static $current = null;
  230. $previous = $current;
  231. $current = $callback;
  232. return $previous;
  233. }
  234. /**
  235. * @internal
  236. */
  237. function _checkTypehint(callable $callback, \Throwable $reason): bool
  238. {
  239. if (\is_array($callback)) {
  240. $callbackReflection = new \ReflectionMethod($callback[0], $callback[1]);
  241. } elseif (\is_object($callback) && !$callback instanceof \Closure) {
  242. $callbackReflection = new \ReflectionMethod($callback, '__invoke');
  243. } else {
  244. assert($callback instanceof \Closure || \is_string($callback));
  245. $callbackReflection = new \ReflectionFunction($callback);
  246. }
  247. $parameters = $callbackReflection->getParameters();
  248. if (!isset($parameters[0])) {
  249. return true;
  250. }
  251. $expectedException = $parameters[0];
  252. // Extract the type of the argument and handle different possibilities
  253. $type = $expectedException->getType();
  254. $isTypeUnion = true;
  255. $types = [];
  256. switch (true) {
  257. case $type === null:
  258. break;
  259. case $type instanceof \ReflectionNamedType:
  260. $types = [$type];
  261. break;
  262. case $type instanceof \ReflectionIntersectionType:
  263. $isTypeUnion = false;
  264. case $type instanceof \ReflectionUnionType;
  265. $types = $type->getTypes();
  266. break;
  267. default:
  268. throw new \LogicException('Unexpected return value of ReflectionParameter::getType');
  269. }
  270. // If there is no type restriction, it matches
  271. if (empty($types)) {
  272. return true;
  273. }
  274. foreach ($types as $type) {
  275. if ($type instanceof \ReflectionIntersectionType) {
  276. foreach ($type->getTypes() as $typeToMatch) {
  277. assert($typeToMatch instanceof \ReflectionNamedType);
  278. $name = $typeToMatch->getName();
  279. if (!($matches = (!$typeToMatch->isBuiltin() && $reason instanceof $name))) {
  280. break;
  281. }
  282. }
  283. assert(isset($matches));
  284. } else {
  285. assert($type instanceof \ReflectionNamedType);
  286. $name = $type->getName();
  287. $matches = !$type->isBuiltin() && $reason instanceof $name;
  288. }
  289. // If we look for a single match (union), we can return early on match
  290. // If we look for a full match (intersection), we can return early on mismatch
  291. if ($matches) {
  292. if ($isTypeUnion) {
  293. return true;
  294. }
  295. } else {
  296. if (!$isTypeUnion) {
  297. return false;
  298. }
  299. }
  300. }
  301. // If we look for a single match (union) and did not return early, we matched no type and are false
  302. // If we look for a full match (intersection) and did not return early, we matched all types and are true
  303. return $isTypeUnion ? false : true;
  304. }