Timer: scheduling and blocking boundaries
Model and public API
Section titled “Model and public API”The class is localzet\Timer. Intervals are floating-point seconds; callbacks receive the supplied args array as positional arguments. Timers execute on the selected event loop in the current process. A timer is not a durable scheduled job and does not coordinate workers.
| Method | Behavior |
|---|---|
| init(?EventInterface $event = null): void | Use the given loop, Server global loop or a new Select loop |
| repeat(float $interval, callable $callback, array $args = []): int | Repeating timer ID |
| delay(float $delay, callable $callback, array $args = []): int | One-shot timer ID |
| add(float $interval, callable $callback, ?array $args = [], bool $persistent = true): int | Compatibility wrapper: repeat or delay |
| del(int $id): bool | Remove a timer; false if no matching registration |
| delAll(): void | Delete every timer on the selected loop, including other application timers |
| getAll(): array | Diagnostics ['count' => integer], not a list of timer definitions |
| sleep(float $delay): void | Suspend only with a SuspensionCapableInterface backend; otherwise blocking usleep |
| signalHandle(): void | Compatibility no-op; no separate SIGALRM scheduler |
Negative intervals throw RuntimeException. Application input must also reject non-finite numbers and bound its maximum; the current negative-only validation is not a complete user-input policy. Precision depends on the loop/OS and load. Zero does not mean a guaranteed periodic frequency.
Complete executable example
Section titled “Complete executable example”This explicitly drives a Select loop without starting a Server: cancellation, a one-shot callback with an argument, loop stop and the diagnostic count are verified.
<?php
declare(strict_types=1);
use localzet\Timer;use localzet\Server\Events\Select;
require __DIR__ . '/vendor/autoload.php';
$loop = new Select();Timer::init($loop);$messages = [];$cancelled = Timer::repeat(0.005, static function (): void { throw new RuntimeException('Cancelled timer executed');});Timer::del($cancelled);Timer::delay(0.01, static function (string $message) use (&$messages): void { $messages[] = $message;}, ['hello']);Timer::delay(0.03, static function () use ($loop): void { $loop->stop();});$loop->run();
if ($messages !== ['hello'] || Timer::getAll() !== ['count' => 0]) { throw new RuntimeException('Unexpected timer state');}echo "timer-example-ok\n";php timers.phpPlace it next to vendor/autoload.php. Expected output: timer-example-ok. Use the recipe setup for the pinned source revision.
Worker and connection lifetime
Section titled “Worker and connection lifetime”Register timers in onServerStart/onConnect as appropriate, not in the pre-fork master for request-specific work. Keep IDs in the owning object/context and delete them on connection close. A closure capturing a connection keeps that object alive. Do not call delAll() to clean up one connection. Repeating callbacks must be short, handle failures deliberately and avoid overlapping external work through explicit coordination.
Timer::sleep is not universally asynchronous. With Select it blocks the worker; with a suspension-capable backend the call still needs a supported coroutine/Fiber context. Prefer delay/repeat for simple scheduling. An event loop alone does not make blocking HTTP, SQL or file access asynchronous.