PHP: Планировщик задач (Cron)

PHP
Реализовать планировщик задач на PHP можно через интеграцию с системным cron — это надёжный и эффективный подход. В этом случае системный планировщик запускает PHP‑скрипт в заданное время, а скрипт выполняет нужные операции и завершает работу. Разберём процесс пошагово.

Принцип работы


Системный cron запускает PHP‑скрипт по расписанию (например, раз в минуту или в конкретное время суток). Скрипт:
  1. Проверяет, нужно ли выполнить какую‑либо задачу в текущий момент.
  2. Выполняет задачи, соответствующие текущему времени.
  3. Логирует действия.
  4. Завершает работу.

Такой подход исключает необходимость бесконечных циклов и пауз в PHP‑коде.

Шаг 1. Подготовка окружения


Убедитесь, что:
  • на сервере установлен PHP и доступен из командной строки (php -v);
  • есть доступ к редактированию cron‑задач (crontab -e);
  • у скрипта есть права на запись в директорию для логов.

Шаг 2. Создание PHP‑скрипта планировщика


Создайте файл "task_scheduler.php" со следующим содержимым:
<?php
// Файл task_scheduler.php

// Конфигурация
$logFile = __DIR__ . '/scheduler.log';
$lockFile = __DIR__ . '/scheduler.lock';

/**
 * Записывает сообщение в лог с временной меткой
 */
function logMessage($message) {
    global $logFile;
    $timestamp = date('Y-m-d H:i:s');
    file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
}

/**
 * Проверяет, не запущен ли уже экземпляр скрипта
 */
function isAlreadyRunning() {
    global $lockFile;
    if (file_exists($lockFile)) {
        $pid = (int)file_get_contents($lockFile);
        // Проверяем, жив ли процесс с этим PID
        if (posix_kill($pid, 0)) {
            return true;
        }
        // Если процесс мёртв, удаляем старый lock‑файл
        unlink($lockFile);
    }
    return false;
}

/**
 * Создаёт lock‑файл для защиты от параллельного запуска
 */
function createLock() {
    global $lockFile;
    file_put_contents($lockFile, getmypid());
}

/**
 * Удаляет lock‑файл после завершения работы
 */
function removeLock() {
    global $lockFile;
    if (file_exists($lockFile)) {
        unlink($lockFile);
    }
}

// Защита от параллельного запуска
if (isAlreadyRunning()) {
    logMessage("Планировщик уже запущен. Выход.");
    exit(1);
}
createLock();

// Список задач с cron‑расписанием
$tasks = [
    [
        'task' => function() {
            logMessage("Выполняется очистка кэша...");
            // Логика очистки кэша
            // Например: удаление старых файлов
            logMessage("Очистка кэша завершена.");
        },
        'schedule' => '0 0 * * *',    // ежедневно в 00:00
        'description' => 'Очистка кэша'
    ],
    [
        'task' => function() {
            logMessage("Выполняется рассылка уведомлений...");
            // Логика рассылки уведомлений
            logMessage("Рассылка уведомлений завершена.");
        },
        'schedule' => '0 9 * * 0',  // каждое воскресенье в 09:00
        'description' => 'Рассылка уведомлений'
    ]
];

/**
 * Проверяет, нужно ли выполнить задачу по cron‑расписанию
 */
function shouldRun($cronExpression) {
    $parts = explode(' ', $cronExpression);
    if (count($parts) !== 5) {
        return false;
    }

    $minute = (int)date('i');
    $hour = (int)date('H');
    $dayOfMonth = (int)date('j');
    $month = (int)date('n');
    $dayOfWeek = (int)date('w'); // 0 — воскресенье, 6 — суббота

    return matchCronPart($parts[0], $minute) &&
           matchCronPart($parts[1], $hour) &&
           matchCronPart($parts[2], $dayOfMonth) &&
           matchCronPart($parts[3], $month) &&
           matchCronPart($parts[4], $dayOfWeek);
}

/**
 * Сравнивает часть cron‑выражения с текущим значением
 */
function matchCronPart($cronPart, $currentValue) {
    if ($cronPart === '*') {
        return true;
    }
    if (str_contains($cronPart, ',')) {
        $values = array_map('intval', explode(',', $cronPart));
        return in_array($currentValue, $values);
    }
    return (int)$cronPart === $currentValue;
}

// Основной блок выполнения
try {
    logMessage("Запуск планировщика задач.");

    foreach ($tasks as $task) {
        if (shouldRun($task['schedule'])) {
            logMessage("Запуск задачи: {$task['description']}");
            $task['task']();
        }
    }

    logMessage("Завершение работы планировщика.");
} catch (Exception $e) {
    logMessage("Ошибка: " . $e->getMessage());
} finally {
    removeLock();
}

Шаг 3. Настройка системного cron


Настройте системный планировщик для запуска скрипта. Откройте редактор cron:
crontab -e

Добавьте строку для запуска скрипта каждую минуту:
* * * * * /usr/bin/php /path/to/task_scheduler.php

Где:
  • /usr/bin/php — путь к интерпретатору PHP (проверьте через which php);
  • /path/to/task_scheduler.php — полный путь к вашему скрипту.

Шаг 4. Проверка работы


Запустите скрипт вручную для проверки:
php /path/to/task_scheduler.php

Проверьте лог на наличие записей:
tail -f scheduler.log

Убедитесь, что lock‑файл создаётся и удаляется корректно.

Дополнительные рекомендации


  • Ротация логов. Настройте logrotate для автоматического архивирования и очистки логов.
  • Обработка ошибок. Добавьте try‑catch для каждой задачи отдельно, чтобы ошибка в одной не останавливала остальные.
  • Таймауты. Установите лимит времени выполнения скрипта (set_time_limit(30)), чтобы избежать зависаний.
  • Уведомления о сбоях. Настройте отправку email или сообщений в мессенджер при критических ошибках.
  • Тестирование. Протестируйте расписание на тестовых задачах перед внедрением в продакшен.
  • Масштабирование. Для большого числа задач рассмотрите использование очередей (Redis, RabbitMQ) и фоновых воркеры.

Преимущества подхода
  • Эффективность. Скрипт запускается только когда нужно, не потребляя ресурсы в простое.
  • Точность. Задачи выполняются строго по расписанию системного cron.
  • Надёжность. Защита от параллельных запусков предотвращает конфликты.
  • Прозрачность. Логирование фиксирует все действия и ошибки.
  • Масштабируемость. Легко добавлять новые задачи и менять расписание.


Автор:  03.12.2025 08:56:22 pm