Skip to content

Commit

Permalink
Chained promise returning (#71)
Browse files Browse the repository at this point in the history
* test: pass existing tests for #70 functionality

* build: php 8.1 compatibility

* ci: use latest phpunit

* ci: use latest phpunit

* ci: debug phpunit

* ci: debug phpunit

* ci: debug phpunit

* test: ensure "finally" can return its own promise

* test: ensure "finally" can return its own promise

* test: fix static analysis

* build: ignore complexity error
  • Loading branch information
g105b authored May 5, 2024
1 parent 82a132e commit 0bdb4d7
Show file tree
Hide file tree
Showing 9 changed files with 591 additions and 328 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
run: tar -xvf /tmp/github-actions/build.tar ./

- name: PHP Unit tests
uses: php-actions/phpunit@v3
uses: php-actions/phpunit@master
env:
XDEBUG_MODE: cover
with:
Expand Down
490 changes: 260 additions & 230 deletions composer.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<rule ref="Generic.Files.EndFileNewline" />
<rule ref="Generic.Files.InlineHTML" />
<rule ref="Generic.Files.LineEndings" />
<rule ref="Generic.Files.LineLength" />
<rule ref="Generic.Files.OneClassPerFile" />
<rule ref="Generic.Files.OneInterfacePerFile" />
<rule ref="Generic.Files.OneObjectStructurePerFile" />
Expand Down
6 changes: 6 additions & 0 deletions src/Chain/ChainFunctionTypeError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php
namespace Gt\Promise\Chain;

use Gt\Promise\PromiseException;

class ChainFunctionTypeError extends PromiseException {}
105 changes: 90 additions & 15 deletions src/Chain/Chainable.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<?php
namespace Gt\Promise\Chain;

use Closure;
use ReflectionFunction;
use ReflectionIntersectionType;
use ReflectionNamedType;
use ReflectionUnionType;
use Throwable;
use TypeError;

Expand Down Expand Up @@ -37,20 +41,91 @@ public function callOnRejected(Throwable $reason) {
}

return call_user_func($this->onRejected, $reason);
// try {
// }
// catch(TypeError $error) {
// $reflection = new ReflectionFunction($this->onRejected);
// $param = $reflection->getParameters()[0] ?? null;
// if($param) {
// $paramType = (string)$param->getType();
//
// if(!str_contains($error->getMessage(), "must be of type $paramType")) {
// throw $error;
// }
// }
//
// return $reason;
// }
}

public function checkResolutionCallbackType(mixed $resolvedValue):void {
if(isset($this->onResolved)) {
$this->checkType($resolvedValue, $this->onResolved);
}
}

public function checkRejectionCallbackType(Throwable $rejection):void {
if(isset($this->onRejected)) {
$this->checkType($rejection, $this->onRejected);
}
}

/**
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
// phpcs:ignore
private function checkType(mixed $value, callable $callable):void {
if(!$callable instanceof Closure) {
return;
}

$refFunction = new ReflectionFunction($callable);
$refParameterList = $refFunction->getParameters();
if(!isset($refParameterList[0])) {
return;
}
$refParameter = $refParameterList[0];
$nullable = $refParameter->allowsNull();

if(is_null($value)) {
if(!$nullable) {
throw new ChainFunctionTypeError("Then function's parameter is not nullable");
}
}

$allowedTypes = [];
$refType = $refParameter->getType();

if($refType instanceof ReflectionUnionType || $refType instanceof ReflectionIntersectionType) {
/** @var ReflectionNamedType $refSubType */
foreach($refType->getTypes() as $refSubType) {
array_push($allowedTypes, $refSubType->getName());
}
}
else {
/** @var ?ReflectionNamedType $refType */
array_push($allowedTypes, $refType?->getName());
}

$valueType = is_object($value)
? get_class($value)
: gettype($value);
foreach($allowedTypes as $allowedType) {
$allowedType = match($allowedType) {
"int" => "integer",
"float" => "double",
default => $allowedType,
};
if(is_null($allowedType) || $allowedType === "mixed") {
// A typeless property is defined - allow anything!
return;
}
if($allowedType === $valueType) {
return;
}

if(is_a($valueType, $allowedType, true)) {
return;
}

if($allowedType === "string") {
if($valueType === "double" || $valueType === "integer") {
return;
}
}
if($allowedType === "double") {
if(is_numeric($value)) {
return;
}
}
}

throw new ChainFunctionTypeError("Value $value is not compatible with chainable parameter");
}
}
3 changes: 2 additions & 1 deletion src/Chain/ThenChain.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
namespace Gt\Promise\Chain;

class ThenChain extends Chainable {}
class ThenChain extends Chainable {
}
47 changes: 39 additions & 8 deletions src/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@

use Gt\Promise\Chain\CatchChain;
use Gt\Promise\Chain\Chainable;
use Gt\Promise\Chain\ChainFunctionTypeError;
use Gt\Promise\Chain\FinallyChain;
use Gt\Promise\Chain\ThenChain;
use Throwable;

/**
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
class Promise implements PromiseInterface {
private mixed $resolvedValue;
/** @var bool This is required due to the ability to set `null` as a resolved value. */
private bool $resolvedValueSet = false;
private Throwable $rejectedReason;

/** @var Chainable[] */
Expand All @@ -33,7 +40,7 @@ public function getState():PromiseState {
if(isset($this->rejectedReason)) {
return PromiseState::REJECTED;
}
elseif(isset($this->resolvedValue)) {
elseif($this->resolvedValueSet) {
return PromiseState::RESOLVED;
}

Expand Down Expand Up @@ -87,7 +94,12 @@ private function callExecutor():void {
call_user_func(
$this->executor,
function(mixed $value = null) {
$this->resolve($value);
try {
$this->resolve($value);
}
catch(PromiseException $exception) {
$this->reject($exception);
}
},
function(Throwable $reason) {
$this->reject($reason);
Expand All @@ -107,6 +119,7 @@ private function resolve(mixed $value):void {
}

$this->resolvedValue = $value;
$this->resolvedValueSet = true;
}

private function reject(Throwable $reason):void {
Expand All @@ -133,6 +146,7 @@ private function tryComplete():void {
}
}

// phpcs:ignore
private function complete():void {
usort(
$this->chain,
Expand All @@ -155,11 +169,28 @@ function(Chainable $a, Chainable $b) {
}

if($chainItem instanceof ThenChain) {
try {
if($this->resolvedValueSet && isset($this->resolvedValue)) {
$chainItem->checkResolutionCallbackType($this->resolvedValue);
}
}
catch(ChainFunctionTypeError) {
continue;
}

$this->handleThen($chainItem);
}
elseif($chainItem instanceof CatchChain) {
if($handled = $this->handleCatch($chainItem)) {
array_push($this->handledRejections, $handled);
try {
if(isset($this->rejectedReason)) {
$chainItem->checkRejectionCallbackType($this->rejectedReason);
}
if($handled = $this->handleCatch($chainItem)) {
array_push($this->handledRejections, $handled);
}
}
catch(ChainFunctionTypeError) {
continue;
}
}
elseif($chainItem instanceof FinallyChain) {
Expand All @@ -180,8 +211,10 @@ private function handleThen(ThenChain $then):void {
}

try {
$result = $then->callOnResolved($this->resolvedValue)
?? $this->resolvedValue ?? null;
$result = null;
if(isset($this->resolvedValue)) {
$result = $then->callOnResolved($this->resolvedValue);
}

if($result instanceof PromiseInterface) {
$this->chainPromise($result);
Expand All @@ -197,8 +230,6 @@ private function handleThen(ThenChain $then):void {

private function handleCatch(CatchChain $catch):?Throwable {
if($this->getState() !== PromiseState::REJECTED) {
// TODO: This is where #52 can be implemented
// see: (https://github.com/PhpGt/Promise/issues/52)
array_push($this->uncalledCatchChain, $catch);
return null;
}
Expand Down
Loading

0 comments on commit 0bdb4d7

Please sign in to comment.