functions.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. <?php
  2. namespace React\Promise;
  3. /**
  4. * Creates a promise for the supplied `$promiseOrValue`.
  5. *
  6. * If `$promiseOrValue` is a value, it will be the resolution value of the
  7. * returned promise.
  8. *
  9. * If `$promiseOrValue` is a thenable (any object that provides a `then()` method),
  10. * a trusted promise that follows the state of the thenable is returned.
  11. *
  12. * If `$promiseOrValue` is a promise, it will be returned as is.
  13. *
  14. * @param mixed $promiseOrValue
  15. * @return PromiseInterface
  16. */
  17. function resolve($promiseOrValue = null)
  18. {
  19. if ($promiseOrValue instanceof ExtendedPromiseInterface) {
  20. return $promiseOrValue;
  21. }
  22. // Check is_object() first to avoid method_exists() triggering
  23. // class autoloaders if $promiseOrValue is a string.
  24. if (\is_object($promiseOrValue) && \method_exists($promiseOrValue, 'then')) {
  25. $canceller = null;
  26. if (\method_exists($promiseOrValue, 'cancel')) {
  27. $canceller = [$promiseOrValue, 'cancel'];
  28. }
  29. return new Promise(function ($resolve, $reject, $notify) use ($promiseOrValue) {
  30. $promiseOrValue->then($resolve, $reject, $notify);
  31. }, $canceller);
  32. }
  33. return new FulfilledPromise($promiseOrValue);
  34. }
  35. /**
  36. * Creates a rejected promise for the supplied `$promiseOrValue`.
  37. *
  38. * If `$promiseOrValue` is a value, it will be the rejection value of the
  39. * returned promise.
  40. *
  41. * If `$promiseOrValue` is a promise, its completion value will be the rejected
  42. * value of the returned promise.
  43. *
  44. * This can be useful in situations where you need to reject a promise without
  45. * throwing an exception. For example, it allows you to propagate a rejection with
  46. * the value of another promise.
  47. *
  48. * @param mixed $promiseOrValue
  49. * @return PromiseInterface
  50. */
  51. function reject($promiseOrValue = null)
  52. {
  53. if ($promiseOrValue instanceof PromiseInterface) {
  54. return resolve($promiseOrValue)->then(function ($value) {
  55. return new RejectedPromise($value);
  56. });
  57. }
  58. return new RejectedPromise($promiseOrValue);
  59. }
  60. /**
  61. * Returns a promise that will resolve only once all the items in
  62. * `$promisesOrValues` have resolved. The resolution value of the returned promise
  63. * will be an array containing the resolution values of each of the items in
  64. * `$promisesOrValues`.
  65. *
  66. * @param array $promisesOrValues
  67. * @return PromiseInterface
  68. */
  69. function all($promisesOrValues)
  70. {
  71. return map($promisesOrValues, function ($val) {
  72. return $val;
  73. });
  74. }
  75. /**
  76. * Initiates a competitive race that allows one winner. Returns a promise which is
  77. * resolved in the same way the first settled promise resolves.
  78. *
  79. * The returned promise will become **infinitely pending** if `$promisesOrValues`
  80. * contains 0 items.
  81. *
  82. * @param array $promisesOrValues
  83. * @return PromiseInterface
  84. */
  85. function race($promisesOrValues)
  86. {
  87. $cancellationQueue = new CancellationQueue();
  88. $cancellationQueue->enqueue($promisesOrValues);
  89. return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $cancellationQueue) {
  90. resolve($promisesOrValues)
  91. ->done(function ($array) use ($cancellationQueue, $resolve, $reject, $notify) {
  92. if (!is_array($array) || !$array) {
  93. $resolve();
  94. return;
  95. }
  96. foreach ($array as $promiseOrValue) {
  97. $cancellationQueue->enqueue($promiseOrValue);
  98. resolve($promiseOrValue)
  99. ->done($resolve, $reject, $notify);
  100. }
  101. }, $reject, $notify);
  102. }, $cancellationQueue);
  103. }
  104. /**
  105. * Returns a promise that will resolve when any one of the items in
  106. * `$promisesOrValues` resolves. The resolution value of the returned promise
  107. * will be the resolution value of the triggering item.
  108. *
  109. * The returned promise will only reject if *all* items in `$promisesOrValues` are
  110. * rejected. The rejection value will be an array of all rejection reasons.
  111. *
  112. * The returned promise will also reject with a `React\Promise\Exception\LengthException`
  113. * if `$promisesOrValues` contains 0 items.
  114. *
  115. * @param array $promisesOrValues
  116. * @return PromiseInterface
  117. */
  118. function any($promisesOrValues)
  119. {
  120. return some($promisesOrValues, 1)
  121. ->then(function ($val) {
  122. return \array_shift($val);
  123. });
  124. }
  125. /**
  126. * Returns a promise that will resolve when `$howMany` of the supplied items in
  127. * `$promisesOrValues` resolve. The resolution value of the returned promise
  128. * will be an array of length `$howMany` containing the resolution values of the
  129. * triggering items.
  130. *
  131. * The returned promise will reject if it becomes impossible for `$howMany` items
  132. * to resolve (that is, when `(count($promisesOrValues) - $howMany) + 1` items
  133. * reject). The rejection value will be an array of
  134. * `(count($promisesOrValues) - $howMany) + 1` rejection reasons.
  135. *
  136. * The returned promise will also reject with a `React\Promise\Exception\LengthException`
  137. * if `$promisesOrValues` contains less items than `$howMany`.
  138. *
  139. * @param array $promisesOrValues
  140. * @param int $howMany
  141. * @return PromiseInterface
  142. */
  143. function some($promisesOrValues, $howMany)
  144. {
  145. $cancellationQueue = new CancellationQueue();
  146. $cancellationQueue->enqueue($promisesOrValues);
  147. return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $howMany, $cancellationQueue) {
  148. resolve($promisesOrValues)
  149. ->done(function ($array) use ($howMany, $cancellationQueue, $resolve, $reject, $notify) {
  150. if (!\is_array($array) || $howMany < 1) {
  151. $resolve([]);
  152. return;
  153. }
  154. $len = \count($array);
  155. if ($len < $howMany) {
  156. throw new Exception\LengthException(
  157. \sprintf(
  158. 'Input array must contain at least %d item%s but contains only %s item%s.',
  159. $howMany,
  160. 1 === $howMany ? '' : 's',
  161. $len,
  162. 1 === $len ? '' : 's'
  163. )
  164. );
  165. }
  166. $toResolve = $howMany;
  167. $toReject = ($len - $toResolve) + 1;
  168. $values = [];
  169. $reasons = [];
  170. foreach ($array as $i => $promiseOrValue) {
  171. $fulfiller = function ($val) use ($i, &$values, &$toResolve, $toReject, $resolve) {
  172. if ($toResolve < 1 || $toReject < 1) {
  173. return;
  174. }
  175. $values[$i] = $val;
  176. if (0 === --$toResolve) {
  177. $resolve($values);
  178. }
  179. };
  180. $rejecter = function ($reason) use ($i, &$reasons, &$toReject, $toResolve, $reject) {
  181. if ($toResolve < 1 || $toReject < 1) {
  182. return;
  183. }
  184. $reasons[$i] = $reason;
  185. if (0 === --$toReject) {
  186. $reject($reasons);
  187. }
  188. };
  189. $cancellationQueue->enqueue($promiseOrValue);
  190. resolve($promiseOrValue)
  191. ->done($fulfiller, $rejecter, $notify);
  192. }
  193. }, $reject, $notify);
  194. }, $cancellationQueue);
  195. }
  196. /**
  197. * Traditional map function, similar to `array_map()`, but allows input to contain
  198. * promises and/or values, and `$mapFunc` may return either a value or a promise.
  199. *
  200. * The map function receives each item as argument, where item is a fully resolved
  201. * value of a promise or value in `$promisesOrValues`.
  202. *
  203. * @param array $promisesOrValues
  204. * @param callable $mapFunc
  205. * @return PromiseInterface
  206. */
  207. function map($promisesOrValues, callable $mapFunc)
  208. {
  209. $cancellationQueue = new CancellationQueue();
  210. $cancellationQueue->enqueue($promisesOrValues);
  211. return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $mapFunc, $cancellationQueue) {
  212. resolve($promisesOrValues)
  213. ->done(function ($array) use ($mapFunc, $cancellationQueue, $resolve, $reject, $notify) {
  214. if (!\is_array($array) || !$array) {
  215. $resolve([]);
  216. return;
  217. }
  218. $toResolve = \count($array);
  219. $values = [];
  220. foreach ($array as $i => $promiseOrValue) {
  221. $cancellationQueue->enqueue($promiseOrValue);
  222. $values[$i] = null;
  223. resolve($promiseOrValue)
  224. ->then($mapFunc)
  225. ->done(
  226. function ($mapped) use ($i, &$values, &$toResolve, $resolve) {
  227. $values[$i] = $mapped;
  228. if (0 === --$toResolve) {
  229. $resolve($values);
  230. }
  231. },
  232. $reject,
  233. $notify
  234. );
  235. }
  236. }, $reject, $notify);
  237. }, $cancellationQueue);
  238. }
  239. /**
  240. * Traditional reduce function, similar to `array_reduce()`, but input may contain
  241. * promises and/or values, and `$reduceFunc` may return either a value or a
  242. * promise, *and* `$initialValue` may be a promise or a value for the starting
  243. * value.
  244. *
  245. * @param array $promisesOrValues
  246. * @param callable $reduceFunc
  247. * @param mixed $initialValue
  248. * @return PromiseInterface
  249. */
  250. function reduce($promisesOrValues, callable $reduceFunc, $initialValue = null)
  251. {
  252. $cancellationQueue = new CancellationQueue();
  253. $cancellationQueue->enqueue($promisesOrValues);
  254. return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $reduceFunc, $initialValue, $cancellationQueue) {
  255. resolve($promisesOrValues)
  256. ->done(function ($array) use ($reduceFunc, $initialValue, $cancellationQueue, $resolve, $reject, $notify) {
  257. if (!\is_array($array)) {
  258. $array = [];
  259. }
  260. $total = \count($array);
  261. $i = 0;
  262. // Wrap the supplied $reduceFunc with one that handles promises and then
  263. // delegates to the supplied.
  264. $wrappedReduceFunc = function ($current, $val) use ($reduceFunc, $cancellationQueue, $total, &$i) {
  265. $cancellationQueue->enqueue($val);
  266. return $current
  267. ->then(function ($c) use ($reduceFunc, $total, &$i, $val) {
  268. return resolve($val)
  269. ->then(function ($value) use ($reduceFunc, $total, &$i, $c) {
  270. return $reduceFunc($c, $value, $i++, $total);
  271. });
  272. });
  273. };
  274. $cancellationQueue->enqueue($initialValue);
  275. \array_reduce($array, $wrappedReduceFunc, resolve($initialValue))
  276. ->done($resolve, $reject, $notify);
  277. }, $reject, $notify);
  278. }, $cancellationQueue);
  279. }
  280. /**
  281. * @internal
  282. */
  283. function _checkTypehint(callable $callback, $object)
  284. {
  285. if (!\is_object($object)) {
  286. return true;
  287. }
  288. if (\is_array($callback)) {
  289. $callbackReflection = new \ReflectionMethod($callback[0], $callback[1]);
  290. } elseif (\is_object($callback) && !$callback instanceof \Closure) {
  291. $callbackReflection = new \ReflectionMethod($callback, '__invoke');
  292. } else {
  293. $callbackReflection = new \ReflectionFunction($callback);
  294. }
  295. $parameters = $callbackReflection->getParameters();
  296. if (!isset($parameters[0])) {
  297. return true;
  298. }
  299. $expectedException = $parameters[0];
  300. // PHP before v8 used an easy API:
  301. if (\PHP_VERSION_ID < 70100 || \defined('HHVM_VERSION')) {
  302. if (!$expectedException->getClass()) {
  303. return true;
  304. }
  305. return $expectedException->getClass()->isInstance($object);
  306. }
  307. // Extract the type of the argument and handle different possibilities
  308. $type = $expectedException->getType();
  309. $isTypeUnion = true;
  310. $types = [];
  311. switch (true) {
  312. case $type === null:
  313. break;
  314. case $type instanceof \ReflectionNamedType:
  315. $types = [$type];
  316. break;
  317. case $type instanceof \ReflectionIntersectionType:
  318. $isTypeUnion = false;
  319. case $type instanceof \ReflectionUnionType;
  320. $types = $type->getTypes();
  321. break;
  322. default:
  323. throw new \LogicException('Unexpected return value of ReflectionParameter::getType');
  324. }
  325. // If there is no type restriction, it matches
  326. if (empty($types)) {
  327. return true;
  328. }
  329. foreach ($types as $type) {
  330. if (!$type instanceof \ReflectionNamedType) {
  331. throw new \LogicException('This implementation does not support groups of intersection or union types');
  332. }
  333. // A named-type can be either a class-name or a built-in type like string, int, array, etc.
  334. $matches = ($type->isBuiltin() && \gettype($object) === $type->getName())
  335. || (new \ReflectionClass($type->getName()))->isInstance($object);
  336. // If we look for a single match (union), we can return early on match
  337. // If we look for a full match (intersection), we can return early on mismatch
  338. if ($matches) {
  339. if ($isTypeUnion) {
  340. return true;
  341. }
  342. } else {
  343. if (!$isTypeUnion) {
  344. return false;
  345. }
  346. }
  347. }
  348. // If we look for a single match (union) and did not return early, we matched no type and are false
  349. // If we look for a full match (intersection) and did not return early, we matched all types and are true
  350. return $isTypeUnion ? false : true;
  351. }