Skip to content

Commit 0bdb4d7

Browse files
authored
Chained promise returning (#71)
* 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
1 parent 82a132e commit 0bdb4d7

9 files changed

+591
-328
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
run: tar -xvf /tmp/github-actions/build.tar ./
5353

5454
- name: PHP Unit tests
55-
uses: php-actions/phpunit@v3
55+
uses: php-actions/phpunit@master
5656
env:
5757
XDEBUG_MODE: cover
5858
with:

composer.lock

+260-230
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpcs.xml

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
<rule ref="Generic.Files.EndFileNewline" />
2020
<rule ref="Generic.Files.InlineHTML" />
2121
<rule ref="Generic.Files.LineEndings" />
22-
<rule ref="Generic.Files.LineLength" />
2322
<rule ref="Generic.Files.OneClassPerFile" />
2423
<rule ref="Generic.Files.OneInterfacePerFile" />
2524
<rule ref="Generic.Files.OneObjectStructurePerFile" />

src/Chain/ChainFunctionTypeError.php

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
namespace Gt\Promise\Chain;
3+
4+
use Gt\Promise\PromiseException;
5+
6+
class ChainFunctionTypeError extends PromiseException {}

src/Chain/Chainable.php

+90-15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<?php
22
namespace Gt\Promise\Chain;
33

4+
use Closure;
45
use ReflectionFunction;
6+
use ReflectionIntersectionType;
7+
use ReflectionNamedType;
8+
use ReflectionUnionType;
59
use Throwable;
610
use TypeError;
711

@@ -37,20 +41,91 @@ public function callOnRejected(Throwable $reason) {
3741
}
3842

3943
return call_user_func($this->onRejected, $reason);
40-
// try {
41-
// }
42-
// catch(TypeError $error) {
43-
// $reflection = new ReflectionFunction($this->onRejected);
44-
// $param = $reflection->getParameters()[0] ?? null;
45-
// if($param) {
46-
// $paramType = (string)$param->getType();
47-
//
48-
// if(!str_contains($error->getMessage(), "must be of type $paramType")) {
49-
// throw $error;
50-
// }
51-
// }
52-
//
53-
// return $reason;
54-
// }
44+
}
45+
46+
public function checkResolutionCallbackType(mixed $resolvedValue):void {
47+
if(isset($this->onResolved)) {
48+
$this->checkType($resolvedValue, $this->onResolved);
49+
}
50+
}
51+
52+
public function checkRejectionCallbackType(Throwable $rejection):void {
53+
if(isset($this->onRejected)) {
54+
$this->checkType($rejection, $this->onRejected);
55+
}
56+
}
57+
58+
/**
59+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
60+
* @SuppressWarnings(PHPMD.NPathComplexity)
61+
*/
62+
// phpcs:ignore
63+
private function checkType(mixed $value, callable $callable):void {
64+
if(!$callable instanceof Closure) {
65+
return;
66+
}
67+
68+
$refFunction = new ReflectionFunction($callable);
69+
$refParameterList = $refFunction->getParameters();
70+
if(!isset($refParameterList[0])) {
71+
return;
72+
}
73+
$refParameter = $refParameterList[0];
74+
$nullable = $refParameter->allowsNull();
75+
76+
if(is_null($value)) {
77+
if(!$nullable) {
78+
throw new ChainFunctionTypeError("Then function's parameter is not nullable");
79+
}
80+
}
81+
82+
$allowedTypes = [];
83+
$refType = $refParameter->getType();
84+
85+
if($refType instanceof ReflectionUnionType || $refType instanceof ReflectionIntersectionType) {
86+
/** @var ReflectionNamedType $refSubType */
87+
foreach($refType->getTypes() as $refSubType) {
88+
array_push($allowedTypes, $refSubType->getName());
89+
}
90+
}
91+
else {
92+
/** @var ?ReflectionNamedType $refType */
93+
array_push($allowedTypes, $refType?->getName());
94+
}
95+
96+
$valueType = is_object($value)
97+
? get_class($value)
98+
: gettype($value);
99+
foreach($allowedTypes as $allowedType) {
100+
$allowedType = match($allowedType) {
101+
"int" => "integer",
102+
"float" => "double",
103+
default => $allowedType,
104+
};
105+
if(is_null($allowedType) || $allowedType === "mixed") {
106+
// A typeless property is defined - allow anything!
107+
return;
108+
}
109+
if($allowedType === $valueType) {
110+
return;
111+
}
112+
113+
if(is_a($valueType, $allowedType, true)) {
114+
return;
115+
}
116+
117+
if($allowedType === "string") {
118+
if($valueType === "double" || $valueType === "integer") {
119+
return;
120+
}
121+
}
122+
if($allowedType === "double") {
123+
if(is_numeric($value)) {
124+
return;
125+
}
126+
}
127+
}
128+
129+
throw new ChainFunctionTypeError("Value $value is not compatible with chainable parameter");
55130
}
56131
}

src/Chain/ThenChain.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
22
namespace Gt\Promise\Chain;
33

4-
class ThenChain extends Chainable {}
4+
class ThenChain extends Chainable {
5+
}

src/Promise.php

+39-8
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@
33

44
use Gt\Promise\Chain\CatchChain;
55
use Gt\Promise\Chain\Chainable;
6+
use Gt\Promise\Chain\ChainFunctionTypeError;
67
use Gt\Promise\Chain\FinallyChain;
78
use Gt\Promise\Chain\ThenChain;
89
use Throwable;
910

11+
/**
12+
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
13+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
14+
*/
1015
class Promise implements PromiseInterface {
1116
private mixed $resolvedValue;
17+
/** @var bool This is required due to the ability to set `null` as a resolved value. */
18+
private bool $resolvedValueSet = false;
1219
private Throwable $rejectedReason;
1320

1421
/** @var Chainable[] */
@@ -33,7 +40,7 @@ public function getState():PromiseState {
3340
if(isset($this->rejectedReason)) {
3441
return PromiseState::REJECTED;
3542
}
36-
elseif(isset($this->resolvedValue)) {
43+
elseif($this->resolvedValueSet) {
3744
return PromiseState::RESOLVED;
3845
}
3946

@@ -87,7 +94,12 @@ private function callExecutor():void {
8794
call_user_func(
8895
$this->executor,
8996
function(mixed $value = null) {
90-
$this->resolve($value);
97+
try {
98+
$this->resolve($value);
99+
}
100+
catch(PromiseException $exception) {
101+
$this->reject($exception);
102+
}
91103
},
92104
function(Throwable $reason) {
93105
$this->reject($reason);
@@ -107,6 +119,7 @@ private function resolve(mixed $value):void {
107119
}
108120

109121
$this->resolvedValue = $value;
122+
$this->resolvedValueSet = true;
110123
}
111124

112125
private function reject(Throwable $reason):void {
@@ -133,6 +146,7 @@ private function tryComplete():void {
133146
}
134147
}
135148

149+
// phpcs:ignore
136150
private function complete():void {
137151
usort(
138152
$this->chain,
@@ -155,11 +169,28 @@ function(Chainable $a, Chainable $b) {
155169
}
156170

157171
if($chainItem instanceof ThenChain) {
172+
try {
173+
if($this->resolvedValueSet && isset($this->resolvedValue)) {
174+
$chainItem->checkResolutionCallbackType($this->resolvedValue);
175+
}
176+
}
177+
catch(ChainFunctionTypeError) {
178+
continue;
179+
}
180+
158181
$this->handleThen($chainItem);
159182
}
160183
elseif($chainItem instanceof CatchChain) {
161-
if($handled = $this->handleCatch($chainItem)) {
162-
array_push($this->handledRejections, $handled);
184+
try {
185+
if(isset($this->rejectedReason)) {
186+
$chainItem->checkRejectionCallbackType($this->rejectedReason);
187+
}
188+
if($handled = $this->handleCatch($chainItem)) {
189+
array_push($this->handledRejections, $handled);
190+
}
191+
}
192+
catch(ChainFunctionTypeError) {
193+
continue;
163194
}
164195
}
165196
elseif($chainItem instanceof FinallyChain) {
@@ -180,8 +211,10 @@ private function handleThen(ThenChain $then):void {
180211
}
181212

182213
try {
183-
$result = $then->callOnResolved($this->resolvedValue)
184-
?? $this->resolvedValue ?? null;
214+
$result = null;
215+
if(isset($this->resolvedValue)) {
216+
$result = $then->callOnResolved($this->resolvedValue);
217+
}
185218

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

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

0 commit comments

Comments
 (0)