wyrihaximus/ticking-promise
A ReactPHP promise that fires a callback on a recurring interval, making it simple to schedule periodic tasks inside asynchronous PHP applications without manual event-loop timer wiring.
composer require wyrihaximus/ticking-promise
The package does one thing: it turns "run on the next event loop tick" into a react/promise PromiseInterface you can then(), await(), all() or return from a callback. Instead of Loop::futureTick() (which returns void and gives you no handle on completion), you get a value that participates in promise chains.
Where to look first: read src/ in the repository. The whole surface is a single small class, so confirm the factory signature (notably whether it takes an explicit LoopInterface or uses the global loop from react/event-loop) before writing code against it. There is no config, no service provider, no facades.
First use case: cooperative yielding inside a long synchronous loop so timers, streams and other coroutines get a chance to run.
use WyriHaximus\TickingPromise\TickingPromise;
use function React\Async\async;
use function React\Async\await;
$import = async(function (array $rows): void {
foreach ($rows as $index => $row) {
$this->handle($row); // CPU work, blocks the loop
if ($index % 100 === 0) {
await(TickingPromise::create()); // hand the loop back for one tick
}
}
});
$import($rows);
Loop::run(); // nothing happens without a running loop
If you prefer no coroutine layer, the same thing in plain promise style:
TickingPromise::create()->then(function (): void {
$this->flushBuffers();
});
Chunked / batched processing with backpressure. Yield a tick every N items, not every item. Every tick is a full loop iteration: cheap, but not free. Tune N by measuring.
Ordering work after queued callbacks. Because the promise resolves through the tick queue, anything already queued with futureTick() (and I/O plus timers dispatched in the same iteration) is processed before your then() handler runs. That makes the package a clean "after the current batch, before the next" primitive.
Sequencing steps without nesting. Chain ticks instead of nesting futureTick() closures:
TickingPromise::create()
->then(fn () => $this->stepOne())
->then(fn () => TickingPromise::create())
->then(fn () => $this->stepTwo())
->then(null, fn (Throwable $e) => $this->logger->error('pipeline failed', ['e' => $e]));
Fan-out with periodic breathing room. When spawning a large set of promises, interleave ticks so stream reads and timers are not starved by a single burst of then() callbacks:
$pending = [];
foreach ($urls as $url) {
$pending[] = $this->http->get($url);
if (count($pending) % 50 === 0) {
$pending[] = TickingPromise::create();
}
}
await(all($pending));
Deterministic tests. In wyrihaximus/async-test-utilities based tests, awaiting a tick is a reliable way to flush pending loop callbacks before asserting, instead of usleep() or ad hoc loop manipulation. Extend WyriHaximus\AsyncTestUtilities\AsyncTestCase and let the base class own the loop lifecycle.
Composition with real delays. A tick is zero elapsed time. For backoff or polling intervals combine it with react/promise-timer: tick to yield, sleep() to actually wait.
Deferring recursion. Recursive promise chains that re-enter immediately can grow the stack and monopolise an iteration. Insert a tick at each recursion step to flatten it into separate loop iterations.
Loop::run() (or an await() that drives the loop) is active. In a CLI command or queue worker that never starts the loop, the promise stays pending forever and looks like a hang.LoopInterface instance (or vice versa), the callback is queued on a loop that is not spinning. Verify which loop is used, and if the factory accepts one, always pass the same instance you run().Loop::stop() to break out of tick-driven work. It tears down everything else using the loop mid-flight and makes tests flaky. Let callbacks return and the loop drain naturally.then() handler rejects the derived promise with nobody listening. Always attach a rejection handler at the end of a tick-started chain, or route it through wyrihaximus/psr-3-callable-throwable-logger.composer.json, since a minor release is the entire API for a package this size.Deferred and ignore the result after cancelling.react/promise major in composer.json. On promise v3 you get catch() and finally() directly on the chain; on v2 you write then(null, $onRejected). Mixing majors across a dependency graph is the usual source of "method not found" in async Laravel code.How can I help you explore Laravel packages today?