Советы по Моделям Laravel

Источник: «Laravel Model Tips»
Laravel предоставляет огромное количество классных возможностей, помогающих улучшить опыт разработки (DX). Но из-за регулярных релизов, стрессов, связанных с повседневной работой, и огромного количества доступных функций легко упустить некоторые менее известные возможности, которые могут помочь улучшить код.

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

Выявление и предотвращение проблем N+1

Первый совет, который рассмотрим, — как обнаружить и предотвратить N+1 запросы.

N+1 запросы — распространённая проблема, возникающая при ленивой загрузке отношений, где N — количество запросов, выполняемых для получения связанных моделей.

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

$posts = Post::all();

foreach ($posts as $post) {
// Делаем что-нибудь с сообщением...

// Получаем пользователя сообщения
echo $post->user->name;
}

Хотя приведённый выше код выглядит нормально, на самом деле он приведёт к проблеме N+1. Допустим, в базе данных 100 сообщений. В первой строке выполняется один запрос для получения всех сообщений. Затем внутри цикла foreach, когда обращаемся к $post->user, это вызовет новый запрос, чтобы получить пользователя для этого сообщения; в результате получится ещё 100 запросов. Это означает, что в общей сложности будет выполнен 101 запрос. Как можно себе представить, это не очень хорошо! Это может замедлить работу приложения и создать ненужную нагрузку на базу данных.

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

К счастью, Laravel предоставляет удобный метод Model::preventLazyLoading(), который можно использовать для выявления и предотвращения подобных проблем с N+1. Этот метод предписывает Laravel выбрасывать исключение при ленивой загрузке отношений, так что можете быть уверены, что всегда выполняете нетерпеливую загрузку отношений.

Чтобы использовать этот метод, можно добавить вызов метода Model::preventLazyLoading() в класс App\Providers\AppServiceProvider:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventLazyLoading();
}
}

Теперь, если запустить код, приведённый выше, для получения каждого сообщения и доступа к пользователю, создавшему это сообщение, возникнет исключение Illuminate\Database\LazyLoadingViolationException с сообщением:

Attempted to lazy load [user] on model [App\Models\Post] but lazy loading is disabled.

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

$posts = Post::with('user')->get();

foreach ($posts as $post) {
// Делаем что-нибудь с сообщением...

// Получаем пользователя сообщения
echo $post->user->name;
}

Теперь приведённый выше код будет успешно запущен и вызовет только два запроса: один для получения всех сообщений и один для получения всех пользователей для этих сообщений.

Предотвращение доступа к отсутствующим атрибутам

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

Представьте, что есть модель App\Models\User со следующими полями:

Что произойдёт, если выполнить следующий код?

$user = User::query()->first();

$name = $user->full_name;

Если предположить, что в модели нет аксессора full_name, переменная $name будет равна null. Но мы не будем знать, происходит это потому, что поле full_name на самом деле равно null, потому что поле не было получено из базы данных или потому, что поле не существует в модели. Как можно представить, это может привести к неожиданному поведению, которое иногда бывает трудно обнаружить.

Laravel предоставляет метод Model::preventAccessingMissingAttributes(), позволяющий предотвратить эту проблему. Метод предписывает Laravel выбрасывать исключение всякий раз, когда происходит попытка доступа к полю, не существующему в текущем экземпляре модели.

Чтобы включить эту функцию, можно добавить вызов метода Model::preventAccessingMissingAttributes() в класс App\Providers\AppServiceProvider:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventAccessingMissingAttributes();
}
}

Теперь, если выполнить код примера и попытаться получить доступ к полю full_name в модели App\Models\User, возникнет исключение Illuminate\Database\Eloquent\MissingAttributeException с сообщением:

The attribute [full_name] either does not exist or was not retrieved for model [App\Models\User].

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

$user = User::query()
->select(['id', 'name'])
->first();

$user->email;

Если запретить доступ к отсутствующим атрибутам, то возникнет исключение:

The attribute [email] either does not exist or was not retrieved for model [App\Models\User].

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

Стоит отметить, что метод preventAccessingMissingAttributes был удалён из документации Laravel (коммит), но он по-прежнему работает. Не уверен в причине его удаления, но об этом стоит знать. Это может быть признаком того, что он будет удалён в будущем.

Предотвращение тихого сброса атрибутов

Подобно методу preventAccessingMissingAttributes, Laravel предоставляет метод preventSilentlyDiscardingAttributes, помогающий предотвратить неожиданное поведение при обновлении моделей.

Предположим, есть класс модели App\Models\User:

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'password',
];

// ...
}

Как видите, поля name, email и password являются заполняемыми (fillable). Но что произойдёт, если попытаться обновить несуществующее поле в модели (например, full_name) или поле, которое существует, но не заполняется (например, email_verified_at)?

$user = User::query()->first();

$user->update([
'full_name' => 'Ash', // Поле не существует
'email_verified_at' => now(), // Поле существует, но не заполняемое
// Обновление других полей здесь...
]);

Если запустить приведённый выше код, поля full_name и email_verified_at будут проигнорированы, поскольку они не были определены как заполняемые поля. Но при этом не возникнет никаких ошибок, и мы не узнаем, что эти поля были молча отброшены.

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

Чтобы использовать этот метод, можно добавить вызов метода Model::preventSilentlyDiscardingAttributes() в класс App\Providers\AppServiceProvider:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes();
}
}

Вышеописанное приведёт к выбрасыванию ошибки.

Теперь, если попробовать выполнить приведённый выше пример кода и обновить пользовательские поля first_name и email_verified_at, возникнет исключение Illuminate\Database\Eloquent\MassAssignmentException с сообщением:

Add fillable property [full_name, email_verified_at] to allow mass assignment on [App\Models\User].

Стоит отметить, что метод preventSilentlyDiscardingAttributes будет выделять незаполняемые поля только при использовании таких методов, как fill или update. Если вручную устанавливать каждое свойство, то таких ошибок не будет. Для примера возьмём следующий код:

$user = User::query()->first();

$user->full_name = 'Ash';
$user->email_verified_at = now();

$user->save();

В приведённом выше коде поле full_name не существует в базе данных, поэтому вместо того, чтобы Laravel перехватил его за нас, оно будет перехвачено на уровне базы данных. Если бы использовалась база данных MySQL, то возникла следующая ошибка:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'full_name' in 'field list' (Connection: mysql, SQL: update `users` set `email_verified_at` = 2024-08-02 16:04:08, `full_name` = Ash, `users`.`updated_at` = 2024-08-02 16:04:08 where `id` = 1)

Включение строгого режима для моделей

Если необходимо использовать все три метода, о которых говорилось выше, можно включить их все сразу с помощью метода Model::shouldBeStrict(). Этот метод включит настройки preventLazyLoading, preventAccessingMissingAttributes и preventSilentlyDiscardingAttributes.

Чтобы использовать этот метод, можно добавить вызов Model::shouldBeStrict() в класс App\Providers\AppServiceProvider:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::shouldBeStrict();
}
}

Это эквивалентно:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;

class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventLazyLoading();
Model::preventSilentlyDiscardingAttributes();
Model::preventAccessingMissingAttributes();
}
}

Как и метод preventAccessingMissingAttributes, метод shouldBeStrict был удалён из документации Laravel (коммит), но по-прежнему работает. Это может быть признаком того, что он будет удалён в будущем.

Использование UUID

По умолчанию в моделях Laravel в качестве первичного ключа используются автоинкрементные ID. Но могут возникнуть ситуации, когда желательно использовать универсальные уникальные идентификаторы (UUID).

UUID — это 128-битная (или 36-символьная) буквенно-цифровая строка, используемая для уникальной идентификации ресурсов. Благодаря тому, что они генерируются, крайне маловероятно, что они будут совпадать с другими UUID. Пример UUID: 1fa24c18-39fd-4ff2-8f23-74ccd08462b0.

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

Например, в маршруте используются автоинкрементные ID. Для доступа к пользователю можно создать такой маршрут:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('/users/{user}', function (User $user) {
dd($user->toArray());
});

Злоумышленник может перебирать идентификаторы (например, /users/1, /users/2, /users/3 и т. д.), пытаясь получить доступ к информации других пользователей, если маршруты небезопасны. Если использовать UUID, то URL будут выглядеть примерно так: /users/1fa24c18-39fd-4ff2-8f23-74ccd08462b0, /users/b807d48d-0d01-47ae-8bbc-59b2acea6ed3, и /users/ec1dde93-c67a-4f14-8464-c0d29c95425f. Как видите, их гораздо сложнее угадать.

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

Использование UUID в качестве первичного ключа

Для начала рассмотрим, как изменить первичный ключ на UUID.

Для этого необходимо убедиться, что в таблице есть столбец, способный хранить UUID. Laravel предоставляет удобный метод $table->uuid, который можно использовать в миграциях.

Представьте, что есть базовая миграция, создающая таблицу comments:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/

public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->uuid();
$table->foreignId('user_id');
$table->foreignId('post_id');
$table->string('content');
$table->timestamps();
});
}

// ...
}

Как видно из миграции, в ней определено поле UUID. По умолчанию это поле будет называться uuid, но при желании его можно изменить, передав методу uuid имя столбца.

Затем необходимо указать Laravel использовать новое поле uuid в качестве первичного ключа для модели App\Models\Comment. Также необходимо добавить трейт, позволяющий Laravel автоматически генерировать UUID. Это можно сделать, переопределив свойство $primaryKey модели и использовать трейт Illuminate\Database\Eloquent\Concerns\HasUuids:

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
use HasUuids;

protected $primaryKey = 'uuid';
}

Теперь модель должна быть настроена и готова к использованию UUID в качестве первичного ключа. Возьмём этот пример кода:

use App\Models\Comment;
use App\Models\Post;
use App\Models\User;

$user = User::first();
$post = Post::first();

$comment = new Comment();
$comment->content = 'The comment content goes here.';
$comment->user_id = $user->id;
$comment->post_id = $post->id;
$comment->save();

dd($comment->toArray());

// [
// "content" => "The comment content goes here."
// "user_id" => 1
// "post_id" => 1
// "uuid" => "9cb16a60-8c56-46f9-89d9-d5d118108bc5"
// "updated_at" => "2024-08-05T11:40:16.000000Z"
// "created_at" => "2024-08-05T11:40:16.000000Z"
// ]

В дампе модели видно, что поле uuid было заполнено UUID.

Добавление поля UUID в модель

Если предпочитаете хранить автоинкрементные ID для внутренних отношений, но использовать UUID для публичных идентификаторов, можно добавить поле UUID в модель.

Предположим, что таблице есть поля id и uuid. Поскольку для первичного ключа будет использоваться поле id, не нужно определять свойство $primaryKey в модели.

Можно переопределить метод uniqueIds, доступный через трейт Illuminate\Database\Eloquent\Concerns\HasUuids. Этот метод должен возвращать массив полей, для которых должны быть сгенерированы UUID.

Давайте обновим модель App\Models\Comment, чтобы включить в неё поле, которое называется uuid:

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
use HasUuids;

public function uniqueIds(): array
{
return ['uuid'];
}
}

Теперь, если создать новую модель App\Models\Comment, можно увидеть, что поле uuid было заполнено UUID:

[
"id" => 1
"content" => "The comment content goes here."
"user_id" => 1
"post_id" => 1
"uuid" => "9cb16a60-8c56-46f9-89d9-d5d118108bc5"
"updated_at" => "2024-08-05T11:40:16.000000Z"
"created_at" => "2024-08-05T11:40:16.000000Z"
]

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

Использование ULID

Аналогично использованию UUID в моделях Laravel, иногда возникает необходимость использовать универсально уникальные лексикографически сортируемые идентификаторы (ULID).

ULID — это 128-битная (или 26-символьная) буквенно-цифровая строка, используемая для уникальной идентификации ресурсов. Пример ULID: 01J4HEAEYYVH4N2AKZ8Y1736GD.

Определить поля ULID можно точно так же, как и поля UUID. Единственное отличие заключается в том, что при обновлении модели вместо трейта Illuminate\Database\Eloquent\Concerns\HasUuids следует использовать трейт Illuminate\Database\Eloquent\Concerns\HasUlids.

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

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
use HasUlids;
}

Изменение поля, используемого для привязки модели к маршруту

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

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

По умолчанию Laravel использует поле первичного ключа модели (обычно это поле id) для привязки модели к маршруту. Например, может быть маршрут для отображения информации об одном пользователе:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('/users/{user}', function (User $user) {
dd($user->toArray());
});

Маршрут, определённый в приведённом выше примере, попытается найти пользователя, существующего в базе данных с указанным идентификатором. Например, в базе данных существует пользователь с идентификатором 1. При посещении URL /users/1 Laravel автоматически извлекает пользователя с ID 1 из базы данных и передаёт его в функцию замыкания (или контроллер) для выполнения действий. Но если модель с указанным идентификатором не существует, Laravel автоматически вернёт ответ 404 Not Found.

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

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

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

Изменение поля для всех маршрутов

Если необходимо определить поле, используемое для всех маршрутов, это можно сделать, определив метод getRouteKeyName в модели. Этот метод должен возвращать имя поля, которое необходимо использовать для привязки модели к маршруту.

Например, представьте, что необходимо изменить привязку всех моделей к маршрутам для модели App\Models\Post, чтобы использовать поле slug вместо поля id. Это можно сделать, добавив метод getRouteKeyName в модель Post:

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
use HasFactory;

public function getRouteKeyName()
{
return 'slug';
}

// ...
}

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

use App\Models\Post;
use Illuminate\Support\Facades\Route;

Route::get('/posts/{post}', function (Post $post) {
dd($post->toArray());
});

И при посещении URL /posts/my-first-post Laravel автоматически извлекает из базы данных пост со slug my-first-post и передаёт его в функцию замыкания (или контроллер) для выполнения действий.

Изменение поля для отдельных маршрутов

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

Это можно сделать, используя синтаксис :field в определении маршрута. Предположим, нужно использовать поле slug для привязки модели к маршруту в одном маршруте. Маршрут можно определить следующим образом:

Route::get('/posts/{post:slug}', function (Post $post) {
dd($post->toArray());
});

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

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

При получении нескольких моделей из базы данных с помощью такого метода, как App\Models\User::all(), Laravel обычно помещает их в экземпляр класса Illuminate\Database\Eloquent\Collection. Этот класс предоставляет множество полезных методов для работы с возвращаемыми моделями. Однако могут возникнуть ситуации, когда необходимо вернуть класс коллекции вместо класса по умолчанию.

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

В Laravel легко переопределить тип возвращаемой коллекции.

Рассмотрим пример. Представим, что есть модель App\Models\Post, и когда получаем её из базы данных, необходимо вернуть её в экземпляр класса App\Collections\PostCollection.

Можно создать новый файл app/Collections/PostCollection.php и определить класс коллекции следующим образом:

declare(strict_types=1);

namespace App\Collections;

use App\Models\Post;
use Illuminate\Support\Collection;

/**
* @extends Collection<int, Post>
*/

class PostCollection extends Collection
{
// ...
}

В примере выше был создан новый класс App\Collections\PostCollection, расширяющий класс Illuminate\Support\Collection в Laravel. Кроме того, указали, что эта коллекция будет содержать только экземпляры класса App\Models\Post, используя docblock. Это поможет IDE понять тип данных, находящихся в коллекции.

Затем можно обновить модель App\Models\Post, чтобы она возвращала экземпляр собственного класса коллекции, переопределив метод newCollection следующим образом:

namespace App\Models;

use App\Collections\PostCollection;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
// ...

public function newCollection(array $models = []): PostCollection
{
return new PostCollection($models);
}
}

В примере был использован массив моделей App\Models\Post, переданных в метод newCollection, и возвращён новый экземпляр класса App\Collections\PostCollection.

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

use App\Models\Post;

$posts = Post::all();

// $posts is an instance of App\Collections\PostCollection

Сравнение моделей

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

Давайте рассмотрим распространённые проблемы и причины, по которым их стоит избегать.

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

Предполагая, что в модели App\Models\Comment существует отношение post и что первый комментарий в базе данных принадлежит первому сообщению, давайте рассмотрим пример:

// ⚠️ Избегайте использования `===` при сравнении моделей

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::first();

$postsAreTheSame = $comment->post === $post;

// $postsAreTheSame будет false.

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

Например:

// ⚠️ Избегайте использования `==` при сравнении моделей

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::first();

$postsAreTheSame = $comment->post == $post;

// $postsAreTheSame будет true.

В приведённом выше примере проверка == вернёт true, потому что $comment->post и $post — это один и тот же класс и они имеют одинаковые атрибуты и значения. Но что произойдёт, если изменить атрибуты в модели $post так, чтобы они были разными?

Воспользуемся методом select, чтобы взять только поля id и content из таблицы posts:

// ⚠️ Избегайте использования `==` при сравнении моделей

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::query()->select(['id', 'content'])->first();

$postsAreTheSame = $comment->post == $post;

// $postsAreTheSame будет false.

Даже если $comment->post — это та же модель, что и $post, проверка == вернёт false, потому что у моделей загружены разные атрибуты. Как видите, это может привести к неожиданному поведению, которое довольно сложно отследить, особенно если при добавлении метода select в запрос тесты начинают давать сбой.

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

Для проверки одинаковости двух моделей можно использовать метод is:

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::query()->select(['id', 'content'])->first();

$postsAreTheSame = $comment->post->is($post);

// $postsAreTheSame будет true.

Аналогичным образом можно использовать метод isNot, чтобы проверить, являются ли две модели не одинаковыми:

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::query()->select(['id', 'content'])->first();

$postsAreNotTheSame = $comment->post->isNot($post);

// $postsAreNotTheSame будет false.

Использование whereBelongsTo при построении запросов

Последний совет — скорее личное предпочтение, но думаю, что так запросы легче читать и понимать.

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

$user = User::first();
$post = Post::first();

$comments = Comment::query()
->where('user_id', $user->id)
->where('post_id', $post->id)
->get();

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

$user = User::first();
$post = Post::first();

$comments = Comment::query()
->whereBelongsTo($user)
->whereBelongsTo($post)
->get();

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

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

Заключение

Надеюсь, эта статья дала вам несколько новых советов по работе с моделями Laravel. Теперь вы должны уметь обнаруживать и предотвращать проблемы N+1, предотвращать доступ к отсутствующим атрибутам, предотвращать тихое отбрасывание атрибутов и изменять тип первичного ключа на UUID или ULID. Вы также узнали, как изменить поле, используемое для привязки модели к маршруту, указать тип возвращаемой коллекции, сравнить модели и использовать whereBelongsTo при построении запросов.

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

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

Как создать PHP пакет

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

Что нового в Pest 3 и как его обновить