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