-
-
Notifications
You must be signed in to change notification settings - Fork 2
Home
As PHP is inherently synchronous, running code asynchronously requires a background loop that observes and dispatches events while handling promise resolutions. PHP.GT/Async introduces an event loop, timers and a promise-based approach to handling deferred operations.
This package provides:
- Event loop - the core mechanism that continuously observes and dispatches events.
- Timers - schedule callbacks to execute after a delay or at specific times.
- Promise handling - enables deferred execution and resolution of asynchronous tasks.
This example shows how an IndividualTimer can be used to schedule some ticks in the future. Here, the callback function simply calculates how many seconds have passed since the script's start and echos. You can see the correlation with the output to the three trigger times that are added after creating the IndividualTimer object.
use Gt\Async\Loop;
use Gt\Async\Timer\IndividualTimer;
$timeAtScriptStart = microtime(true);
$timer = new IndividualTimer();
$timer->addTriggerTime($timeAtScriptStart + 1);
$timer->addTriggerTime($timeAtScriptStart + 5);
$timer->addTriggerTime($timeAtScriptStart + 10);
$timer->addCallback(function() use($timeAtScriptStart) {
$now = microtime(true);
$secondsPassed = round($now - $timeAtScriptStart);
echo "Number of seconds passed: $secondsPassed", PHP_EOL;
});
$loop = new Loop();
$loop->addTimer($timer);
echo "Starting...", PHP_EOL;
$loop->run();
This example shows how a PeriodicTimer can be used to schedule ticks forever, combined with an IndividualTimer that will cause the loop to halt after a specified period.
To articulate the use of asynchronous methods, a "tiny tick timer" is introduced that runs very frequently, alongside the countdown. You can imagine the concurrent timers being used to execute actual workloads.
use Gt\Async\Loop;
use Gt\Async\Timer\IndividualTimer;
use Gt\Async\Timer\PeriodicTimer;
$stopTimer = new IndividualTimer(5);
$periodicTimer = new PeriodicTimer(1, true);
$tinyTickTimer = new PeriodicTimer(0.1, true);
$countdownNumber = 5;
$loop = new Loop();
$periodicTimer->addCallback(function() use(&$countdownNumber) {
echo "Countdown: $countdownNumber", PHP_EOL;
$countdownNumber--;
});
$tinyTickTimer->addCallback(function() {
echo ".";
});
$stopTimer->addCallback(function() use($loop) {
echo "LIFT OFF!", PHP_EOL;
$loop->halt();
});
$loop->addTimer($periodicTimer);
$loop->addTimer($tinyTickTimer);
$loop->addTimer($stopTimer);
echo "Starting...", PHP_EOL;
$loop->run();
This example counts the number of vowels and the number of consonants in example.txt, showing how concurrent slow file reads can use Promises to defer work, with a future callback when the work is complete.
A PeriodicLoop
is used with a purposefully long period with the file reading code being done one byte at a time, to simulate a slow connection.
Note: This is an example wrapped in a class, showing an example of how a framework could offer promise-driven filesystem functionality.
use Gt\Async\Loop;
use Gt\Async\Timer\PeriodicTimer;
use Gt\Promise\Deferred;
use Gt\Promise\PromiseInterface;
class SlowFileReader {
private SplFileObject $file;
private Loop $loop;
private Deferred $deferred;
private int $characterCount;
public function __construct(string $filename) {
$this->file = new SplFileObject($filename);
}
// We need to inject the background loop that will dispatch the processing calls.
public function setLoop(Loop $loop):void {
$this->loop = $loop;
}
// This is the public function that will be called, returning a Promise that
// represents the completed work.
public function countCharacters(string $charMap):PromiseInterface {
// A new Deferred is created to assign this class's specific process function.
$this->deferred = new Deferred();
$this->deferred->addProcess(
fn() => $this->processNextCharacter($charMap)
);
// The Deferred is added to the Loop's default timer, which will call its
// process function each tick.
$this->loop->addDeferredToTimer($this->deferred);
// The Deferred creates its own Promise, so it knows what to resolve when the
// work is complete.
return $this->deferred->getPromise();
}
// This functions is called by the Deferred, as the Deferred is invoked by the
// background Loop. It must not do much work per call, as to not block the
// execution of other deferred tasks.
private function processNextCharacter(string $charMap):void {
if(!isset($this->characterCount)) {
$this->characterCount = 0;
}
if($this->file->eof()) {
// When the file reaches the end, this deferred task is finished. We can now
// resolve the Deferred with the final value.
$this->deferred->resolve($this->characterCount);
return;
}
$char = $this->file->fread(1);
$char = strtolower($char);
if(strlen($char) <= 0) {
return;
}
if(strstr($charMap, $char)) {
$this->characterCount++;
}
}
}
// A periodic timer will be used to call the deferred tasks ten times per
// second. This is actually quite slow, used to illustrate how concurrent tasks
// will behave.
$timer = new PeriodicTimer(0.1, true);
$timer->addCallback(function() {
echo ".";
});
// This loop will be called to run forever by the run() function at the bottom
// of this file, but here we are setting the loop to halt if all internal
// Deferred objects complete.
$loop = new Loop();
$loop->addTimer($timer);
$loop->haltWhenAllDeferredComplete(true);
$loop->addHaltCallback(function() {
echo PHP_EOL, "Loop has halted because all Deferred tasks are complete.";
});
// Create the example classes to slowly loop over the characters of the file.
$reader1 = new SlowFileReader("example.txt");
$reader1->setLoop($loop);
$reader2 = new SlowFileReader("example.txt");
$reader2->setLoop($loop);
// The countCharacters function returns a Promise, meaning it will not actually
// undertake any work itself, but will resolve the promise when the work
// completes. The work is undertaken by the Deferred object, which is triggered
// by the Loop's timers.
$reader1->countCharacters("aeiou")
->then(function(int $numVowels):void {
echo PHP_EOL, "Example text has $numVowels vowels.";
});
// Another Promise can be added, so their Deferred's work is undertaken
// concurrently.
$reader2->countCharacters("bcdfghjklmnpqrstvwxyz")
->then(function(int $numConsonants):void {
echo PHP_EOL, "Example text has $numConsonants consonants.";
});
// Here we execute the loop, which has been set to halt when all Deferred
// objects complete.
$loop->run();
echo PHP_EOL, "Complete!", PHP_EOL;