diff --git a/composer.json b/composer.json index 135b6d9..d45770c 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "promises", "php" ], + "homepage": "https://github.com/lazervel", "autoload": { "psr-4": { "Modassir\\Promise\\": "src/" diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php new file mode 100644 index 0000000..0878252 --- /dev/null +++ b/src/ErrorHandler.php @@ -0,0 +1,38 @@ + \ No newline at end of file diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php new file mode 100644 index 0000000..4b6b579 --- /dev/null +++ b/src/Exception/ExceptionInterface.php @@ -0,0 +1,26 @@ + \ No newline at end of file diff --git a/src/Exception/UnhandledPromiseRejection.php b/src/Exception/UnhandledPromiseRejection.php new file mode 100644 index 0000000..1da4a59 --- /dev/null +++ b/src/Exception/UnhandledPromiseRejection.php @@ -0,0 +1,26 @@ + \ No newline at end of file diff --git a/src/Executor.php b/src/Executor.php index 72837a2..be6aa98 100644 --- a/src/Executor.php +++ b/src/Executor.php @@ -2,116 +2,248 @@ declare(strict_types=1); +/** + * The PHP Promise handling PHP promises with additional utilities and features. + + * The (promise) Github Repository + * @see https://github.com/lazervel/promise + * + * @author Shahzada Modassir + * @copyright (c) Shahzada Modassir 2024 + * + * @license MIT License + * @see https://github.com/lazervel/promise/blob/main/LICENSE + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Modassir\Promise; -final class Executor extends Stack +use Modassir\Promise\Exception\UnhandledPromiseRejection; + + class Executor { - public $rejected = false; + /** + * Default promise state, + * initially set to 'pending' until resolved or rejected. + * + * @var string + */ + protected $state = 'pending'; + + /** + * Flag to know if executor handler was already fired + * + * @var bool + */ private $fired = false; /** + * Flag to know if catch block exists or not exists status + * + * @var bool + */ + private $catched = false; + + /** + * Flag to prevent firing + * + * @var bool + */ + private $locked = false; + + /** + * Flag to know if executor handler is currently firing + */ + private $firing = false; + + /** + * Actual callback arguments + * + * @var array + */ + private $arguments; + + /** + * A callback used to initialize the executor constructor. + * + * @var callable + */ + private $executor; + + /** + * A callback used to call the finally method of promise. + * + * @var callable + */ + protected $finallyClouser; + + /** + * Flag to know rejected or not rejected status + * + * @var bool + */ + private $rejected = false; + + /** + * + * @var array + */ + protected $queue = []; + + /** + * Creates a new Executor instance. + * Initializes a new instance of Executor with the given $executor. * - * @param \Modassir\Promise\Promise $promise [required] + * @param callable $executor [required] + * A callback used to initialize the executor. * @return void */ - public function __construct(Promise $promise) + public function __construct(callable $executor) { - $this->promise = $promise; + $this->executor = $executor; } /** + * Execute the final executor with resolve custom Error or Exception. * - * @param mixed $value [required] - * @param string $state [required] + * @return void + */ + protected function finalExecutorExecute() : void + { + try { + ErrorHandler::PHP_ErrorHandlerActivate(); + $this->execute(); + } catch(\Exception $e) { + $this->execute($e, 'reject', $e->getMessage()); + } catch(\Error $e) { + $this->execute($e, 'reject', $e->getMessage()); + } + } + + /** + * Creates a new resolved promise. * + * @param mixed $value [optional] * @return void */ - private function fire($value, string $state) + private function resolve($value = null) : void { - $this->promise->state = $state; - $this->promise->value = $value; - $this->fired = true; + $this->fireWith($value, 'resolved'); } /** + * Creates a new rejected promise. * - * @param \Modassir\Promise\Executor $executor [required] + * @param mixed $value [optional] + * @return void */ - private function resolver($executor) + private function reject($value = null) : void { - return static function($value = null) use ($executor) { - return $executor->resolve($value); - }; + $this->rejected = true; + $this->fireWith($value, 'rejected'); } /** * - * @param \Modassir\Promise\Executor $executor [required] + * + * @param string $handler [required] + * @return static Returns resolve or reject Handler. */ - private function rejector($executor) + private function getHandler(string $handler) { - return static function($value = null) use ($executor) { - return $executor->reject($value); + $promise = $this; + return static function($value = null) use ($promise, $handler) { + $promise->$handler($value); }; } /** - * @param mixed $value [optional] + * Call all callbacks with the given context and arguments + * fire - Method will used to call promise block handlers of specific state, + * This method could not be fire of 'pending' state. * @return void */ - public function resolve($value = null) : void + private function fire() : void { - if (!$this->fired) { - $this->fire($value, 'resolved'); + $this->locked = $this->firing = true; + foreach($this->queue as $queue) { + + if (($clouser = $queue['finally'] ?? false)) { + \call_user_func($clouser); + } elseif (($clouser = $queue[$this->state] ?? false)) { + \call_user_func_array($clouser, $this->arguments); + } } } /** + * Call all callbacks with the given context and arguments + * + * @param mixed $value [optional] + * @param string $state [required] * - * @param object $stack [required] * @return void */ - private function rejected(object $stack) : void + private function fireWith($value = null, string $state) : void { - $this->rejected = true; - $this->fire($stack, 'rejected'); + if (!$this->locked) { + $this->arguments = [$value, $this->state = $state]; + if (!$this->firing) { + $this->fire(); + } + } } /** - * @param mixed $value [optional] + * Set all handlers queue for 'resolved', 'rejected', 'finally' in queue + * + * @param callable $fnHandler [required] + * @param string $state [required] + * * @return void */ - public function reject($value = null) : void + private function setHandler(callable $fnHandler, string $state) : void { - if (!$this->fired) { - $this->rejected = true; - $this->fire($value, 'reject'); - } + $this->queue[] = [$state => $fnHandler]; } /** * - * @param \Modassir\Promise\Promise $promise [required] - * @return \Modassir\Promise\Executor + * + * @param callable $onFulfilled [optional] + * @param callable $onRejected [optional] + * + * @return void */ - public static function with(Promise $promise) + protected function addHandlers(callable $onFulfilled = null, callable $onRejected = null) : void { - return new self($promise); + if ($onRejected) $this->catched = true; + $onFulfilled && $this->setHandler($onFulfilled, 'resolved'); + $onRejected && $this->setHandler($onRejected, 'rejected'); } /** - * @param callable $executor [required] - * A callback used to initialize the promise. + * + * @throws Modassir\Promise\Exception\UnhandledPromiseRejection Rejected without catch blocking. + * + * @return void */ - public function execute(callable $executor) + private function execute($value = null, string $handler = null, string $EMessage = null) : void { - Stack::PHP_CustomErrorHandlerActivate(); - try { - $executor($this->resolver($this), $this->rejector($this)); - } catch(\Exception $e) { - $this->rejected($e); - } catch(\Error $e) { - $this->rejected($e); + if ($value && $handler) { + $this->$handler($value); + } else { + \call_user_func($this->executor, $this->getHandler('resolve'), $this->getHandler('reject')); + } + + if (!$this->catched && $this->rejected) { + throw new UnhandledPromiseRejection( + sprintf( + 'This error originated either by throwing inside of an method without a catch block, or by rejecting a promise which was not handled with ->catch(). The promise rejected with the reason [%s].', + $EMessage ?? $this->arguments[0] ?? NULL + ) + ); } } } diff --git a/src/Promise.php b/src/Promise.php index 90a7085..ccb7169 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -2,87 +2,95 @@ declare(strict_types=1); +/** + * The PHP Promise handling PHP promises with additional utilities and features. + + * The (promise) Github Repository + * @see https://github.com/lazervel/promise + * + * @author Shahzada Modassir + * @copyright (c) Shahzada Modassir 2024 + * + * @license MIT License + * @see https://github.com/lazervel/promise/blob/main/LICENSE + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Modassir\Promise; -class Promise +class Promise extends Executor { - public $state = 'pending'; - private $executor; - public $value; - /** * Creates a new Promise instance. + * Initializes a new instance of Promise with the given $executor. + * * @param callable $executor [required] * A callback used to initialize the promise. * @return void */ public function __construct(callable $executor) { - $this->executor = Executor::with($this)->execute($executor); - } - - /** - * @param callable $onFulfilled [optional] - * The callback to execute when the Promise is resolved. - * @return \Modassir\Promise\Promise - */ - public function done(?callable $onFulfilled = null) - { - return $this->then($onFulfilled); - } - - /** - * @param callable $onRejected [optional] - * The callback to execute when the Promise is rejected. - * @return \Modassir\Promise\Promise - */ - public function fail(?callable $onRejected = null) - { - return $this->catch($onRejected); + parent::__construct($executor); } /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). + * The resolved value cannot be modified from the callback. + * * @param callable $onFinally [optional] * The callback to execute when the Promise is settled (fulfilled or rejected). * @return \Modassir\Promise\Promise */ - public function finally(?callable $onFinally = null) + public function finally(callable $onFinally = null) { - return $this->done($onFinally)->fail($onFinally); + if ($onFinally) { + $this->queue[] = ['finally' => $onFinally]; + } + return $this; } /** + * Attaches a callback for only the rejection of the Promise. + * * @param callable $onRejected [optional] * The callback to execute when the Promise is rejected. - * @return \Modassir\Promise\Promise + * + * @return \Modassir\Promise\Promise A Promise for the completion of the callback. */ - public function catch(?callable $onRejected = null) + public function catch(callable $onRejected = null) { return $this->then(null, $onRejected); } /** - * @param callable $onFinally [optional] - * The callback to execute when the Promise is settled (fulfilled or rejected). - * @return \Modassir\Promise\Promise - */ - public function always(?callable $onFinally = null) - { - return $this->finally($onFinally); - } - - /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * * @param callable $onFulfilled [optional] * The callback to execute when the Promise is resolved. * @param callable $onRejected [optional] * The callback to execute when the Promise is rejected. * - * @return \Modassir\Promise\Promise + * @return \Modassir\Promise\Promise A Promise for the completion of which ever callback is executed. */ - public function then(?callable $onFulfilled = null, ?callable $onRejected = null) + public function then(callable $onFulfilled = null, callable $onRejected = null) { - $this->state === 'rejected' ? $onRejected && $onRejected($this->value) : $onFulfilled && $onFulfilled($this->value); + $this->addHandlers($onFulfilled, $onRejected); return $this; } + + /** + * Destructor method. + * This method is automatically called when the object is destroyed. + * It is used to release any resources or perform cleanup tasks, + * such as closing database connections, file handles, or other resources. + * + * @throws Modassir\Promise\Exception\UnhandledPromiseRejection Rejected without catch blocking. + * @return void + */ + public function __destruct() + { + $this->finalExecutorExecute(); + } } ?> \ No newline at end of file diff --git a/src/Stack.php b/src/Stack.php deleted file mode 100644 index a44d6f3..0000000 --- a/src/Stack.php +++ /dev/null @@ -1,87 +0,0 @@ -code = $code; - $this->message = $message; - $this->file = $file; - $this->line = $line; - } - - public function getMessage() - { - return $this->message; - } - - public function getCode() - { - return $this->code; - } - - protected function getStack() - { - return $this; - } - - public function getFile() - { - return $this->file; - } - - public function getLine() - { - return $this->line; - } - - public function getTraceAsString() - { - $exception = new \Exception($this->message); - return $exception->getTraceAsString(); - } - - /** - * - */ - private static function shutdown() - { - $error = \error_get_last(); - if ($error === null) { - return; - } - new self($error->type, $error->message, $error->file, $error->line); - } - - public static function PHP_CustomErrorHandlerActivate() - { - \set_error_handler(self::errorHandler(...)); - \register_shutdown_function(self::shutdown(...)); - } - - /** - * - */ - private static function errorHandler(int $code, string $message, string $file, int $line) - { - new self($code, $message, $file, $line); - } - - public function __toString() - { - return ''; - } -} -?>