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

Hesko

22 677 Монеток

Обзор атрибутов Laravel: что и как использовать

Атрибуты в PHP — это отличный способ добавлять метаданные к вашим классам, методам и свойствам. Laravel предоставляет множество готовых атрибутов, которые можно использовать для улучшения структуры и читаемости вашего кода.

Наблюдатель (ObservedBy)

Атрибут ObservedBy позволяет указать наблюдателя для модели. Это помогает держать код модели чистым, перенося логику наблюдателя в отдельный класс.

use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
    // ...
}

Глобальная область (ScopedBy)

Атрибут ScopedBy позволяет назначить глобальные условия для модели. Это полезно, когда нужно применять общие ограничения к запросам для конкретной модели.

namespace App\Models;

use App\Models\Scopes\ActiveScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([ActiveScope::class])]
class User extends Model
{
    // ...
}

Контекстные атрибуты

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

Пример:

namespace App\Http\Controllers;

use Illuminate\Container\Attributes\Auth;
use Illuminate\Container\Attributes\Cache;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Log;
use Illuminate\Container\Attributes\Tag;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Database\Connection;
use Psr\Log\LoggerInterface;

class PhotoController extends Controller
{
    public function __construct(
        #[Auth('web')] protected Guard $auth,
        #[Cache('redis')] protected Repository $cache,
        #[Config('app.timezone')] protected string $timezone,
        #[DB('mysql')] protected Connection $connection,
        #[Log('daily')] protected LoggerInterface $log,
        #[Tag('reports')] protected iterable $reports,
    )
    {
        // ...
    }
}

Laravel также поддерживает автоматическое внедрение текущего пользователя в маршруты:

use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;

Route::get('/user', function (#[CurrentUser] User $user) {
    return $user;
})->middleware('auth');

Удаление при отсутствии модели (DeleteWhenMissingModels)

При использовании моделей в заданиях вы можете добавить атрибут DeleteWhenMissingModels, чтобы задание автоматически удалялось, если указанная модель отсутствует в базе данных.

namespace Acme;

use Illuminate\Queue\Attributes\DeleteWhenMissingModels;

#[DeleteWhenMissingModels]
class ProcessPodcastJob
{
    public function __construct(
        public Podcast $podcast,
    ) {}
}

Без загрузки связей (WithoutRelations)

Атрибут WithoutRelations позволяет загрузить модель без её связей. Это удобно, если вам не нужны связанные данные в фоновых задачах.

class ProcessPodcastJob
{
    public function __construct(
        #[WithoutRelations]
        public Podcast $podcast,
    ) {}
}

Новые атрибуты постепенно появляются, что бы упростить работу с кодом.

А какие атрибуты хотели бы видеть вы? 💡

0

Сигналы в 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