Skip to content

Commit 1d80710

Browse files
author
Greg Bowler
committed
Use php.gt/promise for Promise implementation
1 parent 180c7ea commit 1d80710

6 files changed

Lines changed: 168 additions & 46 deletions

File tree

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
<?php
22
/**
3-
* This example counts the number of vowels in example1.txt and the number of
4-
* consonants in example2.txt, showing how concurrent slow file reads can
5-
* use Promises to defer work, with a future callback when the work is complete.
3+
* This example counts the number of vowels and the number of consonants
4+
* in example.txt, showing how concurrent slow file reads can use Promises
5+
* to defer work, with a future callback when the work is complete.
66
*
7-
* A PeriodicLoop is used with a long period and the file reading code
8-
* is done one byte at a time, to simulate a slow connection.
7+
* A PeriodicLoop is used with a purposefully long period with the file
8+
* reading code being done one byte at a time, to simulate a slow connection.
99
*
1010
* Note: This is an example wrapped in a class, showing an example of how a
1111
* framework could offer promise-driven filesystem functionality.
1212
*/
1313

1414
use Gt\Async\Loop;
15-
use Gt\Async\Promise\Deferred;
16-
use Gt\Async\Promise\Promise;
1715
use Gt\Async\Timer\PeriodicTimer;
16+
use Gt\Promise\Deferred;
17+
use Gt\Promise\PromiseInterface;
1818

1919
require("../vendor/autoload.php");
2020

@@ -35,10 +35,10 @@ public function setLoop(Loop $loop):void {
3535

3636
// This is the public function that will be called, returning a Promise that
3737
// represents the completed work.
38-
public function countCharacters(string $charMap):Promise {
38+
public function countCharacters(string $charMap):PromiseInterface {
3939
// A new Deferred is created to assign this class's specific process function.
4040
$this->deferred = new Deferred();
41-
$this->deferred->addProcessFunction(
41+
$this->deferred->addProcess(
4242
fn() => $this->processNextCharacter($charMap)
4343
);
4444
// The Deferred is added to the Loop's default timer, which will call its
@@ -52,9 +52,7 @@ public function countCharacters(string $charMap):Promise {
5252
// This functions is called by the Deferred, as the Deferred is invoked by the
5353
// background Loop. It must not do much work per call, as to not block the
5454
// execution of other deferred tasks.
55-
public function processNextCharacter(string $charMap):void {
56-
// TODO: Nice. Got this far. Now it's time to finish Promise implementation,
57-
// and then we can complete this example's functionality here.
55+
private function processNextCharacter(string $charMap):void {
5856
if(!isset($this->characterCount)) {
5957
$this->characterCount = 0;
6058
}
@@ -80,24 +78,40 @@ public function processNextCharacter(string $charMap):void {
8078
// second. This is actually quite slow, used to illustrate how concurrent tasks
8179
// will behave.
8280
$timer = new PeriodicTimer(0.1, true);
81+
$timer->addCallback(function() {
82+
echo ".";
83+
});
84+
85+
// This loop will be called to run forever by the run() function at the bottom
86+
// of this file, but here we are setting the loop to halt if all internal
87+
// Deferred objects complete.
8388
$loop = new Loop();
8489
$loop->addTimer($timer);
8590
$loop->haltWhenAllDeferredComplete(true);
8691

92+
// Create the example classes to slowly loop over the characters of the file.
8793
$reader1 = new SlowFileReader("example.txt");
8894
$reader1->setLoop($loop);
8995
$reader2 = new SlowFileReader("example.txt");
9096
$reader2->setLoop($loop);
9197

92-
$reader1->countCharacters("aeiou");
93-
//->then(function(int $numVowels):void {
94-
// echo "Example text has $numVowels vowels.", PHP_EOL;
95-
//});
96-
//
97-
//$reader2->countCharacters("bcdfghjklmnpqrstvwxyz")
98-
//->then(function(int $numConsonants):void {
99-
// echo "Example text has $numConsonants consonants.", PHP_EOL;
100-
//});
98+
// The countCharacters function returns a Promise, meaning it will not actually
99+
// undertake any work itself, but will resolve the promise when the work
100+
// completes. The work is undertaken by the Deferred object, which is triggered
101+
// by the Loop's timers.
102+
$reader1->countCharacters("aeiou")
103+
->then(function(int $numVowels):void {
104+
echo "Example text has $numVowels vowels.", PHP_EOL;
105+
});
106+
107+
// Another Promise can be added, so their Deferred's work is undertaken
108+
// concurrently.
109+
$reader2->countCharacters("bcdfghjklmnpqrstvwxyz")
110+
->then(function(int $numConsonants):void {
111+
echo "Example text has $numConsonants consonants.", PHP_EOL;
112+
});
101113

114+
// Here we execute the loop, which has been set to halt when all Deferred
115+
// objects complete.
102116
$loop->run();
103117
echo "Complete!", PHP_EOL;

src/Loop.php

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?php
22
namespace Gt\Async;
33

4-
use Gt\Async\Promise\Deferred;
54
use Gt\Async\Timer\Timer;
65
use Gt\Async\Timer\TimerOrder;
6+
use Gt\Promise\Deferred;
77

88
/**
99
* The core event loop class, used to dispatch all events via different added
@@ -25,6 +25,8 @@ class Loop {
2525
private bool $haltWhenAllDeferredComplete;
2626
/** @var Deferred[] */
2727
private array $activeDeferred;
28+
/** @var callable[] */
29+
private array $haltCallbackList;
2830

2931
public function __construct() {
3032
$this->timerList = [];
@@ -37,6 +39,7 @@ public function __construct() {
3739
};
3840
$this->haltWhenAllDeferredComplete = false;
3941
$this->activeDeferred = [];
42+
$this->haltCallbackList = [];
4043
}
4144

4245
public function addTimer(Timer $timer):void {
@@ -49,28 +52,29 @@ public function addDeferredToTimer(
4952
):void {
5053
$timer = $timer ?? $this->timerList[0];
5154

52-
foreach($deferred->getProcessFunctionArray() as $function) {
53-
$timer->addCallback($function);
54-
}
55-
$timer->addCallback(function() use($deferred, $timer) {
56-
if(!$deferred->isActive()) {
55+
$deferred->addCompleteCallback(
56+
function() use ($deferred, $timer) {
5757
$this->removeDeferredFromTimer(
5858
$deferred,
5959
$timer
6060
);
61-
}
62-
});
61+
});
62+
63+
foreach($deferred->getProcessList() as $function) {
64+
$timer->addCallback($function);
65+
}
6366

6467
$this->activeDeferred[] = $deferred;
6568
}
6669

70+
6771
public function removeDeferredFromTimer(
6872
Deferred $deferred,
6973
Timer $timer = null
7074
):void {
7175
$timer = $timer ?? $this->timerList[0];
7276

73-
foreach($deferred->getProcessFunctionArray() as $function) {
77+
foreach($deferred->getProcessList() as $function) {
7478
$timer->removeCallback($function);
7579
}
7680
$activeDeferredIndex = array_search(
@@ -107,6 +111,20 @@ public function run(bool $forever = true):void {
107111

108112
public function halt():void {
109113
$this->forever = false;
114+
115+
foreach($this->haltCallbackList as $callback) {
116+
call_user_func($callback);
117+
}
118+
}
119+
120+
public function haltWhenAllDeferredComplete(
121+
bool $shouldHalt = true
122+
):void {
123+
$this->haltWhenAllDeferredComplete = $shouldHalt;
124+
}
125+
126+
public function addHaltCallback(callable $callback):void {
127+
array_push($this->haltCallbackList, $callback);
110128
}
111129

112130
public function getTriggerCount():int {
@@ -123,10 +141,6 @@ public function waitUntil(float $waitUntilEpoch):void {
123141
call_user_func($this->sleepFunction, $diff);
124142
}
125143

126-
public function haltWhenAllDeferredComplete(bool $halt):void {
127-
$this->haltWhenAllDeferredComplete = $halt;
128-
}
129-
130144
private function triggerNextTimers():int {
131145
$timerOrder = new TimerOrder($this->timerList);
132146

src/Timer/Timer.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
<?php
22
namespace Gt\Async\Timer;
33

4-
use Gt\Async\Asyncable;
5-
64
/**
75
* Represents one or more trigger times. If the tick function is called when
86
* a timer is due, it will execute the timer's callback(s).
@@ -29,11 +27,10 @@ public function setTimeFunction(callable $callable):void {
2927
}
3028

3129
public function addCallback(callable $callback):void {
32-
$this->callbackList[] = $callback;
30+
array_push($this->callbackList, $callback);
3331
}
3432

3533
public function removeCallback(callable $callback):void {
36-
// TODO: Throw exception if it doesn't exist.
3734
$callbackIndex = array_search($callback, $this->callbackList);
3835
unset($this->callbackList[$callbackIndex]);
3936
}

test/phpunit/LoopTest.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
use Gt\Async\Loop;
55
use Gt\Async\Timer\Timer;
6+
use Gt\Promise\Deferred;
7+
use PHPUnit\Framework\MockObject\MockObject;
68
use PHPUnit\Framework\TestCase;
9+
use stdClass;
710

811
class LoopTest extends TestCase {
912
public function testRunWithNoTimer() {
@@ -170,4 +173,85 @@ public function testSleepActuallyDelays() {
170173
round($endEpoch, 2)
171174
);
172175
}
176+
177+
public function testAddDeferredToTimerNoTimerPassed() {
178+
$callback = function(){};
179+
180+
$timer = self::createMock(Timer::class);
181+
$timer->expects(self::once())
182+
->method("addCallback")
183+
->with(self::identicalTo($callback));
184+
185+
$deferred = self::createMock(Deferred::class);
186+
$deferred->expects(self::once())
187+
->method("getProcessList")
188+
->willReturn([$callback]);
189+
190+
$sut = new Loop();
191+
$sut->addTimer($timer);
192+
$sut->addDeferredToTimer($deferred);
193+
}
194+
195+
public function testAddDeferredToTimerSpecificTimer() {
196+
$callback = function(){};
197+
198+
$timer1 = self::createMock(Timer::class);
199+
$timer1->expects(self::never())
200+
->method("addCallback");
201+
$timer2 = self::createMock(Timer::class);
202+
$timer2->expects(self::once())
203+
->method("addCallback")
204+
->with(self::identicalTo($callback));
205+
206+
$deferred = self::createMock(Deferred::class);
207+
$deferred->expects(self::once())
208+
->method("getProcessList")
209+
->willReturn([$callback]);
210+
211+
$sut = new Loop();
212+
$sut->addTimer($timer1);
213+
$sut->addTimer($timer2);
214+
$sut->addDeferredToTimer($deferred, $timer2);
215+
}
216+
217+
public function testHaltWhenAllDeferredComplete() {
218+
$deferredCompleteCallback = null;
219+
$deferredProcess = function(){};
220+
221+
$deferred = self::createMock(Deferred::class);
222+
$deferred->expects(self::once())
223+
->method("addCompleteCallback")
224+
->willReturnCallback(function(callable $cb) use(&$deferredCompleteCallback) {
225+
$deferredCompleteCallback = $cb;
226+
});
227+
$deferred->expects(self::exactly(2))
228+
->method("getProcessList")
229+
->willReturn([$deferredProcess]);
230+
231+
$timer = self::createMock(Timer::class);
232+
$timer->expects(self::once())
233+
->method("addCallback")
234+
->with(self::identicalTo($deferredProcess));
235+
$timer->expects(self::once())
236+
->method("removeCallback")
237+
->with(self::identicalTo($deferredProcess));
238+
239+
/** @var MockObject|callable $haltCallback */
240+
$haltCallback = self::getMockBuilder(stdClass::class)
241+
->addMethods(["__invoke"])
242+
->getMock();
243+
$haltCallback->expects(self::once())
244+
->method("__invoke");
245+
246+
$sut = new Loop();
247+
$sut->addHaltCallback($haltCallback);
248+
$sut->haltWhenAllDeferredComplete();
249+
$sut->addTimer($timer);
250+
$sut->addDeferredToTimer($deferred, $timer);
251+
$sut->run();
252+
253+
// Simulate the Deferred object calling its complete callback:
254+
self::assertIsCallable($deferredCompleteCallback);
255+
call_user_func($deferredCompleteCallback);
256+
}
173257
}

test/phpunit/Promise/PromiseTest.php

Lines changed: 0 additions & 8 deletions
This file was deleted.

test/phpunit/Timer/IndividualTimerTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,25 @@ public function testAddCallbackTriggeredAtCorrectTime() {
127127
$sut->tick(); // 1006
128128
self::assertEquals(4, $callbackCount);
129129
}
130+
131+
public function testRemoveCallback() {
132+
$exampleCallbackCount = 0;
133+
$exampleCallback = function() use(&$exampleCallbackCount) {
134+
$exampleCallbackCount++;
135+
};
136+
137+
$epoch = 1000;
138+
$sut = new IndividualTimer();
139+
$sut->addTriggerTime($epoch);
140+
$sut->addTriggerTime($epoch + 1);
141+
$sut->setTimeFunction(function() use(&$epoch) {
142+
return $epoch++;
143+
});
144+
145+
$sut->addCallback($exampleCallback);
146+
$sut->tick();
147+
$sut->removeCallback($exampleCallback);
148+
$sut->tick();
149+
self::assertEquals(1, $exampleCallbackCount);
150+
}
130151
}

0 commit comments

Comments
 (0)