canceller = $canceller; // Explicitly overwrite arguments with null values before invoking // resolver function. This ensure that these arguments do not show up // in the stack trace in PHP 7+ only. $cb = $resolver; $resolver = $canceller = null; $this->call($cb); } public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null !== $this->result) { return $this->result->then($onFulfilled, $onRejected, $onProgress); } if (null === $this->canceller) { return new static($this->resolver($onFulfilled, $onRejected, $onProgress)); } // This promise has a canceller, so we create a new child promise which // has a canceller that invokes the parent canceller if all other // followers are also cancelled. We keep a reference to this promise // instance for the static canceller function and clear this to avoid // keeping a cyclic reference between parent and follower. $parent = $this; ++$parent->requiredCancelRequests; return new static( $this->resolver($onFulfilled, $onRejected, $onProgress), static function () use (&$parent) { if (++$parent->cancelRequests >= $parent->requiredCancelRequests) { $parent->cancel(); } $parent = null; } ); } public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null !== $this->result) { return $this->result->done($onFulfilled, $onRejected, $onProgress); } $this->handlers[] = static function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected) { $promise ->done($onFulfilled, $onRejected); }; if ($onProgress) { $this->progressHandlers[] = $onProgress; } } public function otherwise(callable $onRejected) { return $this->then(null, static function ($reason) use ($onRejected) { if (!_checkTypehint($onRejected, $reason)) { return new RejectedPromise($reason); } return $onRejected($reason); }); } public function always(callable $onFulfilledOrRejected) { return $this->then(static function ($value) use ($onFulfilledOrRejected) { return resolve($onFulfilledOrRejected())->then(function () use ($value) { return $value; }); }, static function ($reason) use ($onFulfilledOrRejected) { return resolve($onFulfilledOrRejected())->then(function () use ($reason) { return new RejectedPromise($reason); }); }); } public function progress(callable $onProgress) { return $this->then(null, null, $onProgress); } public function cancel() { if (null === $this->canceller || null !== $this->result) { return; } $canceller = $this->canceller; $this->canceller = null; $this->call($canceller); } private function resolver(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { return function ($resolve, $reject, $notify) use ($onFulfilled, $onRejected, $onProgress) { if ($onProgress) { $progressHandler = static function ($update) use ($notify, $onProgress) { try { $notify($onProgress($update)); } catch (\Throwable $e) { $notify($e); } catch (\Exception $e) { $notify($e); } }; } else { $progressHandler = $notify; } $this->handlers[] = static function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject, $progressHandler) { $promise ->then($onFulfilled, $onRejected) ->done($resolve, $reject, $progressHandler); }; $this->progressHandlers[] = $progressHandler; }; } private function reject($reason = null) { if (null !== $this->result) { return; } $this->settle(reject($reason)); } private function settle(ExtendedPromiseInterface $promise) { $promise = $this->unwrap($promise); if ($promise === $this) { $promise = new RejectedPromise( new \LogicException('Cannot resolve a promise with itself.') ); } $handlers = $this->handlers; $this->progressHandlers = $this->handlers = []; $this->result = $promise; $this->canceller = null; foreach ($handlers as $handler) { $handler($promise); } } private function unwrap($promise) { $promise = $this->extract($promise); while ($promise instanceof self && null !== $promise->result) { $promise = $this->extract($promise->result); } return $promise; } private function extract($promise) { if ($promise instanceof LazyPromise) { $promise = $promise->promise(); } return $promise; } private function call(callable $cb) { // Explicitly overwrite argument with null value. This ensure that this // argument does not show up in the stack trace in PHP 7+ only. $callback = $cb; $cb = null; // Use reflection to inspect number of arguments expected by this callback. // We did some careful benchmarking here: Using reflection to avoid unneeded // function arguments is actually faster than blindly passing them. // Also, this helps avoiding unnecessary function arguments in the call stack // if the callback creates an Exception (creating garbage cycles). if (is_array($callback)) { $ref = new \ReflectionMethod($callback[0], $callback[1]); } elseif (is_object($callback) && !$callback instanceof \Closure) { $ref = new \ReflectionMethod($callback, '__invoke'); } else { $ref = new \ReflectionFunction($callback); } $args = $ref->getNumberOfParameters(); try { if ($args === 0) { $callback(); } else { // Keep references to this promise instance for the static resolve/reject functions. // By using static callbacks that are not bound to this instance // and passing the target promise instance by reference, we can // still execute its resolving logic and still clear this // reference when settling the promise. This helps avoiding // garbage cycles if any callback creates an Exception. // These assumptions are covered by the test suite, so if you ever feel like // refactoring this, go ahead, any alternative suggestions are welcome! $target =& $this; $progressHandlers =& $this->progressHandlers; $callback( static function ($value = null) use (&$target) { if ($target !== null) { $target->settle(resolve($value)); $target = null; } }, static function ($reason = null) use (&$target) { if ($target !== null) { $target->reject($reason); $target = null; } }, static function ($update = null) use (&$progressHandlers) { foreach ($progressHandlers as $handler) { $handler($update); } } ); } } catch (\Throwable $e) { $target = null; $this->reject($e); } catch (\Exception $e) { $target = null; $this->reject($e); } } }