Eloquent: Примеры трейтов в Моделях

Источник: «Traits in Laravel Eloquent: 4 Practical Examples»
Трейты в PHP — мощный функционал позволяющий повторно использовать код в нескольких классах не повторяясь.

В этой статье я покажу четыре примера использования трейтов в Моделях Eloquent.


Пример 1. Общие отношения

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

Пример можно найти в пакете spatie laravel-permissions в нём есть трейт HasRoles:

/**
* A model may have multiple roles.
*/

public function roles(): BelongsToMany
{
$relation = $this->morphToMany(
config('permission.models.role'),
'model',
config('permission.table_names.model_has_roles'),
config('permission.column_names.model_morph_key'),
app(PermissionRegistrar::class)->pivotRole
);

if (! app(PermissionRegistrar::class)->teams) {
return $relation;
}

return $relation->wherePivot(app(PermissionRegistrar::class)->teamsKey, getPermissionsTeamId())
->where(function ($q) {
$teamField = config('permission.table_names.roles').'.'.app(PermissionRegistrar::class)->teamsKey;
$q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId());
});
}

В этом трейте есть отношение role(). Значит, следующая модель, использующая этот трейт, также будет иметь отношение role().

app/Models/User.php:

use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
use HasRoles;

// ...
}

И если вам нужна это отношение в другой модели, можно просто добавить трейт use HasRole и в эту модель. Звучит здорово, правда?

Пример 2. Многоразовые методы

Ещё один отличный вариант использований трейтов — использовать повторно используемые методы.

Давайте взглянем на трейт HasTag из пакета spatie laravel-tags:

 public function scopeWithoutTags(
Builder $query,
string | array | ArrayAccess | Tag $tags,
string $type = null
): Builder {
$tags = static::convertToTags($tags, $type);

return $query
->whereDoesntHave('tags', function (Builder $query) use ($tags) {
$tagIds = collect($tags)->pluck('id');

$query->whereIn('tags.id', $tagIds);
});
}

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

Кроме того, у это трейта есть метода присоединяющие/отсоединяющие одиночные/множественные теги:

public function attachTags(array | ArrayAccess | Tag $tags, string $type = null): static
{
$className = static::getTagClassName();

$tags = collect($className::findOrCreate($tags, $type));

$this->tags()->syncWithoutDetaching($tags->pluck('id')->toArray());

return $this;
}

public function attachTag(string | Tag $tag, string | null $type = null)
{
return $this->attachTags([$tag], $type);
}

public function detachTags(array | ArrayAccess $tags, string | null $type = null): static
{
$tags = static::convertToTags($tags, $type);

collect($tags)
->filter()
->each(fn (Tag $tag) => $this->tags()->detach($tag));

return $this;
}

public function detachTag(string | Tag $tag, string | null $type = null): static
{
return $this->detachTags([$tag], $type);
}

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

app/Models/Post.php:

use Spatie\Tags\HasTags;

class Post extends Model
{
use HasTags;

// ...
}

И теперь мы можем использовать эти методы в нашей модели:

$post = Post::create(['title' => 'My first post']);
$post->attachTag('laravel');

Как это круто? Мы просто перестали повторяться и сделали наш код более читабельным и удобным в сопровождении.

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

Пример 3. Расширение трейтов

В некоторых случаях вы хотите изменить как трейт работает.

Давайте посмотрим трейт InteractsWithMedia из пакета spatie laravel-medialibrary

Посмотрите на метод getFallbackMediaUrl():

public function getFallbackMediaUrl(string $collectionName = 'default', string $conversionName = ''): string
{
$fallbackUrls = optional($this->getMediaCollection($collectionName))->fallbackUrls;

if (in_array($conversionName, ['', 'default'], true)) {
return $fallbackUrls['default'] ?? '';
}

return $fallbackUrls[$conversionName] ?? $fallbackUrls['default'] ?? '';
}

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

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

/app/Models/Post.php:

use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;

class Post extends Model implements HasMedia
{
use InteractsWithMedia;

// ...

public function getFallbackMediaUrl(string $collectionName = 'default', string $conversionName = ''): string
{
$fallbackUrls = optional($this->getMediaCollection($collectionName))->fallbackUrls;

$defaultMediaUrl = "https://laravel.com/img/logotype.min.svg";

if (in_array($conversionName, ['', 'default'], true)) {
return $fallbackUrls['default'] ?? $defaultMediaUrl;
}

return $fallbackUrls[$conversionName] ?? $fallbackUrls['default'] ?? $defaultMediaUrl;
}
}

Пример 4. Добавление слушателей из трейтов

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

Например, если вы хотите, чтобы при удалении модели выполнялось определённое действие. Этот вариант можно найти в ранее упомянутой spatie laravel-medialibrary.

Давайте найдём метод bootInteractsWithMedia() и посмотрим что он делает:

public static function bootInteractsWithMedia()
{
static::deleting(function (HasMedia $model) {
if ($model->shouldDeletePreservingMedia()) {
return;
}

if (in_array(SoftDeletes::class, class_uses_recursive($model))) {
if (! $model->forceDeleting) {
return;
}
}

$model->media()->cursor()->each(fn (Media $media) => $media->delete());
});
}

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

Это быстрый способ добавить Слушателя в Модель, не добавляя его в саму Модель.

Эти методы загрузки имеют правила именования для автоматической загрузки Laravel:

  1. Начинается с boot
  2. Включает имя трейта в нотации PascalCase

Например, boot + InteractsWithMedia = bootInteractsWithMedia

Заключение

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

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

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

OpenAI PHP Client. Документация

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

CSP: Политика безопасности контента