Руководство по событиям модели Laravel

Источник: «A guide to Laravel's model events»
Рассмотрим, что такое события модели и как их использовать в приложении Laravel. Также рассмотрим, как тестировать события модели и проблемы, на которые следует обратить внимание при их использовании.

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

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

Что такое события и слушатели

Возможно, вы уже слышали о «событиях» и «слушателях». Но если нет, то вот краткая информация о них:

События

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

Как правило, в Laravel события — это PHP-классы. Кроме событий, предоставляемых фреймворком или сторонними пакетами, они обычно хранятся в директории app/Events.

Вот пример простого класса событий, отправляющего событие каждый раз, когда пользователь регистрируется на сайте:

declare(strict_types=1);

namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class UserRegistered
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;

public function __construct(public User $user)
{
//
}
}

В базовом примере выше у есть класс событий App\Events\UserRegistered, принимающий в своём конструкторе экземпляр модели User. Этот класс событий представляет собой простой контейнер данных, хранящий экземпляр пользователя, который был зарегистрирован.

Когда событие будет отправлено, оно вызовет всех слушателей, которые его ожидают.

Вот простой пример того, как можно отправить событие, когда пользователь регистрируется:

use App\Events\UserRegistered;
use App\Models\User;

$user = User::create([
'name' => 'Eric Barnes',
'email' => 'eric@example.com',
]);

UserRegistered::dispatch($user);

В примере выше создаётся новый пользователь, а затем отправляется событие App\Events\UserRegistered с экземпляром пользователя. При условии, что слушатели зарегистрированы правильно, это вызовет всех слушателей, слушающих событие App\Events\UserRegistered.

Слушатели

Слушатели — это блоки кода, которые должны запускаться при наступлении определённого события.

Например, продолжая пример с регистрацией пользователей, может потребоваться отправить приветственное письмо пользователю при его регистрации. Можно создать слушателя, слушающего событие App\Events\UserRegistered и отправляющего приветственное письмо.

В Laravel слушатели обычно (но не всегда — об этом мы поговорим позже) являются классами, находящимися в каталоге app/Listeners.

Пример слушателя, отправляющего приветственное письмо пользователю при его регистрации, может выглядеть следующим образом:

declare(strict_types=1);

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Mail;

final readonly class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
$event->user->notify(new WelcomeNotification());
}
}

Как видно из приведённого выше примера кода, класс слушателя App\Listeners\SendWelcomeEmail содержит метод handle, принимающий экземпляр события App\Events\UserRegistered. Этот метод отвечает за отправку приветственного письма пользователю.

Для более подробного изучения событий и слушателей можно обратиться к официальной документации: https://laravel.com/docs/11.x/events.

Что такое события модели

В приложениях Laravel обычно требуется вручную отправлять события при выполнении определённых действий. Как было показано в примере выше, для отправки события можно использовать следующий код:

UserRegistered::dispatch($user);

Однако при работе с моделями Eloquent в Laravel есть некоторые события, автоматически отправляемые для нас, поэтому их не нужно отправлять вручную. Нужно только создать для них слушателей, если необходимо выполнить какое-либо действие при их наступлении.

В приведённом ниже списке показаны события, автоматически отправляемые моделями Eloquent, а также их триггеры:

В приведённом выше списке можно заметить, что названия некоторых событий похожи: например, creating и created. События, заканчивающиеся на ing, выполняются до того, как происходит действие, и изменения сохраняются в базе данных. В то время как события, заканчивающиеся на ed, выполняются после выполнения действия, и изменения сохраняются в базе данных.

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

Прослушивание событий модели с помощью dispatchesEvents

Одним из способов прослушивания событий модели является определение свойства dispatchesEvents для модели.

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

Чтобы прояснить ситуацию, рассмотрим пример.

Представьте, что создаём приложение для ведения блога, содержащее две модели: App\Models\Post и App\Models\Author. Будем считать, что обе эти модели поддерживают soft delete. Когда сохраняется новый App\Models\Post, необходимо рассчитать время чтения публикации на основе длины контента. Когда происходит soft delete автора, необходимо выполнить soft delete всех его публикаций.

Настройка моделей

Модель App\Models\Author может выглядеть следующим образом:

declare(strict_types=1);

namespace App\Models;

use App\Events\AuthorDeleted;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;

final class Author extends Model
{
use HasFactory;
use SoftDeletes;

protected $dispatchesEvents = [
'deleted' => AuthorDeleted::class,
];

public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}

В приведённой выше модели:

Теперь создадим модель App\Models\Post:

declare(strict_types=1);

namespace App\Models;

use App\Events\PostSaving;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

final class Post extends Model
{
use HasFactory;
use SoftDeletes;

protected $dispatchesEvents = [
'saving' => PostSaving::class,
];

public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
}

В модели App\Models\Post, приведённой выше:

Модели подготовлены, приступим к созданию классов событий App\Events\AuthorDeleted и App\Events\PostSaving.

Создание классов событий

Создадим класс события App\Events\PostSaving, срабатывающий при сохранении новой публикации:

declare(strict_types=1);

namespace App\Events;

use App\Models\Post;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class PostSaving
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;

public function __construct(public Post $post)
{
//
}
}

В приведённом выше коде показан класс событий App\Events\PostSaving, принимающий в своём конструкторе экземпляр модели App\Models\Post. Этот класс событий представляет собой простой контейнер данных, в котором хранится сохраняемый экземпляр публикации.

Аналогично, можно создать класс события App\Events\AuthorDeleted, который будет срабатывать при удалении автора:

declare(strict_types=1);

namespace App\Events;

use App\Models\Author;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class AuthorDeleted
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;

public function __construct(public Author $author)
{
//
}
}

В классе App\Events\AuthorDeleted приведённом выше, видно, что конструктор принимает экземпляр модели App\Models\Author.

Теперь можно перейти к созданию слушателей.

Создание слушателей

Сначала создадим слушатель, используемый для расчёта предполагаемого времени прочтения публикации.

Создадим новый класс слушателя App\Listeners\CalculateReadTime:

declare(strict_types=1);

namespace App\Listeners;

use App\Events\PostSaving;
use Illuminate\Support\Str;

final readonly class CalculateReadTime
{
public function handle(PostSaving $event): void
{
$event->post->read_time_in_seconds = (int) ceil(
(Str::wordCount($event->post->content) / 265) * 60
);
}
}

Как видно из приведённого выше кода, в нём есть единственный метод handle. Это метод, автоматически вызываемый при срабатывании события App\Events\PostSaving. Он принимает экземпляр класса события App\Events\PostSaving, содержащий сохраняемую публикацию.

В методе handle используется наивная формула для расчёта времени чтения публикации. В данном случае предполагается, что средняя скорость чтения составляет 265 слов в минуту. Вычисляем время чтения в секундах, а затем устанавливаем атрибут read_time_in_seconds в модели поста.

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

Также можно создать слушатель, который будет мягко удалять все связанные публикации, когда автор будет мягко удалён.

Можно создать новый класс слушателя App\Listeners\SoftDeleteAuthorRelationships:

declare(strict_types=1);

namespace App\Listeners;

use App\Events\AuthorDeleted;

final readonly class SoftDeleteAuthorRelationships
{
public function handle(AuthorDeleted $event): void
{
$event->author->posts()->delete();

// Soft delete любых других отношений...
}
}

В приведённом выше слушателе метод handle принимает экземпляр класса события App\Events\AuthorDeleted. Этот класс событий содержит автора, который удаляется. Затем удаляем публикации автора с помощью метода delete в отношении posts.

В результате, когда модель App\Models\Author будет мягко удалена, все публикации автора также будут мягко удалены.

Стоит отметить, что для достижения этой цели скорее всего потребуется более надёжное решение для многократного использования. Но для целей этой статьи оставим всё как есть.

Прослушивание событий модели с помощью замыканий

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

Давайте рассмотрим предыдущий пример мягкого удаления публикаций при мягком удалении автора. Можно обновить модель App\Models\Author, включив в неё замыкание, слушающее событие модели deleted:

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;

final class Author extends Model
{
use HasFactory;
use SoftDeletes;

protected static function booted(): void
{
self::deleted(static function (Author $author): void {
$author->posts()->delete();
});
}

public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}

В приведённой выше модели видно, что слушатель определяется внутри метода модели booted. Для прослушивания события модели deleted используется self::deleted. Аналогично, если бы требовалось создать слушателя для события модели created, можно было бы использовать self::created, и так далее. Метод self::deleted принимает замыкание, получающее App\Models\Author, который удаляется. Это замыкание будет выполнено при удалении модели, что приведёт к удалению всех публикаций автора.

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

Полезный совет: также можно использовать функцию Illuminate\Events\queueable, чтобы сделать замыкание очередным. Это означает, что код слушателя будет помещён в очередь для выполнения в фоновом режиме, а не в тот же жизненный цикл запроса. Для этого можно обновить слушатель следующим образом:

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use function Illuminate\Events\queueable;

final class Author extends Model
{
// ...

protected static function booted(): void
{
self::deleted(queueable(static function (Author $author): void {
$author->posts()->delete();
}));
}

// ...
}

Как видно из вышеприведённого примера, замыкание обёрнуто в функцию Illuminate\Events\queueable.

Прослушивание событий модели с помощью наблюдателей

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

Обычно это классы, находящиеся в каталоге app/Observers и содержащие методы, соответствующие событиям модели, которые необходимо прослушивать. Например, если необходимо прослушать событие модели deleted, то в классе наблюдателя нужно определить метод deleted. Если необходимо прослушивать событие модели created, то в классе наблюдателя нужно определить метод created и так далее.

Давайте посмотрим, как можно создать наблюдателя модели для модели App\Models\Author, слушающего событие модели deleted:

declare(strict_types=1);

namespace App\Observers;

use App\Models\Author;

final readonly class AuthorObserver
{
public function deleted(Author $author): void
{
$author->posts()->delete();
}
}

Как видно из приведённого выше кода, был создан наблюдатель, содержащий метод deleted. Этот метод принимает экземпляр удаляемой модели App\Models\Author. Затем удаляются публикации автора с помощью метода delete отношения posts.

Допустим, в качестве примера, нужно определить слушателей для событий модели created и updated. Можно обновить наблюдателя следующим образом:

declare(strict_types=1);

namespace App\Observers;

use App\Models\Author;

final readonly class AuthorObserver
{
public function created(Author $author): void
{
// Логика, выполняемая при создании автора...
}

public function updated(Author $author): void
{
// Логика, выполняемая при обновлении автора...
}

public function deleted(Author $author): void
{
$author->posts()->delete();
}
}

Чтобы методы App\Observers\AuthorObserver заработали, необходимо указать Laravel использовать их. Для этого можно воспользоваться атрибутом #[Illuminate\Database\Eloquent\Attributes\ObservedBy]. Это позволит связать наблюдателя с моделью, подобно тому, как регистрируются глобальные диапазоны запросов с помощью атрибута #[ScopedBy] (как показано в статье Освоение области запросов в Laravel). Обновим модель App\Models\Author для использования наблюдателя следующим образом:

declare(strict_types=1);

namespace App\Models;

use App\Observers\AuthorObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;

#[ObservedBy(AuthorObserver::class)]
final class Author extends Model
{
// ...
}

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

Тестирование событий модели

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

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

Сначала напишем тест, гарантирующий, что публикации автора будут мягко удалены, когда автор будет мягко удалён. Тест может выглядеть примерно так:

declare(strict_types=1);

namespace Tests\Feature\Models;

use App\Models\Author;
use App\Models\Post;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class AuthorTest extends TestCase
{
use LazilyRefreshDatabase;

#[Test]
public function author_can_be_soft_deleted(): void
{
// Создание автора и публикации.
$author = Author::factory()->create();

$post = Post::factory()->for($author)->create();

// Удаление автора.
$author->delete();

// Утверждение, что автор и связанная с ним публикация
// мягко удалены.
$this->assertSoftDeleted($author);
$this->assertSoftDeleted($post);
}
}

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

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

Аналогичным образом можно написать несколько тестов, проверяющих, что время чтения публикации вычисляется при её создании или обновлении. Тесты могут выглядеть примерно так:

declare(strict_types=1);

namespace Tests\Feature\Models;

use App\Models\Author;
use App\Models\Post;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class PostTest extends TestCase
{
use LazilyRefreshDatabase;

#[Test]
public function read_time_is_calculated_when_storing_post(): void
{
$post = Post::factory()
->for(Author::factory())
->create([
'content' => 'This is a post with some content.'
]);

$this->assertSame(2, $post->read_time_in_seconds);
}

#[Test]
public function read_time_is_calculated_when_updating_post(): void
{
$post = Post::factory()
->for(Author::factory())
->create();

$post->content = 'This is a post with some content. ...';
$post->save();

$this->assertSame(8, $post->read_time_in_seconds);
}
}

У нас есть два теста:

Проблемы связанные с использованием событий модели

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

События модели отправляются только от моделей Eloquent. Это означает, что если используется фасад Illuminate\Support\Facades\DB для взаимодействия с базовыми данными модели в базе данных, его события не будут отправляться.

Например, возьмём простой пример, в котором автор удаляется с помощью фасада Illuminate\Support\Facades\DB:

use Illuminate\Support\Facades\DB;

DB::table('authors')
->where('id', $author->id)
->delete();

Выполнение приведённого выше кода приведёт к удалению автора из базы данных, как и ожидалось. Но события модели deleting и deleted не будут отправлены. Поэтому, если для этих событий модели были определены слушатели при удалении автора, они не будут запущены.

Аналогично, если выполняется массовое обновление или удаление моделей с помощью Eloquent, события модели saved, updated, deleted и deleting не будут отправляться для затронутых моделей. Это происходит потому, что события отправляются из самих моделей. Но при массовом обновлении и удалении модели фактически не извлекаются из базы данных, поэтому события не отправляются.

Предположим, что для удаления автора используется следующий код:

use App\Models\Author;

Author::query()->whereKey($author->id)->delete();

Поскольку метод delete вызывается непосредственно на конструкторе запросов, события модели deleting и deleted не будут отправляться для этого автора.

Альтернативные подходы

Мне нравится использовать события модели в своих проектах. Они служат отличным способом разделения кода, а также позволяют автоматически запускать логику, когда нет достаточного контроля над кодом, влияющим на модель. Например, если удаляется автор в Laravel Nova, всё равно можно запустить некоторую логику, при удалении автора.

Однако важно знать, когда стоит прибегнуть к другому подходу.

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

Поэтому можно создать три отдельных слушателя (по одному для каждой из этих задач), выполняющихся каждый раз, когда создаётся новый экземпляр App\Models\Post.

А теперь вернёмся к одному из предыдущих тестов:

declare(strict_types=1);

namespace Tests\Feature\Models;

use App\Models\Author;
use App\Models\Post;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class AuthorTest extends TestCase
{
use LazilyRefreshDatabase;

#[Test]
public function author_can_be_soft_deleted(): void
{
$author = Author::factory()->create();

$post = Post::factory()->for($author)->create();

$author->delete();

$this->assertSoftDeleted($author);
$this->assertSoftDeleted($post);
}
}

Если выполнить приведённый выше тест, то при создании модели App\Models\Post через её фабрику также будут выполняться эти три действия. Конечно, вычисление времени чтения — это второстепенная задача, поэтому она не имеет большого значения. Но не нужно пытаться выполнять вызовы API или отправлять уведомления во время тестирования. Это непредвиденные побочные эффекты. Если разработчик, пишущий тесты, не знает об этих побочных эффектах, это может усложнить отслеживание причин, по которым эти действия происходят.

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

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

Одним из подходов может быть извлечение кода создания App\Models\Post в класс сервиса или экшена. Например, простой класс сервиса может выглядеть следующим образом:

declare(strict_types=1);

namespace App\Services;

use App\DataTransferObjects\PostData;
use App\Models\Post;
use Illuminate\Support\Str;

final readonly class PostService
{
public function createPost(PostData $postData): void
{
$post = Post::create([
'title' => $postData->title,
'content' => $postData->content,
'author_id' => $postData->authorId,
'read_time_in_seconds' => $this->calculateReadTime($postData->content),
]);

$this->sendPostCreatedNotification($post);
$this->publishToTwitter($post);
}

public function updatePost(Post $post, PostData $postData): void
{
$post->update([
'title' => $postData->title,
'content' => $postData->content,
'read_time_in_seconds' => $this->calculateReadTime($postData->content),
]);
}

private function calculateReadTime(string $content): int
{
return (int) ceil(
(Str::wordCount($content) / 265) * 60
);
}

private function sendPostCreatedNotification(Post $post): void
{
// Отправка уведомления всем подписчикам...
}

private function publishToTwitter(Post $post): void
{
// Выполнение вызова API Twitter...
}
}

В приведённом выше классе в ручную вызывается код, вычисляющий время чтения, отправляющий уведомление и публикующий его в Twitter. Это означает, что есть больше контроля над тем, когда эти действия выполняются. Также можно легко имитировать эти методы в тестах, предотвращая их запуск. Кроме того, есть возможность поставить эти действия в очередь, если это потребуется (что, скорее всего, и произойдёт в данном сценарии).

В результате этого можно отказаться от использования событий и слушателей модели для этих действий. Это означает, что можно использовать новый класс App\Services\PostService в коде приложения и безопасно использовать фабрики моделей в коде тестов.

Бонус такого подхода заключается в том, что код становится более понятным. Как уже упоминалось, общая критика использования событий и слушателей заключается в том, что они могут скрывать бизнес-логику в неожиданных местах. Так что если к команде присоединится новый разработчик, он может не знать, где и почему происходят определённые действия, вызванные событием модели.

Однако если вы всё же хотите использовать события и слушателей для подобной логики, можно рассмотреть возможность применения более явного подхода. Например, можно отправлять событие из класса сервиса, которое запускает слушателей. Таким образом, вы всё ещё можете использовать преимущества разделения событий и слушателей, но будет больше контроля над тем, когда события будут отправляться.

Например, можно обновить метод createPost в примере App\Services\PostService выше, чтобы он отправлял событие:

declare(strict_types=1);

namespace App\Services;

use App\DataTransferObjects\PostData;
use App\Events\PostCreated;
use App\Models\Post;
use Illuminate\Support\Str;

final readonly class PostService
{
public function createPost(PostData $postData): void
{
$post = Post::create([
'title' => $postData->title,
'content' => $postData->content,
'author_id' => $postData->authorId,
'read_time_in_seconds' => $this->calculateReadTime($postData->content),
]);

PostCreated::dispatch($post);
}

// ...

}

Используя описанный выше подход, по-прежнему можно иметь отдельных слушателей для выполнения API-запроса к Twitter и отправки уведомления. Но при этом появляется больше контроля над тем, когда эти действия выполняются, поэтому при использовании фабрик моделей они не запускаются внутри тестов.

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

Преимущества и недостатки использования событий модели

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

Преимущества

Недостатки

Заключение

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

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

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

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

Создание npm пакета на TypeScript с поддержкой CommonJS и ESM

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

Как копировать папки через SSH