Skip to content

Commit 1e15108

Browse files
author
Greg Bowler
authored
Chained promises (#14)
* 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 * Remove composer's lockfile * Update tests with more realistic usages * Add documentation comments to complex test * Introduce dependent deferred processes * Pass tests for #12 by nulling resolved value after completion callback * feature: add process to Deferred
1 parent ebd0067 commit 1e15108

File tree

3 files changed

+133
-30
lines changed

3 files changed

+133
-30
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/vendor
2+
composer.lock
23
/test/phpunit/_coverage
34
.phpunit.result.cache
45
/example.*

src/Promise.php

+41-22
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class Promise implements PromiseInterface, HttpPromiseInterface {
1414
private $resolvedValue;
1515
/** @var Chainable[] */
1616
private array $chain;
17+
/** @var Chainable[] */
18+
private array $pendingChain;
1719
/** @var callable */
1820
private $executor;
1921
private ?Throwable $rejection;
@@ -24,6 +26,7 @@ class Promise implements PromiseInterface, HttpPromiseInterface {
2426
public function __construct(callable $executor) {
2527
$this->state = HttpPromiseInterface::PENDING;
2628
$this->chain = [];
29+
$this->pendingChain = [];
2730
$this->rejection = null;
2831

2932
$this->executor = $executor;
@@ -79,13 +82,6 @@ private function complete(
7982
callable $onFulfilled = null,
8083
callable $onRejected = null
8184
):void {
82-
if(isset($this->rejection)) {
83-
$this->state = HttpPromiseInterface::REJECTED;
84-
}
85-
elseif(isset($this->resolvedValue)) {
86-
$this->state = HttpPromiseInterface::FULFILLED;
87-
}
88-
8985
if($onFulfilled || $onRejected) {
9086
$this->then($onFulfilled, $onRejected);
9187
}
@@ -125,18 +121,34 @@ private function handleChain():void {
125121
}
126122
}
127123
else {
128-
$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;
124+
if(isset($this->resolvedValue)) {
125+
$value = $then->callOnFulfilled($this->resolvedValue);
126+
127+
if($value instanceof PromiseInterface) {
128+
unset($this->resolvedValue);
129+
130+
array_push($this->pendingChain, $this->chain[0] ?? null);
131+
132+
$value->then(function($resolvedValue) {
133+
$this->resolvedValue = $resolvedValue;
134+
$then = array_pop($this->pendingChain);
135+
if($then) {
136+
$then->callOnFulfilled($this->resolvedValue);
137+
$this->resolvedValue = null;
138+
}
139+
$this->complete();
140+
});
141+
break;
142+
}
143+
144+
$this->state = HttpPromiseInterface::FULFILLED;
145+
if(!is_null($value)) {
146+
$this->resolvedValue = $value;
147+
}
135148
}
136-
137-
$this->state = HttpPromiseInterface::FULFILLED;
138-
if(!is_null($value)) {
139-
$this->resolvedValue = $value;
149+
elseif($then instanceof FinallyChain
150+
&& isset($this->rejection)) {
151+
$then->callOnFulfilled($this->rejection);
140152
}
141153
}
142154
}
@@ -145,10 +157,19 @@ private function handleChain():void {
145157
}
146158
}
147159

148-
if(!$emptyChain
149-
&& $reason = array_shift($rejectedForwardQueue)) {
160+
$reason = array_shift($rejectedForwardQueue);
161+
if($reason && !$emptyChain) {
150162
throw $reason;
151163
}
164+
165+
if($emptyChain) {
166+
if($reason) {
167+
$this->state = HttpPromiseInterface::REJECTED;
168+
}
169+
else {
170+
$this->state = HttpPromiseInterface::FULFILLED;
171+
}
172+
}
152173
}
153174

154175
public function getState():string {
@@ -212,11 +233,9 @@ private function resolve($value):void {
212233
}
213234

214235
$this->resolvedValue = $value;
215-
$this->state = HttpPromiseInterface::FULFILLED;
216236
}
217237

218238
private function reject(Throwable $reason):void {
219239
$this->rejection = $reason;
220-
$this->state = HttpPromiseInterface::REJECTED;
221240
}
222241
}

test/phpunit/PromiseTest.php

+91-8
Original file line numberDiff line numberDiff line change
@@ -583,15 +583,20 @@ public function testNoCatchMethodBubblesThrowables() {
583583
public function testWait() {
584584
$callCount = 0;
585585
$resolveCallback = null;
586-
$executor = function(callable $resolve, callable $reject) use(&$resolveCallback):void {
586+
$rejectCallback = null;
587+
$completeCallback = null;
588+
$executor = function(callable $resolve, callable $reject, callable $complete) use(&$resolveCallback, &$rejectCallback, &$completeCallback):void {
587589
$resolveCallback = $resolve;
590+
$rejectCallback = $reject;
591+
$completeCallback = $complete;
588592
};
589593
$resolvedValue = "Done!";
590594
$sut = new Promise($executor);
591595

592-
$waitTask = function() use(&$callCount, $resolveCallback, $resolvedValue) {
596+
$waitTask = function() use(&$callCount, $resolveCallback, $resolvedValue, $completeCallback) {
593597
if($callCount >= 10) {
594598
call_user_func($resolveCallback, $resolvedValue);
599+
call_user_func($completeCallback);
595600
}
596601
else {
597602
$callCount++;
@@ -606,15 +611,20 @@ public function testWait() {
606611
public function testWaitNotUnwrapped() {
607612
$callCount = 0;
608613
$resolveCallback = null;
609-
$executor = function(callable $resolve, callable $reject) use(&$resolveCallback):void {
614+
$rejectCallback = null;
615+
$completeCallback = null;
616+
$executor = function(callable $resolve, callable $reject, callable $complete) use(&$resolveCallback, &$rejectCallback, &$completeCallback):void {
610617
$resolveCallback = $resolve;
618+
$rejectCallback = $reject;
619+
$completeCallback = $complete;
611620
};
612621
$resolvedValue = "Done!";
613622
$sut = new Promise($executor);
614623

615-
$waitTask = function() use(&$callCount, $resolveCallback, $resolvedValue) {
624+
$waitTask = function() use(&$callCount, $resolveCallback, $resolvedValue, $completeCallback) {
616625
if($callCount >= 10) {
617626
call_user_func($resolveCallback, $resolvedValue);
627+
call_user_func($completeCallback);
618628
}
619629
else {
620630
$callCount++;
@@ -629,18 +639,23 @@ public function testWaitNotUnwrapped() {
629639
public function testWaitUnwrapsFinalValue() {
630640
$callCount = 0;
631641
$resolveCallback = null;
632-
$executor = function(callable $resolve, callable $reject) use(&$resolveCallback):void {
642+
$rejectCallback = null;
643+
$completeCallback = null;
644+
$executor = function(callable $resolve, callable $reject, callable $complete) use(&$resolveCallback, &$rejectCallback, &$completeCallback):void {
633645
$resolveCallback = $resolve;
646+
$rejectCallback = $reject;
647+
$completeCallback = $complete;
634648
};
635649
$resolvedValue = "Done!";
636650
$sut = new Promise($executor);
637651
$sut->then(function($fulfilled) {
638652
return "Returned from within onFulfilled!";
639653
});
640654

641-
$waitTask = function() use(&$callCount, $resolveCallback, $resolvedValue) {
655+
$waitTask = function() use(&$callCount, $resolveCallback, $resolvedValue, $completeCallback) {
642656
if($callCount >= 10) {
643657
call_user_func($resolveCallback, $resolvedValue);
658+
call_user_func($completeCallback);
644659
}
645660
else {
646661
$callCount++;
@@ -680,12 +695,80 @@ public function testFulfilledReturnsNewPromiseThatIsResolved() {
680695
return $messagePromise;
681696
})->then(self::mockCallable(1, "Your number is 105"));
682697

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.
685698
$numberPromiseContainer->resolve(105);
686699
$messagePromiseContainer->resolve("Your number is $numberToResolveWith");
687700
}
688701

702+
/**
703+
* Similar test to the one above, but done in a different style.
704+
* Closer to a real-world usage, this emulates getting a person's
705+
* address from their name, from an external list.
706+
*/
707+
public function testFulfilledReturnsNewPromiseThatIsResolved2() {
708+
// Our fake data source that will be "searched" by a deferred task (not using an
709+
// actual Deferred object, but instead, longhand performing a loop outside
710+
// of the Promise callback).
711+
$addressBook = [
712+
"Adrian Appleby" => "16B Acorn Grove",
713+
"Bentley Buttersworth" => "59 Brambetwicket Drive",
714+
"Cacey Coggleton" => "10 Cambridge Road",
715+
];
716+
// The search term used to resolve the first promise with.
717+
$searchTerm = null;
718+
// We will store any parameters received by the promise fulfilment callbacks.
719+
$receivedNames = [];
720+
$receivedAddresses = [];
721+
722+
// All references to the various callbacks, usually handled by a Deferred:
723+
$fulfill = null;
724+
$reject = null;
725+
$complete = null;
726+
$innerFulfill = null;
727+
$innerReject = null;
728+
$innerComplete = null;
729+
$innerPromise = null;
730+
731+
$sut = new Promise(function($f, $r, $c) use(&$fulfill, &$reject, &$complete) {
732+
$fulfill = $f;
733+
$reject = $r;
734+
$complete = $c;
735+
});
736+
737+
// Define asynchronous behaviour:
738+
$sut->then(function(string $name) use(&$innerFulfil, &$innerReject, &$innerComplete, &$innerPromise, &$searchTerm, &$receivedNames) {
739+
array_push($receivedNames, $name);
740+
$searchTerm = $name;
741+
742+
$innerPromise = new Promise(function($f, $r, $c) use(&$innerFulfil, &$innerReject, &$innerComplete) {
743+
$innerFulfil = $f;
744+
$innerReject = $r;
745+
$innerComplete = $c;
746+
});
747+
return $innerPromise;
748+
})->then(function(string $address) use(&$receivedAddresses) {
749+
array_push($receivedAddresses, $address);
750+
});
751+
752+
// This is the "user code" that initiates the search.
753+
// Completing the promise resolution with "Butter" will call the Promise's
754+
// onFulfilled callback, thus our $searchTerm variable should contain "Butter".
755+
call_user_func($fulfill, "Butter");
756+
call_user_func($complete);
757+
self::assertEquals("Butter", $searchTerm);
758+
759+
// This is the deferred task for the search:
760+
foreach($addressBook as $name => $address) {
761+
if(strstr($name, $searchTerm)) {
762+
call_user_func($innerFulfil, $address);
763+
call_user_func($innerComplete);
764+
}
765+
}
766+
767+
self::assertCount(1, $receivedNames);
768+
self::assertCount(1, $receivedAddresses);
769+
self::assertEquals($addressBook["Bentley Buttersworth"], $receivedAddresses[0]);
770+
}
771+
689772
protected function getTestPromiseContainer():TestPromiseContainer {
690773
$resolveCallback = null;
691774
$rejectCallback = null;

0 commit comments

Comments
 (0)