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