Skip to content

Commit b675727

Browse files
author
Greg Bowler
committed
WIP: Promise implementation
1 parent c7cc45d commit b675727

11 files changed

Lines changed: 375 additions & 0 deletions

File tree

example/03-slow-file-reads.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
/**
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.
6+
*
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.
9+
*
10+
* Note: This is an example wrapped in a class, showing an example of how a
11+
* framework could offer promise-driven filesystem functionality.
12+
*/
13+
14+
use Gt\Async\Loop;
15+
use Gt\Async\Promise\Deferred;
16+
use Gt\Async\Promise\Promise;
17+
use Gt\Async\Timer\PeriodicTimer;
18+
19+
require("../vendor/autoload.php");
20+
21+
class SlowFileReader {
22+
private SplFileObject $file;
23+
private Loop $loop;
24+
private Deferred $deferred;
25+
private int $characterCount;
26+
27+
public function __construct(string $filename) {
28+
$this->file = new SplFileObject($filename);
29+
}
30+
31+
// We need to inject the background loop that will dispatch the processing calls.
32+
public function setLoop(Loop $loop):void {
33+
$this->loop = $loop;
34+
}
35+
36+
// This is the public function that will be called, returning a Promise that
37+
// represents the completed work.
38+
public function countCharacters(string $charMap):Promise {
39+
// A new Deferred is created to assign this class's specific process function.
40+
$this->deferred = new Deferred();
41+
$this->deferred->addProcessFunction(
42+
fn() => $this->processNextCharacter($charMap)
43+
);
44+
// The Deferred is added to the Loop's default timer, which will call its
45+
// process function each tick.
46+
$this->loop->addDeferredToTimer($this->deferred);
47+
// The Deferred creates its own Promise, so it knows what to resolve when the
48+
// work is complete.
49+
return $this->deferred->getPromise();
50+
}
51+
52+
// This functions is called by the Deferred, as the Deferred is invoked by the
53+
// background Loop. It must not do much work per call, as to not block the
54+
// 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.
58+
if(!isset($this->characterCount)) {
59+
$this->characterCount = 0;
60+
}
61+
62+
if($this->file->eof()) {
63+
$this->deferred->resolve($this->characterCount);
64+
return;
65+
}
66+
67+
$char = $this->file->fread(1);
68+
$char = strtolower($char);
69+
if(strlen($char) <= 0) {
70+
return;
71+
}
72+
73+
if(strstr($charMap, $char)) {
74+
$this->characterCount++;
75+
}
76+
}
77+
}
78+
79+
// A periodic timer will be used to call the deferred tasks ten times per
80+
// second. This is actually quite slow, used to illustrate how concurrent tasks
81+
// will behave.
82+
$timer = new PeriodicTimer(0.1, true);
83+
$loop = new Loop();
84+
$loop->addTimer($timer);
85+
$loop->haltWhenAllDeferredComplete(true);
86+
87+
$reader1 = new SlowFileReader("example.txt");
88+
$reader1->setLoop($loop);
89+
$reader2 = new SlowFileReader("example.txt");
90+
$reader2->setLoop($loop);
91+
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+
//});
101+
102+
$loop->run();
103+
echo "Complete!", PHP_EOL;

example/example.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The quick brown fox jumped over the lazy dogs.

src/Loop.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22
namespace Gt\Async;
33

4+
use Gt\Async\Promise\Deferred;
45
use Gt\Async\Timer\Timer;
56
use Gt\Async\Timer\TimerOrder;
67

@@ -21,6 +22,9 @@ class Loop {
2122
/** @var callable Function that delivers the current time in milliseconds as a float */
2223
private $timeFunction;
2324
private bool $forever;
25+
private bool $haltWhenAllDeferredComplete;
26+
/** @var Deferred[] */
27+
private array $activeDeferred;
2428

2529
public function __construct() {
2630
$this->timerList = [];
@@ -31,12 +35,58 @@ public function __construct() {
3135
$this->timeFunction = function():float {
3236
return microtime(true);
3337
};
38+
$this->haltWhenAllDeferredComplete = false;
39+
$this->activeDeferred = [];
3440
}
3541

3642
public function addTimer(Timer $timer):void {
3743
$this->timerList [] = $timer;
3844
}
3945

46+
public function addDeferredToTimer(
47+
Deferred $deferred,
48+
Timer $timer = null
49+
):void {
50+
$timer = $timer ?? $this->timerList[0];
51+
52+
foreach($deferred->getProcessFunctionArray() as $function) {
53+
$timer->addCallback($function);
54+
}
55+
$timer->addCallback(function() use($deferred, $timer) {
56+
if(!$deferred->isActive()) {
57+
$this->removeDeferredFromTimer(
58+
$deferred,
59+
$timer
60+
);
61+
}
62+
});
63+
64+
$this->activeDeferred[] = $deferred;
65+
}
66+
67+
public function removeDeferredFromTimer(
68+
Deferred $deferred,
69+
Timer $timer = null
70+
):void {
71+
$timer = $timer ?? $this->timerList[0];
72+
73+
foreach($deferred->getProcessFunctionArray() as $function) {
74+
$timer->removeCallback($function);
75+
}
76+
$activeDeferredIndex = array_search(
77+
$deferred,
78+
$this->activeDeferred
79+
);
80+
if($activeDeferredIndex !== false) {
81+
unset($this->activeDeferred[$activeDeferredIndex]);
82+
}
83+
84+
if($this->haltWhenAllDeferredComplete
85+
&& empty($this->activeDeferred)) {
86+
$this->halt();
87+
}
88+
}
89+
4090
public function setSleepFunction(callable $sleepFunction):void {
4191
$this->sleepFunction = $sleepFunction;
4292
}
@@ -73,6 +123,10 @@ public function waitUntil(float $waitUntilEpoch):void {
73123
call_user_func($this->sleepFunction, $diff);
74124
}
75125

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

src/Promise/Canceller.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
namespace Gt\Async\Promise;
3+
4+
class Canceller {
5+
6+
}

src/Promise/Deferred.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
namespace Gt\Async\Promise;
3+
4+
use Throwable;
5+
6+
class Deferred {
7+
private Resolver $resolver;
8+
private Promise $promise;
9+
private array $processFunctionArray;
10+
private bool $isActive;
11+
12+
public function __construct(
13+
Canceller $canceller = null,
14+
Resolver $resolver = null
15+
) {
16+
$this->resolver = $resolver ?? new Resolver();
17+
$this->promise = new Promise($this->resolver, $canceller);
18+
$this->processFunctionArray = [];
19+
$this->isActive = true;
20+
}
21+
22+
public function getPromise():Promise {
23+
return $this->promise;
24+
}
25+
26+
public function isActive():bool {
27+
return $this->isActive;
28+
}
29+
30+
/** @param mixed|null $value */
31+
public function resolve($value = null):void {
32+
$this->resolver->resolve($value);
33+
$this->isActive = false;
34+
}
35+
36+
public function reject(Throwable $reason):void {
37+
$this->resolver->reject($reason);
38+
$this->isActive = false;
39+
}
40+
41+
public function addProcessFunction(callable $function):void {
42+
$this->processFunctionArray[] = $function;
43+
}
44+
45+
/** @return callable[] */
46+
public function getProcessFunctionArray():array {
47+
return $this->processFunctionArray;
48+
}
49+
}

src/Promise/Promise.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
namespace Gt\Async\Promise;
3+
4+
use Gt\Async\AsyncException;
5+
use Throwable;
6+
7+
class Promise {
8+
private Resolver $resolver;
9+
private ?Canceller $canceller;
10+
private array $handlers;
11+
/** @var mixed */
12+
private $result;
13+
14+
public function __construct(
15+
Resolver $resolver,
16+
Canceller $canceller = null
17+
) {
18+
$this->resolver = $resolver;
19+
$this->canceller = $canceller;
20+
$this->handlers = [];
21+
22+
try {
23+
$this->resolver->call();
24+
}
25+
catch(Throwable $e) {
26+
$this->reject($e);
27+
}
28+
}
29+
30+
public function then(
31+
callable $onFulfilled = null,
32+
callable $onRejected = null
33+
):self {
34+
35+
}
36+
37+
public function catch(
38+
callable $onRejected = null
39+
):self {
40+
41+
}
42+
43+
public function cancel():void {
44+
45+
}
46+
47+
private function reject(Throwable $reason):void {
48+
if(is_null($this->result)) {
49+
$this->settle(new RejectedPromise($reason));
50+
}
51+
}
52+
53+
private function settle(PromiseInterface $promise) {
54+
$result = $this->unwrap($promise);
55+
56+
if($result === $this) {
57+
throw new AsyncException("A promise can't settle itself");
58+
}
59+
60+
if($result instanceof self) {
61+
$result->requiredCancelRequests++;
62+
}
63+
else {
64+
// TODO: React docs: Unset canceller only when not following a pending promise.
65+
$this->canceller = null;
66+
}
67+
68+
$handlers = $this->handlers;
69+
$this->handlers = [];
70+
$this->result = $result;
71+
72+
foreach($handlers as $handler) {
73+
$handler($result);
74+
}
75+
}
76+
77+
private function unwrap(PromiseInterface $promise):PromiseInterface {
78+
while($promise instanceof self
79+
&& !is_null($promise->result)) {
80+
$promise = $promise->result;
81+
}
82+
83+
return $promise;
84+
}
85+
}

src/Promise/PromiseInterface.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
namespace Gt\Async\Promise;
3+
4+
interface PromiseInterface {
5+
public function then(
6+
callable $onFulfilled = null,
7+
callable $onRejected = null
8+
):self;
9+
10+
public function catch(
11+
callable $onRejected = null
12+
):self;
13+
14+
public function done(
15+
callable $onFulfilled = null,
16+
callable $onRejected = null
17+
):void;
18+
19+
public function cancel():void;
20+
}

src/Promise/RejectedPromise.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
namespace Gt\Async\Promise;
3+
4+
use Throwable;
5+
6+
class RejectedPromise implements PromiseInterface {
7+
public function __construct(Throwable $reason) {
8+
9+
}
10+
11+
public function then(callable $onFulfilled = null, callable $onRejected = null):PromiseInterface {
12+
// TODO: Implement then() method.
13+
}
14+
15+
public function catch(callable $onRejected = null):PromiseInterface {
16+
// TODO: Implement catch() method.
17+
}
18+
19+
public function done(callable $onFulfilled = null, callable $onRejected = null):void {
20+
// TODO: Implement done() method.
21+
}
22+
23+
public function cancel():void {
24+
// TODO: Implement cancel() method.
25+
}
26+
}

0 commit comments

Comments
 (0)