Skip to content

Commit 7a4f103

Browse files
author
Greg Bowler
authored
Chained promises should always resolve their value before calling (#13)
* Add missing typehint * Add missing typehint * Add missing typehint * Update dependencies * Add badges and reorder intro docs * Update dependencies * Isolate bug #12 * Add support for 7.4 and 8.0 * Improve code quality * Upgrade dependencies with PHP 8
1 parent 148ab09 commit 7a4f103

5 files changed

+63
-14
lines changed

composer.lock

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

src/Chain/Chainable.php

+19-5
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,25 @@ public function callOnRejected(Throwable $reason) {
4848
$param = $reflection->getParameters()[0] ?? null;
4949
if($param) {
5050
$paramType = (string)$param->getType();
51-
if(!strstr(
52-
$error->getMessage(),
53-
"must be of type $paramType"
54-
)) {
55-
throw $error;
51+
52+
// TypeError messages behave slightly differently between PHP 7 and 8.
53+
// This strange if block will be dropped when PHP 7.4 support is dropped.
54+
if(PHP_VERSION[0] >= 8) {
55+
if(!strstr(
56+
$error->getMessage(),
57+
"must be of type $paramType"
58+
)) {
59+
throw $error;
60+
}
61+
}
62+
else {
63+
$paramType = str_replace("\\", "\\\\", $paramType);
64+
if(!preg_match(
65+
"/must be (of the type|an instance of) $paramType/",
66+
$error->getMessage()
67+
)) {
68+
throw $error;
69+
}
5670
}
5771
}
5872

src/Promise.php

+12-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Promise implements PromiseInterface, HttpPromiseInterface {
1919
private ?Throwable $rejection;
2020
/** @var callable */
2121
private $waitTask;
22-
private float $waitTaskDelayMicroseconds;
22+
private float $waitTaskDelay;
2323

2424
public function __construct(callable $executor) {
2525
$this->state = HttpPromiseInterface::PENDING;
@@ -126,6 +126,14 @@ private function handleChain():void {
126126
}
127127
else {
128128
$value = $then->callOnFulfilled($this->resolvedValue);
129+
if($value instanceof PromiseInterface) {
130+
$value->then(function($resolvedValue) {
131+
$this->resolvedValue = $resolvedValue;
132+
$this->complete();
133+
});
134+
break;
135+
}
136+
129137
$this->state = HttpPromiseInterface::FULFILLED;
130138
if(!is_null($value)) {
131139
$this->resolvedValue = $value;
@@ -149,10 +157,10 @@ public function getState():string {
149157

150158
public function setWaitTask(
151159
callable $task,
152-
float $delayMicroseconds = 1_000
160+
float $delaySeconds = 0.01
153161
):void {
154162
$this->waitTask = $task;
155-
$this->waitTaskDelayMicroseconds = $delayMicroseconds;
163+
$this->waitTaskDelay = $delaySeconds;
156164
}
157165

158166
/** @param bool $unwrap */
@@ -163,7 +171,7 @@ public function wait($unwrap = true) {
163171

164172
while($this->getState() === HttpPromiseInterface::PENDING) {
165173
call_user_func($this->waitTask);
166-
usleep($this->waitTaskDelayMicroseconds);
174+
usleep((int)($this->waitTaskDelay * 1_000_000));
167175
}
168176

169177
$this->complete();

src/PromiseResolvedWithAnotherPromiseException.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ class PromiseResolvedWithAnotherPromiseException extends PromiseException {
77
const DEFAULT_MESSAGE = "A Promise must be resolved with a concrete value, not another Promise.";
88

99
public function __construct(
10-
$message = self::DEFAULT_MESSAGE,
11-
$code = 0,
10+
string $message = self::DEFAULT_MESSAGE,
11+
int $code = 0,
1212
Throwable $previous = null
1313
) {
1414
parent::__construct($message, $code, $previous);

test/phpunit/PromiseTest.php

+28-1
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,12 @@ public function testMatchingTypedCatchRejectionHandlerCanHandleInternalTypeError
409409
// should bubble out of the chain rather than being seen as
410410
// missing the RangeException type hint.
411411
self::expectException(TypeError::class);
412-
self::expectExceptionMessage("DateTime::__construct(): Argument #1 (\$datetime) must be of type string, Closure given");
412+
if(PHP_VERSION[0] >= 8) {
413+
self::expectExceptionMessage("DateTime::__construct(): Argument #1 (\$datetime) must be of type string, Closure given");
414+
}
415+
else {
416+
self::expectExceptionMessage("DateTime::__construct() expects parameter 1 to be string, object given");
417+
}
413418

414419
$sut->catch(function(PromiseException $reason1) use($onRejected1) {
415420
call_user_func($onRejected1, $reason1);
@@ -659,6 +664,28 @@ public function testWaitWithNoWaitTask() {
659664
$sut->wait();
660665
}
661666

667+
public function testFulfilledReturnsNewPromiseThatIsResolved() {
668+
$numberPromiseContainer = $this->getTestPromiseContainer();
669+
$numberPromise = $numberPromiseContainer->getPromise();
670+
671+
$messagePromiseContainer = $this->getTestPromiseContainer();
672+
$messagePromise = $messagePromiseContainer->getPromise();
673+
674+
$numberToResolveWith = null;
675+
676+
// The first onFulfilled takes the number to process, and returns a new promise
677+
// which should resolve to a message containing the number.
678+
$numberPromise->then(function(int $number) use($messagePromiseContainer, $messagePromise, &$numberToResolveWith) {
679+
$numberToResolveWith = $number;
680+
return $messagePromise;
681+
})->then(self::mockCallable(1, "Your number is 105"));
682+
683+
// TODO: Issue #12: 105 resolves before the message does, so the numberPromise's then function will return a pending Promise.
684+
// The chained then should only be called after the message is resolved, so this needs to be stored internally somewhere for fulfillment.
685+
$numberPromiseContainer->resolve(105);
686+
$messagePromiseContainer->resolve("Your number is $numberToResolveWith");
687+
}
688+
662689
protected function getTestPromiseContainer():TestPromiseContainer {
663690
$resolveCallback = null;
664691
$rejectCallback = null;

0 commit comments

Comments
 (0)