Любите загадки? Событие еще доступно на сайте.

Hesko

22 677 Монеток

Сигналы в Artisan командах

Утро понедельника, вы приходите в офис, наливаете себе первую чашку кофе и готовитесь к очередному погружению в код. У вас стоит задача: отправка запроса на сервер и сохранение результата в базу данных. Всё идёт по плану, и вы начинаете с простого кода:

foreach ($documents as $document) {
    $response = Http::post('https://api.example.com/print/', [
        'document' => $document->content,
    ]);

    $document->update($response->json());
}

Что произойдет, если нам нужно остановить выполнение этого сценария, например, для обновления кода на сервере? Ведь он может остановиться когда запрос будет отправлен, но еще не был сохранен в базе данных. Как безопасно прервать выполнение, чтобы избежать дублирования данных при повторном запуске сценария?

В таких случаях на помощь приходят сигналы.

Что такое сигналы?

Сигналы — это события, которые процесс может прослушивать и на которые он может реагировать. В Laravel, когда вы нажимаете Ctrl+C в терминале для завершения команды artisan serve, вы отправляете сигнал SIGINT текущему процессу, что приводит к его остановке.

В PHP сигналы обрабатываются с помощью функции pcntl_signal, куда передаются номер сигнала и функция-обработчик. При получении сигнала будет вызван обработчик:

$signalHandler = function () {
    // Действия при получении сигнала
};

pcntl_signal(SIGINT, $signalHandler);

Все константы сигналов можно найти на официальном сайте PHP.

Пример использования сигналов в команде Laravel

Допустим, у нас есть команда, выполняющая длительную операцию:

class ProcessDocumentsCommand extends Command
{
    public function handle()
    {
        $this->info(now()->format('H:i:s') . ' - Начало обработки...');

        // Длительная операция
        $end = now()->addSeconds(10)->format('s');
        while (now()->format('s') !== $end) {
            // Эмуляция длительного процесса
        }

        $this->info(now()->format('H:i:s') . ' - Обработка завершена.');
    }
}

Обратите внимание в примере не используется функция sleep(10), так использование сигналов на самом деле сокращает его, так что если ваш код полагается на это, это может быть проблемой!

Если мы запустим эту команду и попробуем прервать её выполнение, нажав Ctrl+C, то процесс будет остановлен немедленно, и, если в этот момент шёл запрос к серверу, его результаты могут не сохраниться.

Добавим обработку сигналов, используя интерфейс SignalableCommandInterface из пакета symfony/console, который доступен в Laravel:

class ProcessDocumentsCommand extends Command implements SignalableCommandInterface
{
    /**
     * Execute the console command.
     */
    public function handle()
    {
        $this->info(now()->format('H:i:s') . ' - Начало обработки...');

        // Длительная операция
        $end = now()->addSeconds(10)->format('s');
        while (now()->format('s') !== $end) {
            // Эмуляция длительного процесса
        }

        $this->info(now()->format('H:i:s') . ' - Обработка завершена.');
    }

    /**
     * @return array
     */
    public function getSubscribedSignals(): array
    {
        return [SIGINT, SIGTERM];
    }

    /**
     * @param int       $signal
     * @param false|int $previousExitCode
     *
     * @return int|false
     */
    public function handleSignal(int $signal, false|int $previousExitCode = 0): int|false
    {
        $this->info(now()->format('H:i:s') . ' - Завершение работы...');

        return false;
    }
}

Этот интерфейс добавляет два метода: getSubscribedSignals и handleSignal. В первом методе возвращаем массив сигналов, на которые хотим реагировать, а во втором методе обрабатываем сигналы. Возвращая false в методе handleSignal, мы позволяем команде завершиться самостоятельно без принудительного прерывания через exit($exitCode).

Теперь, запустив команду и нажав Ctrl+C, вы увидите следующее:

00:07:02 - Начало обработки...
00:07:03 - Завершение работы...
00:07:10 - Обработка завершена.

Применение сигналов для решения основной задачи

Добавим флаг, указывающий на необходимость завершения процесса:

class ProcessDocumentsCommand extends Command implements SignalableCommandInterface
{
    /**
    * @var bool Флаг завершения работы
    */
    private bool $shouldExit = false;

    public function handle()
    {
        $documents = Document::limit(10)->get();

        foreach ($documents as $document) {
            // Если был сигнал завершить работу, то выходим
            if ($this->shouldExit) {
                exit();
            }

            $response = Http::post('https://api.example.com/print/', [
                'document' => $document->content,
            ]);

            $document->update($response->json());
        }
    }

    public function getSubscribedSignals(): array
    {
        return [SIGINT, SIGTERM];
    }

    public function handleSignal(int $signal, false|int $previousExitCode = 0): int|false
    {
        $this->shouldExit = true;

        return false;
    }
}

Laravel предоставляет еще более удобный способ обработки сигналов. Вместо добавления интерфейса SignalableCommandInterface и реализации его методов, можно воспользоваться методом trap:

class ProcessDocumentsCommand extends Command
{
    /**
    * @var bool Флаг завершения работы
    */
    private bool $shouldExit = false;

    public function handle()
    {
        $this->trap([SIGINT, SIGTERM], function (int $signal) {
            $this->shouldExit = true;
        });

        $documents = Document::limit(10)->get();

        foreach ($documents as $document) {
            // Если был сигнал завершить работу, то выходим
            if ($this->shouldExit) {
                exit();
            }

            $response = Http::post('https://api.example.com/print/', [
                'document' => $document->content,
            ]);

            $document->update($response->json());
        }
    }
}

Выбор способа обработки сигналов зависит от ваших предпочтений и конкретных требований задачи. Главное, что сигналы позволяют контролировать выполнение процессов, что может быть полезно для завершения действий, удаления временных файлов, закрытия соединений и многого другого.

3

Создание собственных функций помощников

Laravel предоставляет множество удобных вспомогательных глобальных функций, которые облегчают работу с массивами, путями к файлам, строками и маршрутами, а также любимая всеми функция dd().

Вы также можете создавать свой собственный набор вспомогательных функций для ваших приложений Laravel и PHP-пакетов, используя Composer для их автоматического импорта.

Давайте рассмотрим процесс создания собственных глобальных функций, которые будут автоматически загружаться Laravel.

Создание файла

Первое, где вы могли бы захотеть использовать ваши вспомогательные функции, это в контексте приложения Laravel. В зависимости от ваших предпочтений, вы можете организовать расположение ваших файлов с вспомогательными функциями так, как вам удобно. Например:

  • app/helpers.php
  • app/Http/helpers.php

Я предпочитаю хранить мои в app/helpers.php в корне пространства имен приложения.

Автоматическая загрузка

Чтобы использовать ваши вспомогательные функции PHP, вам нужно загрузить их в во время выполнения. В начале моей карьеры было распространено использование такого кода в начале файла:

require_once ROOT . '/helpers.php';

Сейчас у нас есть гораздо лучшее решение – Composer. Если вы создадите новый проект Laravel, вы увидите ключи autoload и autoload-dev в файле composer.json:

"autoload": {
    "classmap": [
        "database/seeds",
        "database/factories"
    ],
    "psr-4": {
        "App\\": "app/"
    }
},
"autoload-dev": {
    "psr-4": {
        "Tests\\": "tests/"
    }
},

Если вы хотите добавить файл с вспомогательными функциями, Composer предоставляет ключ files (который является массивом путей к файлам), который вы можете определить внутри autoload:

"autoload": {
    "files": [
        "app/helpers.php"
    ],
    "classmap": [
        "database/seeds",
        "database/factories"
    ],
    "psr-4": {
        "App\\": "app/"
    }
},

После добавления нового пути в массив files, вам нужно обновить автозагрузчик:

composer dump-autoload

Теперь файл helpers.php будет загружаться автоматически при каждом запросе, потому что Laravel использует автозагрузчик Composer в public/index.php:

require __DIR__.'/../vendor/autoload.php';

Определение функций

Определение функций в вашем файле – это простая часть, хотя есть несколько моментов, на которые стоит обратить внимание. Все файлы с вспомогательными функциями Laravel обернуты в условие, чтобы избежать коллизий при определении функций:

if (! function_exists('env')) {
    function env($key, $default = null) {
        // ...
    }
}

Я предпочитаю использовать проверки function_exists в моих вспомогательных приложениях, но если вы определяете вспомогательные функции в контексте вашего приложения, вы можете пропустить проверку function_exists.

Пример вспомогательной функции

Мне нравятся функции путей и URL из Rails, которые вы получаете при определении ресурсного маршрута. Например, маршрут photos предоставит функции new_photo_path, edit_photo_path и т. д.

Когда я использую ресурсный маршрутизатор в Laravel, мне нравится добавить несколько вспомогательных функций, которые облегчают определение маршрутов в моих шаблонах. В моей реализации мне нравится иметь функцию которой могу передать модель модель Eloquent и получить обратно URL, например:

create_route($model);
edit_route($model);
show_route($model);
destroy_route($model);

Вот как вы могли бы определить show_route в файле app/helpers.php (остальные будут выглядеть похожим образом):

if (! function_exists('show_route')) {
    function show_route($model, $resource = null)
    {
        $resource = $resource ?? plural_from_model($model);
 
        return route("{$resource}.show", $model);
    }
}
 
if (! function_exists('plural_from_model')) {
    function plural_from_model($model)
    {
        $plural = Str::plural(class_basename($model));
 
        return Str::kebab($plural);
    }
}

Функция plural_from_model() – это просто повторяющия функция которую используют другие функции маршрутов для предсказания имени маршрута на основе kebab-case от множественного числа имени модели.

Например, вот пример имени ресурса, полученный из модели:

$model = new App\LineItem;
plural_from_model($model);
// => line-items
 
plural_from_model(new App\User);
// => users

Используя это соглашение, вы можете определить ресурсный маршрут следующим образом в routes/web.php:

Route::resource('line-items', 'LineItemsController');
Route::resource('users', 'UsersController');

Затем в шаблонах Blade вы могли бы сделать следующее:

<a href="{{ show_route($lineItem) }}">
    {{ $lineItem->name }}
</a>

Что приведет к созданию примерно такого HTML:

<a href="http://localhost/line-items/1">
    Line Item #1
</a>

Пакеты

Ваши пакеты также могут использовать файл с вспомогательными функциями, которые вы хотите сделать доступными для проектов, использующих ваш пакет.

Вы будете использовать тот же подход в файле composer.json пакета, определяя ключ files с массивом ваших файлов с вспомогательными функциями.

Очень важно добавить проверки function_exists() вокруг ваших вспомогательных функций, чтобы проекты, использующие ваш код, не ломались из-за коллизий имен.

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

Узнать больше

Ознакомьтесь с документацией по автозагрузке Composer, чтобы узнать больше о включении файлов и общей информации об автозагрузке классов.

0