Skip to content

Commit c1a49be

Browse files
committed
test: pass existing tests for #70 functionality
1 parent 907b3a4 commit c1a49be

7 files changed

+280
-95
lines changed

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

+89-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,90 @@ 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+
foreach($refType->getTypes() as $refSubType) {
87+
array_push($allowedTypes, $refSubType->getName());
88+
}
89+
}
90+
else {
91+
/** @var ?ReflectionNamedType $refType */
92+
array_push($allowedTypes, $refType?->getName());
93+
}
94+
95+
$valueType = is_object($value)
96+
? get_class($value)
97+
: gettype($value);
98+
foreach($allowedTypes as $allowedType) {
99+
$allowedType = match($allowedType) {
100+
"int" => "integer",
101+
"float" => "double",
102+
default => $allowedType,
103+
};
104+
if(is_null($allowedType) || $allowedType === "mixed") {
105+
// A typeless property is defined - allow anything!
106+
return;
107+
}
108+
if($allowedType === $valueType) {
109+
return;
110+
}
111+
112+
if(is_a($valueType, $allowedType, true)) {
113+
return;
114+
}
115+
116+
if($allowedType === "string") {
117+
if($valueType === "double" || $valueType === "integer") {
118+
return;
119+
}
120+
}
121+
if($allowedType === "double") {
122+
if(is_numeric($value)) {
123+
return;
124+
}
125+
}
126+
}
127+
128+
throw new ChainFunctionTypeError("Value $value is not compatible with chainable parameter");
55129
}
56130
}

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

+33-6
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
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

1011
class Promise implements PromiseInterface {
1112
private mixed $resolvedValue;
13+
/** @var bool This is required due to the ability to set `null` as a resolved value. */
14+
private bool $resolvedValueSet = false;
1215
private Throwable $rejectedReason;
1316

1417
/** @var Chainable[] */
@@ -33,7 +36,7 @@ public function getState():PromiseState {
3336
if(isset($this->rejectedReason)) {
3437
return PromiseState::REJECTED;
3538
}
36-
elseif(isset($this->resolvedValue)) {
39+
elseif($this->resolvedValueSet) {
3740
return PromiseState::RESOLVED;
3841
}
3942

@@ -87,7 +90,12 @@ private function callExecutor():void {
8790
call_user_func(
8891
$this->executor,
8992
function(mixed $value = null) {
90-
$this->resolve($value);
93+
try {
94+
$this->resolve($value);
95+
}
96+
catch(PromiseException $exception) {
97+
$this->reject($exception);
98+
}
9199
},
92100
function(Throwable $reason) {
93101
$this->reject($reason);
@@ -107,6 +115,7 @@ private function resolve(mixed $value):void {
107115
}
108116

109117
$this->resolvedValue = $value;
118+
$this->resolvedValueSet = true;
110119
}
111120

112121
private function reject(Throwable $reason):void {
@@ -133,6 +142,8 @@ private function tryComplete():void {
133142
}
134143
}
135144

145+
/** @SuppressWarnings(PHPMD.CyclomaticComplexity) */
146+
// phpcs:ignore
136147
private function complete():void {
137148
usort(
138149
$this->chain,
@@ -155,11 +166,28 @@ function(Chainable $a, Chainable $b) {
155166
}
156167

157168
if($chainItem instanceof ThenChain) {
169+
try {
170+
if($this->resolvedValueSet) {
171+
$chainItem->checkResolutionCallbackType($this->resolvedValue);
172+
}
173+
}
174+
catch(ChainFunctionTypeError) {
175+
continue;
176+
}
177+
158178
$this->handleThen($chainItem);
159179
}
160180
elseif($chainItem instanceof CatchChain) {
161-
if($handled = $this->handleCatch($chainItem)) {
162-
array_push($this->handledRejections, $handled);
181+
try {
182+
if(isset($this->rejectedReason)) {
183+
$chainItem->checkRejectionCallbackType($this->rejectedReason);
184+
}
185+
if($handled = $this->handleCatch($chainItem)) {
186+
array_push($this->handledRejections, $handled);
187+
}
188+
}
189+
catch(ChainFunctionTypeError) {
190+
continue;
163191
}
164192
}
165193
elseif($chainItem instanceof FinallyChain) {
@@ -180,8 +208,7 @@ private function handleThen(ThenChain $then):void {
180208
}
181209

182210
try {
183-
$result = $then->callOnResolved($this->resolvedValue)
184-
?? $this->resolvedValue ?? null;
211+
$result = $then->callOnResolved($this->resolvedValue);
185212

186213
if($result instanceof PromiseInterface) {
187214
$this->chainPromise($result);

0 commit comments

Comments
 (0)