1<?php 2 3namespace GuzzleHttp\Promise; 4 5/** 6 * Promises/A+ implementation that avoids recursion when possible. 7 * 8 * @link https://promisesaplus.com/ 9 */ 10class Promise implements PromiseInterface 11{ 12 private $state = self::PENDING; 13 private $result; 14 private $cancelFn; 15 private $waitFn; 16 private $waitList; 17 private $handlers = []; 18 19 /** 20 * @param callable $waitFn Fn that when invoked resolves the promise. 21 * @param callable $cancelFn Fn that when invoked cancels the promise. 22 */ 23 public function __construct( 24 callable $waitFn = null, 25 callable $cancelFn = null 26 ) { 27 $this->waitFn = $waitFn; 28 $this->cancelFn = $cancelFn; 29 } 30 31 public function then( 32 callable $onFulfilled = null, 33 callable $onRejected = null 34 ) { 35 if ($this->state === self::PENDING) { 36 $p = new Promise(null, [$this, 'cancel']); 37 $this->handlers[] = [$p, $onFulfilled, $onRejected]; 38 $p->waitList = $this->waitList; 39 $p->waitList[] = $this; 40 return $p; 41 } 42 43 // Return a fulfilled promise and immediately invoke any callbacks. 44 if ($this->state === self::FULFILLED) { 45 $promise = Create::promiseFor($this->result); 46 return $onFulfilled ? $promise->then($onFulfilled) : $promise; 47 } 48 49 // It's either cancelled or rejected, so return a rejected promise 50 // and immediately invoke any callbacks. 51 $rejection = Create::rejectionFor($this->result); 52 return $onRejected ? $rejection->then(null, $onRejected) : $rejection; 53 } 54 55 public function otherwise(callable $onRejected) 56 { 57 return $this->then(null, $onRejected); 58 } 59 60 public function wait($unwrap = true) 61 { 62 $this->waitIfPending(); 63 64 if ($this->result instanceof PromiseInterface) { 65 return $this->result->wait($unwrap); 66 } 67 if ($unwrap) { 68 if ($this->state === self::FULFILLED) { 69 return $this->result; 70 } 71 // It's rejected so "unwrap" and throw an exception. 72 throw Create::exceptionFor($this->result); 73 } 74 } 75 76 public function getState() 77 { 78 return $this->state; 79 } 80 81 public function cancel() 82 { 83 if ($this->state !== self::PENDING) { 84 return; 85 } 86 87 $this->waitFn = $this->waitList = null; 88 89 if ($this->cancelFn) { 90 $fn = $this->cancelFn; 91 $this->cancelFn = null; 92 try { 93 $fn(); 94 } catch (\Throwable $e) { 95 $this->reject($e); 96 } catch (\Exception $e) { 97 $this->reject($e); 98 } 99 } 100 101 // Reject the promise only if it wasn't rejected in a then callback. 102 /** @psalm-suppress RedundantCondition */ 103 if ($this->state === self::PENDING) { 104 $this->reject(new CancellationException('Promise has been cancelled')); 105 } 106 } 107 108 public function resolve($value) 109 { 110 $this->settle(self::FULFILLED, $value); 111 } 112 113 public function reject($reason) 114 { 115 $this->settle(self::REJECTED, $reason); 116 } 117 118 private function settle($state, $value) 119 { 120 if ($this->state !== self::PENDING) { 121 // Ignore calls with the same resolution. 122 if ($state === $this->state && $value === $this->result) { 123 return; 124 } 125 throw $this->state === $state 126 ? new \LogicException("The promise is already {$state}.") 127 : new \LogicException("Cannot change a {$this->state} promise to {$state}"); 128 } 129 130 if ($value === $this) { 131 throw new \LogicException('Cannot fulfill or reject a promise with itself'); 132 } 133 134 // Clear out the state of the promise but stash the handlers. 135 $this->state = $state; 136 $this->result = $value; 137 $handlers = $this->handlers; 138 $this->handlers = null; 139 $this->waitList = $this->waitFn = null; 140 $this->cancelFn = null; 141 142 if (!$handlers) { 143 return; 144 } 145 146 // If the value was not a settled promise or a thenable, then resolve 147 // it in the task queue using the correct ID. 148 if (!is_object($value) || !method_exists($value, 'then')) { 149 $id = $state === self::FULFILLED ? 1 : 2; 150 // It's a success, so resolve the handlers in the queue. 151 Utils::queue()->add(static function () use ($id, $value, $handlers) { 152 foreach ($handlers as $handler) { 153 self::callHandler($id, $value, $handler); 154 } 155 }); 156 } elseif ($value instanceof Promise && Is::pending($value)) { 157 // We can just merge our handlers onto the next promise. 158 $value->handlers = array_merge($value->handlers, $handlers); 159 } else { 160 // Resolve the handlers when the forwarded promise is resolved. 161 $value->then( 162 static function ($value) use ($handlers) { 163 foreach ($handlers as $handler) { 164 self::callHandler(1, $value, $handler); 165 } 166 }, 167 static function ($reason) use ($handlers) { 168 foreach ($handlers as $handler) { 169 self::callHandler(2, $reason, $handler); 170 } 171 } 172 ); 173 } 174 } 175 176 /** 177 * Call a stack of handlers using a specific callback index and value. 178 * 179 * @param int $index 1 (resolve) or 2 (reject). 180 * @param mixed $value Value to pass to the callback. 181 * @param array $handler Array of handler data (promise and callbacks). 182 */ 183 private static function callHandler($index, $value, array $handler) 184 { 185 /** @var PromiseInterface $promise */ 186 $promise = $handler[0]; 187 188 // The promise may have been cancelled or resolved before placing 189 // this thunk in the queue. 190 if (Is::settled($promise)) { 191 return; 192 } 193 194 try { 195 if (isset($handler[$index])) { 196 /* 197 * If $f throws an exception, then $handler will be in the exception 198 * stack trace. Since $handler contains a reference to the callable 199 * itself we get a circular reference. We clear the $handler 200 * here to avoid that memory leak. 201 */ 202 $f = $handler[$index]; 203 unset($handler); 204 $promise->resolve($f($value)); 205 } elseif ($index === 1) { 206 // Forward resolution values as-is. 207 $promise->resolve($value); 208 } else { 209 // Forward rejections down the chain. 210 $promise->reject($value); 211 } 212 } catch (\Throwable $reason) { 213 $promise->reject($reason); 214 } catch (\Exception $reason) { 215 $promise->reject($reason); 216 } 217 } 218 219 private function waitIfPending() 220 { 221 if ($this->state !== self::PENDING) { 222 return; 223 } elseif ($this->waitFn) { 224 $this->invokeWaitFn(); 225 } elseif ($this->waitList) { 226 $this->invokeWaitList(); 227 } else { 228 // If there's no wait function, then reject the promise. 229 $this->reject('Cannot wait on a promise that has ' 230 . 'no internal wait function. You must provide a wait ' 231 . 'function when constructing the promise to be able to ' 232 . 'wait on a promise.'); 233 } 234 235 Utils::queue()->run(); 236 237 /** @psalm-suppress RedundantCondition */ 238 if ($this->state === self::PENDING) { 239 $this->reject('Invoking the wait callback did not resolve the promise'); 240 } 241 } 242 243 private function invokeWaitFn() 244 { 245 try { 246 $wfn = $this->waitFn; 247 $this->waitFn = null; 248 $wfn(true); 249 } catch (\Exception $reason) { 250 if ($this->state === self::PENDING) { 251 // The promise has not been resolved yet, so reject the promise 252 // with the exception. 253 $this->reject($reason); 254 } else { 255 // The promise was already resolved, so there's a problem in 256 // the application. 257 throw $reason; 258 } 259 } 260 } 261 262 private function invokeWaitList() 263 { 264 $waitList = $this->waitList; 265 $this->waitList = null; 266 267 foreach ($waitList as $result) { 268 do { 269 $result->waitIfPending(); 270 $result = $result->result; 271 } while ($result instanceof Promise); 272 273 if ($result instanceof PromiseInterface) { 274 $result->wait(false); 275 } 276 } 277 } 278} 279