Skip to content

Commit

Permalink
Baseline unit tests passed
Browse files Browse the repository at this point in the history
  • Loading branch information
g105b committed Nov 26, 2020
1 parent 2aa6270 commit 5b95f71
Show file tree
Hide file tree
Showing 8 changed files with 473 additions and 3 deletions.
74 changes: 74 additions & 0 deletions src/Deferred.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
namespace Gt\Promise;

use Throwable;

class Deferred implements DeferredInterface {
private Promise $promise;
/** @var callable */
private $resolveCallback;
/** @var callable */
private $rejectCallback;
/** @var callable[] */
private array $processList;
/** @var callable[] */
private array $completeCallback;
private bool $activated;

public function __construct(callable $process = null) {
$this->promise = new Promise(function($resolve, $reject):void {
$this->resolveCallback = $resolve;
$this->rejectCallback = $reject;
});

$this->processList = [];
$this->completeCallback = [];
$this->activated = true;

if(!is_null($process)) {
$this->addProcess($process);
}
}

public function getPromise():PromiseInterface {
return $this->promise;
}

public function resolve($value = null):void {
$this->complete();
call_user_func($this->resolveCallback, $value);
}

public function reject(Throwable $reason):void {
$this->complete();
call_user_func($this->rejectCallback, $reason);
}

private function complete():void {
if(!$this->activated) {
return;
}

$this->activated = false;
foreach($this->completeCallback as $callback) {
call_user_func($callback);
}
}

public function addProcess(callable $process):void {
array_push($this->processList, $process);
}

/** @return callable[] */
public function getProcessList():array {
return $this->processList;
}

public function isActive():bool {
return $this->activated;
}

public function addCompleteCallback(callable $callback):void {
array_push($this->completeCallback, $callback);
}
}
74 changes: 74 additions & 0 deletions src/FulfilledPromise.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
namespace Gt\Promise;

use Throwable;

class FulfilledPromise implements PromiseInterface {
use Resolvable;

/** @var mixed */
private $value;

/** @param ?mixed $promiseOrValue */
public function __construct($promiseOrValue = null) {
if($promiseOrValue instanceof PromiseInterface) {
throw new PromiseException("A FulfilledPromise must be resolved with a concrete value, not a Promise.");
}

$this->value = $promiseOrValue;
}

public function then(
callable $onFulfilled = null,
callable $onRejected = null
):PromiseInterface {
if(is_null($onFulfilled)) {
return $this;
}

return new Promise(
function(callable $resolve, callable $reject)
use ($onFulfilled):void {
try {
$resolve($onFulfilled($this->value));
}
catch(Throwable $reason) {
$reject($reason);
}
}
);
}

public function complete(
callable $onFulfilled = null,
callable $onRejected = null
):void {
if(is_null($onFulfilled)) {
return;
}

$result = $onFulfilled($this->value);

if($result instanceof PromiseInterface) {
$result->complete();
}
}

public function catch(callable $onRejected):PromiseInterface {
return $this;
}

public function finally(
callable $onFulfilledOrRejected
):PromiseInterface {
return $this->then(
function($value)
use ($onFulfilledOrRejected):PromiseInterface {
return $this->resolve($onFulfilledOrRejected())
->then(function() use ($value) {
return $value;
});
}
);
}
}
148 changes: 148 additions & 0 deletions src/Promise.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php
namespace Gt\Promise;

use Throwable;

class Promise implements PromiseInterface {
use Resolvable;

/** @var mixed */
private $result;
/** @var callable[] */
private array $handlers;

/**
* @param callable $executor A function to be executed by the
* constructor, during the process of constructing the Promise. The
* executor is custom code that ties an outcome to a promise. You,
* the programmer, write the executor. The signature of this function is
* expected to be:
* function(callable $resolutionFunc, callable $rejectionFunc):void {}
*/
public function __construct(callable $executor) {
$this->handlers = [];
$this->call($executor);
}

public function then(
callable $onFulfilled = null,
callable $onRejected = null
):PromiseInterface {
if(!is_null($this->result)) {
return $this->result->then(
$onFulfilled,
$onRejected
);
}

return new static($this->resolver($onFulfilled, $onRejected));
}

public function catch(
callable $onRejected
):PromiseInterface {
return $this->then(
null,
fn(Throwable $reason):PromiseInterface => $onRejected($reason)
);
}

public function finally(
callable $onFulfilledOrRejected
):PromiseInterface {
return $this->then(
fn($value) => $this->resolve($onFulfilledOrRejected())->then(fn() => $value),
fn(Throwable $reason) => $this->resolve($onFulfilledOrRejected())->then(fn() => new RejectedPromise($reason))
);
}

public function complete(
callable $onFulfilled = null,
callable $onRejected = null
):void {
if(!is_null($this->result)) {
$this->result->complete($onFulfilled, $onRejected);
return;
}

array_push(
$this->handlers,
function(PromiseInterface $promise) use ($onFulfilled, $onRejected) {
$promise->complete($onFulfilled, $onRejected);
}
);
}

private function resolver(
callable $onFulfilled = null,
callable $onRejected = null
):callable {
return function(callable $resolve, callable $reject)
use ($onFulfilled, $onRejected):void {
array_push(
$this->handlers,
function(PromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject):void {
$promise->then($onFulfilled, $onRejected)
->complete($resolve, $reject);
}
);
};
}

private function reject(Throwable $reason):void {
if(!is_null($this->result)) {
return;
}

$this->settle(new RejectedPromise($reason));
}

private function settle(PromiseInterface $result):void {
$result = $this->unwrap($result);

if($result === $this) {
$result = new RejectedPromise(
new PromiseException("A Promise must be settled with a concrete value, not another Promise.")
);
}

$handlers = $this->handlers;

$this->handlers = [];
$this->result = $result;

foreach($handlers as $handler) {
call_user_func($handler, $result);
}
}

/**
* Obtains a reference to the ultimate promise in the chain.
*/
private function unwrap(PromiseInterface $promise):PromiseInterface {
while($promise instanceof self
&& !is_null($promise->result)) {
$promise = $promise->result;
}

return $promise;
}

private function call(callable $callback):void {
try {
call_user_func(
$callback,
/** @param mixed $value */
function($value = null) {
$this->settle($this->resolve($value));
},
function(Throwable $reason) {
$this->reject($reason);
}
);
}
catch(Throwable $reason) {
$this->reject($reason);
}
}
}
6 changes: 6 additions & 0 deletions src/PromiseException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php
namespace Gt\Promise;

use RuntimeException;

class PromiseException extends RuntimeException {}
75 changes: 75 additions & 0 deletions src/RejectedPromise.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
namespace Gt\Promise;

use Throwable;

class RejectedPromise implements PromiseInterface {
use Resolvable;

private Throwable $reason;

public function __construct(Throwable $reason) {
$this->reason = $reason;
}

public function then(
callable $onFulfilled = null,
callable $onRejected = null
):PromiseInterface {
if(is_null($onRejected)) {
return $this;
}

return new Promise(function(callable $resolve, callable $reject)
use ($onRejected):void {
try {
$resolve($onRejected($this->reason));
}
catch(Throwable $reason) {
$reject($reason);
}
});
}

public function complete(
callable $onFulfilled = null,
callable $onRejected = null
):void {
if(null === $onRejected) {
throw $this->reason;
}

$result = $onRejected($this->reason);

if($result instanceof self) {
throw $result->reason;
}

if($result instanceof PromiseInterface) {
$result->complete();
}
}

public function catch(
callable $onRejected
):PromiseInterface {
return $this->then(null, $onRejected);
}

public function finally(
callable $onFulfilledOrRejected
):PromiseInterface {
return $this->then(
null,
function(Throwable $reason)
use ($onFulfilledOrRejected):PromiseInterface {
return $this->resolve($onFulfilledOrRejected())
->then(function() use ($reason):PromiseInterface {
return new RejectedPromise(
$reason
);
});
}
);
}
}
Loading

0 comments on commit 5b95f71

Please sign in to comment.