1<?php 2 3namespace Sabre\Event; 4 5use Exception; 6 7/** 8 * An implementation of the Promise pattern. 9 * 10 * A promise represents the result of an asynchronous operation. 11 * At any given point a promise can be in one of three states: 12 * 13 * 1. Pending (the promise does not have a result yet). 14 * 2. Fulfilled (the asynchronous operation has completed with a result). 15 * 3. Rejected (the asynchronous operation has completed with an error). 16 * 17 * To get a callback when the operation has finished, use the `then` method. 18 * 19 * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/). 20 * @author Evert Pot (http://evertpot.com/) 21 * @license http://sabre.io/license/ Modified BSD License 22 */ 23class Promise { 24 25 /** 26 * The asynchronous operation is pending. 27 */ 28 const PENDING = 0; 29 30 /** 31 * The asynchronous operation has completed, and has a result. 32 */ 33 const FULFILLED = 1; 34 35 /** 36 * The asynchronous operation has completed with an error. 37 */ 38 const REJECTED = 2; 39 40 /** 41 * The current state of this promise. 42 * 43 * @var int 44 */ 45 public $state = self::PENDING; 46 47 /** 48 * Creates the promise. 49 * 50 * The passed argument is the executor. The executor is automatically 51 * called with two arguments. 52 * 53 * Each are callbacks that map to $this->fulfill and $this->reject. 54 * Using the executor is optional. 55 * 56 * @param callable $executor 57 */ 58 function __construct(callable $executor = null) { 59 60 if ($executor) { 61 $executor( 62 [$this, 'fulfill'], 63 [$this, 'reject'] 64 ); 65 } 66 67 } 68 69 /** 70 * This method allows you to specify the callback that will be called after 71 * the promise has been fulfilled or rejected. 72 * 73 * Both arguments are optional. 74 * 75 * This method returns a new promise, which can be used for chaining. 76 * If either the onFulfilled or onRejected callback is called, you may 77 * return a result from this callback. 78 * 79 * If the result of this callback is yet another promise, the result of 80 * _that_ promise will be used to set the result of the returned promise. 81 * 82 * If either of the callbacks return any other value, the returned promise 83 * is automatically fulfilled with that value. 84 * 85 * If either of the callbacks throw an exception, the returned promise will 86 * be rejected and the exception will be passed back. 87 * 88 * @param callable $onFulfilled 89 * @param callable $onRejected 90 * @return Promise 91 */ 92 function then(callable $onFulfilled = null, callable $onRejected = null) { 93 94 // This new subPromise will be returned from this function, and will 95 // be fulfilled with the result of the onFulfilled or onRejected event 96 // handlers. 97 $subPromise = new self(); 98 99 switch ($this->state) { 100 case self::PENDING : 101 // The operation is pending, so we keep a reference to the 102 // event handlers so we can call them later. 103 $this->subscribers[] = [$subPromise, $onFulfilled, $onRejected]; 104 break; 105 case self::FULFILLED : 106 // The async operation is already fulfilled, so we trigger the 107 // onFulfilled callback asap. 108 $this->invokeCallback($subPromise, $onFulfilled); 109 break; 110 case self::REJECTED : 111 // The async operation failed, so we call teh onRejected 112 // callback asap. 113 $this->invokeCallback($subPromise, $onRejected); 114 break; 115 } 116 return $subPromise; 117 118 } 119 120 /** 121 * Add a callback for when this promise is rejected. 122 * 123 * Its usage is identical to then(). However, the otherwise() function is 124 * preferred. 125 * 126 * @param callable $onRejected 127 * @return Promise 128 */ 129 function otherwise(callable $onRejected) { 130 131 return $this->then(null, $onRejected); 132 133 } 134 135 /** 136 * Marks this promise as fulfilled and sets its return value. 137 * 138 * @param mixed $value 139 * @return void 140 */ 141 function fulfill($value = null) { 142 if ($this->state !== self::PENDING) { 143 throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); 144 } 145 $this->state = self::FULFILLED; 146 $this->value = $value; 147 foreach ($this->subscribers as $subscriber) { 148 $this->invokeCallback($subscriber[0], $subscriber[1]); 149 } 150 } 151 152 /** 153 * Marks this promise as rejected, and set it's rejection reason. 154 * 155 * While it's possible to use any PHP value as the reason, it's highly 156 * recommended to use an Exception for this. 157 * 158 * @param mixed $reason 159 * @return void 160 */ 161 function reject($reason = null) { 162 if ($this->state !== self::PENDING) { 163 throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); 164 } 165 $this->state = self::REJECTED; 166 $this->value = $reason; 167 foreach ($this->subscribers as $subscriber) { 168 $this->invokeCallback($subscriber[0], $subscriber[2]); 169 } 170 171 } 172 173 /** 174 * Stops execution until this promise is resolved. 175 * 176 * This method stops exection completely. If the promise is successful with 177 * a value, this method will return this value. If the promise was 178 * rejected, this method will throw an exception. 179 * 180 * This effectively turns the asynchronous operation into a synchronous 181 * one. In PHP it might be useful to call this on the last promise in a 182 * chain. 183 * 184 * @throws Exception 185 * @return mixed 186 */ 187 function wait() { 188 189 $hasEvents = true; 190 while ($this->state === self::PENDING) { 191 192 if (!$hasEvents) { 193 throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); 194 } 195 196 // As long as the promise is not fulfilled, we tell the event loop 197 // to handle events, and to block. 198 $hasEvents = Loop\tick(true); 199 200 } 201 202 if ($this->state === self::FULFILLED) { 203 // If the state of this promise is fulfilled, we can return the value. 204 return $this->value; 205 } else { 206 // If we got here, it means that the asynchronous operation 207 // errored. Therefore we need to throw an exception. 208 $reason = $this->value; 209 if ($reason instanceof Exception) { 210 throw $reason; 211 } elseif (is_scalar($reason)) { 212 throw new Exception($reason); 213 } else { 214 $type = is_object($reason) ? get_class($reason) : gettype($reason); 215 throw new Exception('Promise was rejected with reason of type: ' . $type); 216 } 217 } 218 219 220 } 221 222 223 /** 224 * A list of subscribers. Subscribers are the callbacks that want us to let 225 * them know if the callback was fulfilled or rejected. 226 * 227 * @var array 228 */ 229 protected $subscribers = []; 230 231 /** 232 * The result of the promise. 233 * 234 * If the promise was fulfilled, this will be the result value. If the 235 * promise was rejected, this property hold the rejection reason. 236 * 237 * @var mixed 238 */ 239 protected $value = null; 240 241 /** 242 * This method is used to call either an onFulfilled or onRejected callback. 243 * 244 * This method makes sure that the result of these callbacks are handled 245 * correctly, and any chained promises are also correctly fulfilled or 246 * rejected. 247 * 248 * @param Promise $subPromise 249 * @param callable $callBack 250 * @return void 251 */ 252 private function invokeCallback(Promise $subPromise, callable $callBack = null) { 253 254 // We use 'nextTick' to ensure that the event handlers are always 255 // triggered outside of the calling stack in which they were originally 256 // passed to 'then'. 257 // 258 // This makes the order of execution more predictable. 259 Loop\nextTick(function() use ($callBack, $subPromise) { 260 if (is_callable($callBack)) { 261 try { 262 263 $result = $callBack($this->value); 264 if ($result instanceof self) { 265 // If the callback (onRejected or onFulfilled) 266 // returned a promise, we only fulfill or reject the 267 // chained promise once that promise has also been 268 // resolved. 269 $result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']); 270 } else { 271 // If the callback returned any other value, we 272 // immediately fulfill the chained promise. 273 $subPromise->fulfill($result); 274 } 275 } catch (Exception $e) { 276 // If the event handler threw an exception, we need to make sure that 277 // the chained promise is rejected as well. 278 $subPromise->reject($e); 279 } 280 } else { 281 if ($this->state === self::FULFILLED) { 282 $subPromise->fulfill($this->value); 283 } else { 284 $subPromise->reject($this->value); 285 } 286 } 287 }); 288 } 289 290 /** 291 * Alias for 'otherwise'. 292 * 293 * This function is now deprecated and will be removed in a future version. 294 * 295 * @param callable $onRejected 296 * @deprecated 297 * @return Promise 298 */ 299 function error(callable $onRejected) { 300 301 return $this->otherwise($onRejected); 302 303 } 304 305 /** 306 * Deprecated. 307 * 308 * Please use Sabre\Event\Promise::all 309 * 310 * @param Promise[] $promises 311 * @deprecated 312 * @return Promise 313 */ 314 static function all(array $promises) { 315 316 return Promise\all($promises); 317 318 } 319 320} 321