Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  
   3  namespace GuzzleHttp\Promise;
   4  
   5  /**
   6   * Promises/A+ implementation that avoids recursion when possible.
   7   *
   8   * @link https://promisesaplus.com/
   9   */
  10  class 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  }