How to Build a Pure PHP Timer with PCNTL and Signals

This article explains how to implement a pure‑PHP timer using CLI mode, the PCNTL extension, and SIGALRM signals, covering basic concepts, task storage structures, the Timer class implementation, example usage, and practical considerations for long‑running services.

21CTO
21CTO
21CTO
How to Build a Pure PHP Timer with PCNTL and Signals

Introduction

Timer tasks are common in web applications. Traditional solutions use crontab with shell scripts or rely on ignore_user_abort() and set_time_limit(), which have limitations. This guide shows how to create a pure PHP timer that runs independently of the browser.

Basic Knowledge

CLI: PHP command‑line mode, distinct from the typical FPM web mode.

Process: The basic execution unit with its own memory space and control block.

Inter‑process Communication (IPC): Mechanisms such as pipes, shared memory, signals, message queues, and sockets enable data exchange between processes.

PCNTL extension: Provides process control functions like pcntl_alarm().

Implementation Principle

Tasks are stored in a three‑dimensional array indexed by timestamps. Each entry contains the execution interval, callback function, arguments, and a persistence flag.

array(
    '1438156396' => array(
        array(1, array('Class','Func'), array(), true),
    )
);
// 1438156396 is the timestamp; the inner array represents interval, callback, arguments, persistence.

The timer uses a signal (SIGALRM) sent every second. The signal handler iterates over the task array, executes due tasks via callbacks, removes one‑time jobs, and re‑schedules persistent jobs.

Timer Class Code

<?php
/**
 * Timer
 */
class Timer {
    // Store all scheduled tasks
    public static $task = array();
    // Interval in seconds
    public static $time = 1;

    /**
     * Start the service
     * @param int $time
     */
    public static function run($time = null) {
        if ($time) {
            self::$time = $time;
        }
        self::installHandler();
        pcntl_alarm(1);
    }

    /** Register signal handler */
    public static function installHandler() {
        pcntl_signal(SIGALRM, array('Timer','signalHandler'));
    }

    /** Signal handler */
    public static function signalHandler() {
        self::task();
        // Schedule next alarm
        pcntl_alarm(self::$time);
    }

    /** Execute callbacks */
    public static function task() {
        if (empty(self::$task)) { return; }
        foreach (self::$task as $time => $arr) {
            $current = time();
            foreach ($arr as $k => $job) {
                $func = $job['func'];   // callback
                $argv = $job['argv'];   // arguments
                $interval = $job['interval'];
                $persist = $job['persist'];
                if ($current == $time) {
                    call_user_fun_array($func, $argv);
                    unset(self::$task[$time][$k]);
                }
                if ($persist) {
                    self::$task[$current+$interval][] = $job;
                }
            }
            if (empty(self::$task[$time])) {
                unset(self::$task[$time]);
            }
        }
    }

    /** Add a task */
    public static function add($interval, $func, $argv = array(), $persist = false) {
        if (is_null($interval)) { return; }
        $time = time() + $interval;
        self::$task[$time][] = array('func'=>$func, 'argv'=>$argv, 'interval'=>$interval, 'persist'=>$persist);
    }

    /** Delete all tasks */
    public function dellAll() {
        self::$task = array();
    }
}
?>

Callback Example

<?php
class DoJob {
    public function job($param = array()) {
        $time = time();
        echo "Time: {$time}, Func: ".get_class()."::".__FUNCTION__ ."(".json_encode($param).")
";
    }
}
?>

Usage Scenario

<?php
require_once(__DIR__.'/Timer.php');
require_once(__DIR__.'/DoJob.php');
Timer::dellAll();
Timer::add(1, array('DoJob','job'), array(), true);   // persistent job
Timer::add(3, array('DoJob','job'), array('a'=>1), false); // one‑time job
echo "Time start: ".time()."
";
Timer::run();
while (1) {
    sleep(1);
    pcntl_signal_dispatch();
}
?>

The script registers two jobs, starts the timer, and enters an infinite loop that dispatches signals. The persistent job runs repeatedly, while the non‑persistent job runs only once.

Conclusion

Before receiving a signal, the process must stay alive; a continuously true loop is used here. In production, such a loop would be part of a long‑running service (e.g., handling I/O or socket connections). PHP timers operate with a granularity of one second, which is sufficient for most scheduling needs.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CLISchedulersignaltimerpcntl
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.