Использование ReactPHP для запуска задач Drupal
ReactPHP — это неблокирующая PHP-инфраструктура, управляемая событиями, которая позволяет вам работать в долго выполняющемся скрипте через цикл событий. По своей сути ReactPHP предоставляет цикл событий и утилиты для запуска событий через определенные промежутки времени и запуска вашего кода. Это отличается от обычного выполнения PHP-скрипта с коротким жизненным циклом и отдельными запросами.
ReactPHP использовался для создания приложений веб-сервера, серверов веб-сокетов и многого другого. Но что, если бы мы использовали ReactPHP для выполнения операций и задач в приложении Drupal?
Технически это может быть осуществимо с набором заданий cron, запланированных через определенные промежутки времени, которые вызывают команды Drush или консоли Drupal. Но здесь есть ограничение: возможность манипулировать записями заданий cron для пользователя всякий раз, когда происходит развертывание. Хостинг-провайдеры, такие как Platform.sh, поддерживают это, но только в основном контейнере приложения. Рабочие контейнеры не поддерживают определения заданий cron. Кроме того, как насчет обработки ошибок?
Мы можем объединить цикл событий ReactPHP с библиотекой ReactPHP ChildProcess для запуска наших инструментов командной строки. Дочерний процесс присоединяется к циклу событий и позволяет нам передавать выходные данные из STDOUT и STDERR. Это позволяет нам регистрировать выходные данные команды или обрабатывать ошибки, возникающие во время этих фоновых процессов.
Проще всего создать функцию, которая выполняет новый дочерний процесс, обеспечивает для него цикл обработки событий и привязывает события к выходным потокам.
function run_command(string $command): void {
$loop = React\EventLoop\Factory::create();
$process = new React\ChildProcess\Process($command);
$process->start($loop);
$process->on('exit', function ($exitCode) use ($command) {
});
$process->stdout->on('data', function ($chunk) {
});
$process->stdout->on('error', function (Exception $e) use ($command) {
});
$process->stderr->on('data', function ($chunk) use ($command) {
if (!empty(trim($chunk))) {
}
});
$process->stderr->on('error', function (Exception $e) use ($command) {
});
$loop->run();
}
Я большой поклонник Rollbar и отправлял туда логи.
Теперь давайте создадим наш основной цикл событий, который будет запускать наши команды с разными интервалами. Мы можем запускать cron каждые двадцать минут
$loop = React\EventLoop\Factory::create();
$loop->addPeriodicTimer(1200, function () {
run_command('drush cron');
});
$loop->run();
Если вы используете систему очередей для асинхронной обработки заданий с помощью расширенного модуля очереди, вам может понадобиться более непрерывная обработка, действующая как демон.
$loop = React\EventLoop\Factory::create();
$loop->addPeriodicTimer(30, function () {
run_command(sprintf('drush advancedqueue:queue:process queue1'));
});
$loop->addPeriodicTimer(120, function () {
run_command(sprintf('drush advancedqueue:queue:process queue2'));
});
$loop->run();
Это позволило нам запускать Drush как дочерний процесс в цикле событий ReactPHP для запуска задач. Что, если мы на самом деле запустим Drupal и вызовем наш код напрямую, а не через дочерние процессы? Мы можем!
Для начала нам нужно запустить Drupal. Это требует создания объекта запроса и фиктивного маршрута. DrupalKernel
и другие компоненты связаны с запросом, содержащим некоторую метаинформацию о маршруте. К счастью, Drupal поддерживает <none>
маршрут.
Нам требуется автозагрузчик и создаем наш объект запроса. Обычно мои PHP-скрипты находятся в каталоге скриптов в корне моего проекта, поэтому мой автозагрузчик находится в ../vendor/autoload.php
.
$autoloader = require __DIR__ . '/../vendor/autoload.php';
$request = Symfony\Component\HttpFoundation\Request::createFromGlobals();
$request->attributes->set(
Symfony\Cmf\Component\Routing\RouteObjectInterface::ROUTE_OBJECT,
new Symfony\Component\Routing\Route('<none>')
);
$request->attributes->set(
Symfony\Cmf\Component\Routing\RouteObjectInterface::ROUTE_NAME,
'<none>'
);
Далее мы загружаем DrupalKernel
. Нам нужно запустить bootEnvironment
метод, это устанавливает некоторую необходимую информацию для Drupal. Далее нам нужно указать путь к сайту, который содержит файл settings.php, который мы хотим использовать; как правило, это sites/default
. Затем мы просто загружаем ядро и запускаем предварительный дескриптор запроса, чтобы все заработало.
$kernel = new Drupal\Core\DrupalKernel('prod', $autoloader);
$kernel::bootEnvironment();
$kernel->setSitePath('sites/default');
Drupal\Core\Site\Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $autoloader);
$kernel->boot();
$kernel->preHandle($request);
Теперь мы можем отключить наш цикл событий и выполнить наш код по мере необходимости.
$loop = React\EventLoop\Factory::create();
$loop->addPeriodicTimer(10, function () {
$cron = \Drupal::service('cron');
$cron->run();
});
$loop->run();
Вот ссылка на полный список файлов, приведенных здесь: