22 677 Монеток
Утро понедельника, вы приходите в офис, наливаете себе первую чашку кофе и готовитесь к очередному погружению в код. У вас стоит задача: отправка запроса на сервер и сохранение результата в базу данных. Всё идёт по плану, и вы начинаете с простого кода:
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.
Допустим, у нас есть команда, выполняющая длительную операцию:
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());
}
}
}
Выбор способа обработки сигналов зависит от ваших предпочтений и конкретных требований задачи. Главное, что сигналы позволяют контролировать выполнение процессов, что может быть полезно для завершения действий, удаления временных файлов, закрытия соединений и многого другого.
22 677 Монеток
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, чтобы узнать больше о включении файлов и общей информации об автозагрузке классов.
{message}