1<?php 2 3namespace Sabre\Event; 4 5use Exception; 6 7/** 8 * An implementation of the Promise pattern. 9 * 10 * Promises basically allow you to avoid what is commonly called 'callback 11 * hell'. It allows for easily chaining of asynchronous operations. 12 * 13 * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/). 14 * @author Evert Pot (http://evertpot.com/) 15 * @license http://sabre.io/license/ Modified BSD License 16 */ 17class Promise { 18 19 /** 20 * Pending promise. No result yet. 21 */ 22 const PENDING = 0; 23 24 /** 25 * The promise has been fulfilled. It was successful. 26 */ 27 const FULFILLED = 1; 28 29 /** 30 * The promise was rejected. The operation failed. 31 */ 32 const REJECTED = 2; 33 34 /** 35 * The current state of this promise. 36 * 37 * @var int 38 */ 39 protected $state = self::PENDING; 40 41 /** 42 * A list of subscribers. Subscribers are the callbacks that want us to let 43 * them know if the callback was fulfilled or rejected. 44 * 45 * @var array 46 */ 47 protected $subscribers = []; 48 49 /** 50 * The result of the promise. 51 * 52 * If the promise was fulfilled, this will be the result value. If the 53 * promise was rejected, this is most commonly an exception. 54 * 55 * @var mixed 56 */ 57 protected $value = null; 58 59 /** 60 * Creates the promise. 61 * 62 * The passed argument is the executor. The executor is automatically 63 * called with two arguments. 64 * 65 * Each are callbacks that map to $this->fulfill and $this->reject. 66 * Using the executor is optional. 67 * 68 * @param callable $executor 69 */ 70 function __construct(callable $executor = null) { 71 72 if ($executor) { 73 $executor( 74 [$this, 'fulfill'], 75 [$this, 'reject'] 76 ); 77 } 78 79 } 80 81 /** 82 * This method allows you to specify the callback that will be called after 83 * the promise has been fulfilled or rejected. 84 * 85 * Both arguments are optional. 86 * 87 * This method returns a new promise, which can be used for chaining. 88 * If either the onFulfilled or onRejected callback is called, you may 89 * return a result from this callback. 90 * 91 * If the result of this callback is yet another promise, the result of 92 * _that_ promise will be used to set the result of the returned promise. 93 * 94 * If either of the callbacks return any other value, the returned promise 95 * is automatically fulfilled with that value. 96 * 97 * If either of the callbacks throw an exception, the returned promise will 98 * be rejected and the exception will be passed back. 99 * 100 * @param callable $onFulfilled 101 * @param callable $onRejected 102 * @return Promise 103 */ 104 function then(callable $onFulfilled = null, callable $onRejected = null) { 105 106 $subPromise = new self(); 107 switch ($this->state) { 108 case self::PENDING : 109 $this->subscribers[] = [$subPromise, $onFulfilled, $onRejected]; 110 break; 111 case self::FULFILLED : 112 $this->invokeCallback($subPromise, $onFulfilled); 113 break; 114 case self::REJECTED : 115 $this->invokeCallback($subPromise, $onRejected); 116 break; 117 } 118 return $subPromise; 119 120 } 121 122 /** 123 * Add a callback for when this promise is rejected. 124 * 125 * I would have used the word 'catch', but it's a reserved word in PHP, so 126 * we're not allowed to call our function that. 127 * 128 * @param callable $onRejected 129 * @return Promise 130 */ 131 function error(callable $onRejected) { 132 133 return $this->then(null, $onRejected); 134 135 } 136 137 /** 138 * Marks this promise as fulfilled and sets its return value. 139 * 140 * @param mixed $value 141 * @return void 142 */ 143 function fulfill($value = null) { 144 if ($this->state !== self::PENDING) { 145 throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); 146 } 147 $this->state = self::FULFILLED; 148 $this->value = $value; 149 foreach ($this->subscribers as $subscriber) { 150 $this->invokeCallback($subscriber[0], $subscriber[1]); 151 } 152 } 153 154 /** 155 * Marks this promise as rejected, and set it's rejection reason. 156 * 157 * @param mixed $reason 158 * @return void 159 */ 160 function reject($reason = null) { 161 if ($this->state !== self::PENDING) { 162 throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); 163 } 164 $this->state = self::REJECTED; 165 $this->value = $reason; 166 foreach ($this->subscribers as $subscriber) { 167 $this->invokeCallback($subscriber[0], $subscriber[2]); 168 } 169 170 } 171 172 /** 173 * It's possible to send an array of promises to the all method. This 174 * method returns a promise that will be fulfilled, only if all the passed 175 * promises are fulfilled. 176 * 177 * @param Promise[] $promises 178 * @return Promise 179 */ 180 static function all(array $promises) { 181 182 return new self(function($success, $fail) use ($promises) { 183 184 $successCount = 0; 185 $completeResult = []; 186 187 foreach ($promises as $promiseIndex => $subPromise) { 188 189 $subPromise->then( 190 function($result) use ($promiseIndex, &$completeResult, &$successCount, $success, $promises) { 191 $completeResult[$promiseIndex] = $result; 192 $successCount++; 193 if ($successCount === count($promises)) { 194 $success($completeResult); 195 } 196 return $result; 197 } 198 )->error( 199 function($reason) use ($fail) { 200 $fail($reason); 201 } 202 ); 203 204 } 205 }); 206 207 } 208 209 /** 210 * This method is used to call either an onFulfilled or onRejected callback. 211 * 212 * This method makes sure that the result of these callbacks are handled 213 * correctly, and any chained promises are also correctly fulfilled or 214 * rejected. 215 * 216 * @param Promise $subPromise 217 * @param callable $callBack 218 * @return void 219 */ 220 protected function invokeCallback(Promise $subPromise, callable $callBack = null) { 221 222 if (is_callable($callBack)) { 223 try { 224 $result = $callBack($this->value); 225 if ($result instanceof self) { 226 $result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']); 227 } else { 228 $subPromise->fulfill($result); 229 } 230 } catch (Exception $e) { 231 $subPromise->reject($e); 232 } 233 } else { 234 if ($this->state === self::FULFILLED) { 235 $subPromise->fulfill($this->value); 236 } else { 237 $subPromise->reject($this->value); 238 } 239 } 240 } 241 242 243} 244