Laravel: Погружение в Уведомления / Notifications

Источник: «Diving into Notifications»
В большинстве Laravel приложений необходимо отправлять уведомления, будь то внутри приложения, по электронной почте или в slack — обычно это уведомления о транзакциях, чтобы предупредить пользователя о каком-либо действии или событии в вашем приложении. Давайте разберёмся, что это такое и как действует.

Первое о чём вам нужно подумать, перед отправкой уведомления пользователю, — это механизм доставки. Мы хотим отправить быстрое электронное письмо? Или это уведомление в системном стиле, когда мы хотим отправить данные в канал slack? Или, может быть, это уведомление в приложении, что кому-то понравилась фотография вашего кота?

В любом случае доставка лежит на основе любого уведомления, и, конечно же, с Laravel вы можете доставлять по нескольким каналам одно уведомление. Мы делаем это, используя метод via в нашем классе Notification, указывая массив каналов, по которым мы хотим отправить уведомление.

Из коробки вы можете отправлять slack, email и in-app уведомления без каких либо дополнительных пакетов. Однако в наши дни существует множество вариантов каналов уведомлений. К счастью, есть поддерживаемый сообществом веб-сайт Laravel Notification Channels, показывает варианты, инструкции по установке и способы отправки уведомлений по множеству различных каналов.

Давайте рассмотрим базовый пример отправки уведомления. У нас есть система, в которой люди могут назначать встречи, например как в Calendy или SavvyCal. Будь то API, веб-взаимодействие или даже CLI, нам всегда нужен один и то же результат. Если кто-то назначает встречу, мы должны уведомить этого человека, сообщив ему, что встреча забронирована, с прикреплённым приглашением из календаря. Мы также хотим сообщить человеку с которым назначена встреча, чтобы он мог быть в курсе. Мы также можем обновить что-то в сторонней системе для управления доступностью.

Для начала, давайте сгенерируем уведомление с помощью консоли artisan:

php artisan make:notification UserMeetingBookedNotification --test

Давайте разберём команду. Мы хотим создать уведомление, эта часть понятна и подходит для большинства команд artisan при генерации кода. Затем мы указываем имя для самого уведомления. Вы могли бы использовать пространства имён для дальнейшей группировки уведомлений, используя либо \, либо / для разделения пространства имён. Мы также указываем опцию --test, чтобы Laravel сгенерировал тест для этого конкретного уведомления; это будет функциональный тест. Я использую pestPHP для своей среды тестирования, поэтому обычно публикую заглушки, для настройки сгенерированного вывода. Про тестирование вы можете прочитать в статье Laravel: Как начать тестировать приложение

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

Приступим к созданию нашего уведомления:

declare(strict_types=1);

namespace App\Notifications;

use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

final class UserMeetingBookedNotification extends Notification implements ShouldQueue
{
use Queueable;

public function __construct(
public readonly string $name,
public readonly string $email,
public readonly CarbonInterface $datetime,
) {}
}

Теперь мы можем создать уведомление для доставки. Но нам нужно определиться с каналом. В этом руководстве я сосредоточусь на параметрах доступных по умолчанию, но Laravel Notification Channels содержит много информации, которую вы можете использовать для альтернативных каналов.

Мы будем использовать это уведомление для отправки пользователя электронного письма, чтобы сообщить, что у него запланирована встреча. Для этого нам нужно добавить метод via и указать, что мы хотим использовать канал mail.

declare(strict_types=1);

namespace App\Notifications;

use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

final class UserMeetingBookedNotification extends Notification implements ShouldQueue
{
use Queueable;

public function __construct(
public readonly string $name,
public readonly string $email,
public readonly CarbonInterface $datetime,
) {}

public function via(mixed $notifiable): array
{
return ['mail'];
}
}

После того как мы добавили метод via нужно описать отправляемое Почтовое Сообщение. Мы делаем это в методе toMail. Общее правило заключается в том, что для каждого канала требуется метод to, где мы добавляем префикс to к каналу с заглавной буквы.

declare(strict_types=1);

namespace App\Notifications;

use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

final class UserMeetingBookedNotification extends Notification implements ShouldQueue
{
use Queueable;

public function __construct(
public readonly string $name,
public readonly string $email,
public readonly CarbonInterface $datetime,
) {}

public function via(mixed $notifiable): array
{
return ['mail'];
}

public function toMail(mixed $notifiable): MailMessage
{
return (new MailMessage)
->subject('New Booking Received.')
->greeting('Booking received')
->line("{$this->name} has booked a meeting with you.")
->line("This has been booked for {$this->datetime->format('l jS \\of F Y h:i:s A')}")
->line("You can email them on {$this->email} if you need to organise anything.");
}
}

Вот и всё; это отправит пользователю простое электронное письмо с предупреждением о том, как он может связаться, и, что он получил бронирование.

Предположим, что теперь нам нужно добавить это в канал 'slack', возможно, с целью отслеживания. Но, мы всё ещё хотим отправить уведомление по электронной почте.

Для начала мы должны протестировать то, что уже создали. Как и со всеми аспектами ваших проектов, вы не можете гарантировать, что всё будет работать, если вы не протестируете их. Давайте сначала напишем тесть для отправки уведомления по электронной почте. Затем мы можем внести свои изменения.

it('can send an email notification when a booking is made', function () {
Notification::fake();

$user = User::factory()->create();

expect(
Bookings::query()->count(),
)->toEqual(0);

$action = app()->make(
abstract: CreateNewBookingAction::class,
);

$action->handle(
user: $user->id,
name: 'Test User',
email: 'test@email.com',
time: now()->addHours(12),
);

expect(
Bookings::query()->count(),
)->toEqual(1);

Notification::assertSentTo(
[$user],
UserMeetingBookedNotification::class,
);
});

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

Мы можем добавить ещё одну проверку в тест убедиться, что тест отправляет уведомление на правильный канал:

it('sends the notification to the correct channels', function () {
Notification::fake();

$user = User::factory()->create();

$action = app()->make(
abstract: CreateNewBookingAction::class,
);

$action->handle(
user: $user->id,
name: 'Test User',
email: 'test@email.com',
time: now()->addHours(12),
);

Notification::assertSentTo(
[$user],
UserMeetingBookedNotification::class,
function (mixed $notification, array $channels): bool {
return in_array('mail', $channels);
},
);
});

Тест аналогичен первоначальному, но на этот раз добавляем третий аргумент к утверждению — анонимную функцию, в которой хотим убедиться, что отправляем уведомление в канал mail.

Теперь мы довольны нашим тестовым покрытием. Мы знаем, что когда action будет обработан, будет отправлено уведомление. И что когда оно будет отправлено, каналы будут включать mail, что означает, что наше электронное письмо будет доставлено.

Вернёмся к добавлению следующего канала slack. Я скрыл метод toMail из приведённого выше примера кода, чтобы было легче увидеть, что делается:

declare(strict_types=1);

namespace App\Notifications;

use Carbon\CarbonInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;

final class UserMeetingBookedNotification extends Notification implements ShouldQueue
{
use Queueable;

public function __construct(
public readonly string $name,
public readonly string $email,
public readonly CarbonInterface $datetime,
) {}

public function via(mixed $notifiable): array
{
return ['mail', 'slack'];
}

public function toSlack(mixed $notifiable): SlackMessage
{
return (new SlackMessage)
->success()
->content("{$this->name} just booked a meeting for {$this->datetime->format('l jS \\of F Y h:i:s A')}.");
}
}

Однако чтобы это работало правильно, нужно убедиться, что мы установили пакет уведомлений от команды laravel:

composer require laravel/slack-notification-channel

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

it('sends the notification to the correct channels', function () {
Notification::fake();

$user = User::factory()->create();

$action = app()->make(
abstract: CreateNewBookingAction::class,
);

$action->handle(
user: $user->id,
name: 'Test User',
email: 'test@email.com',
time: now()->addHours(12),
);

Notification::assertSentTo(
[$user],
UserMeetingBookedNotification::class,
function (mixed $notification, array $channels): bool {
return in_array('mail', $channels)
& in_array('slack', $channels);
},
);
});

Есть и другие тесты, которые мы можем провести с нашими уведомлениями, но это отличное место для старта без чрезмерного усложнения. Мы знаем, что отправляем корректное Уведомление в нужное время нужному пользователю по нужным каналам.

Следует помнить, что Уведомления не нужно ставить в очередь. Они также хорошо могут быть отправлены синхронно.

Итак, теперь мы знаем, как создать и протестировать уведомление; давайте посмотрим на их отправку.

По умолчанию, в приложении Laravel ваша модель User реализует трейт Notifiable позволяющий вам делать что-то вроде следующего:

auth()->user()->notify(new UserMeetingBookedNotification(
name: $request->get('name'),
email: $request->('email'),
));

Это здорово, но трейт Notifiable зависит от модели, имеющей доступное свойство email. Что мы делаем, когда наш вариант использования не совсем соответствует этому подходу? В этом случае трейт Notifiable нам не подходит. Именно по этому я считаю фасад Notification вашим лучшим другом. Вы можете использовать фасад Notification для ручной маршрутизации уведомлений, вместо того, чтобы полагаться на трейт. Давайте посмотрим пример:

Notification::send(['email address one', 'email address two'], new EmailNotification($arguments));

Приведённый пример позволяет легко и быстро отправить уведомления на массив адресов электронной почты. Немного более чистый способ сделать это — использовать уведомления по запросу. Допустим, мы хотим иметь возможность программно отправлять уведомления как часть команды artisan. Вы можете предоставить аргумент для канала и выбрать способ отправки уведомления.

Он начинается с фасада Notification. Затем вы сообщаете ему route уведомления с каналом и аргументом.

Notification::route('slack', 'slack-webhook-url')
->notify(new SlackNotification($argument));

Фасад Notification очень мощный и гибкий, позволяет отправлять уведомления по запросу и быстро их тестировать.

А как вы используете Уведомления в Laravel? Какой у вас любимый канал?

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

Предыдущая Статья

Laravel: CI с GitHub Actions

Следующая Статья

SOP: Что такое Same-origin policy