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