Middle: питання на співбесіді з теми «Eloquent»
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
8 питань
N+1 виникає, коли ви завантажуєте N моделей одним запитом, а потім у циклі звертаєтесь до їхнього зв'язку - це генерує ще N запитів.
$posts = Post::all(); // 1 запит
foreach ($posts as $post) {
echo $post->author->name; // +1 запит на кожен пост → N запитів
}
Рішення - eager loading через with():
$posts = Post::with('author')->get(); // лише 2 запити загалом
Вкладені та умовні зв'язки:
Post::with(['author', 'comments.user'])->get(); // вкладений eager
Post::with(['comments' => fn ($q) => $q->latest()])->get(); // умовний
Лічильники без завантаження зв'язку - withCount() (без N+1 і без вантаження самих рядків):
$posts = Post::withCount('comments')->get(); // доступ через $post->comments_count
Виявлення: Model::preventLazyLoading(! app()->isProduction()) у boot() кидає виняток на ледачих завантаженнях поза продакшеном; також допомагають Telescope і Debugbar.
Поліморфний зв'язок дозволяє моделі належати кільком різним типам моделей через один зв'язок.
class Comment extends Model
{
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
class Post extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
Таблиця comments має commentable_id + commentable_type. Тож Comment може належати і Post, і Video без окремих таблиць. Бувають також many-to-many поліморфні зв'язки (morphToMany), напр. теги.
Observer групує слухачів подій моделі (creating, created, updating, saved, deleting тощо) в один клас - замість роздування boot() моделі.
class PostObserver
{
public function creating(Post $post): void
{
$post->slug = Str::slug($post->title);
}
public function deleted(Post $post): void
{
$post->image()->delete();
}
}
Реєстрація - атрибутом #[ObservedBy(PostObserver::class)] на моделі або в Service Provider. Зручно для генерації slug, очищення пов'язаних ресурсів, аудиту.
Scopes інкапсулюють часто вживані умови запитів.
Local scope - викликається вручну:
public function scopePublished(Builder $query): Builder
{
return $query->where('is_published', true);
}
Post::published()->latest()->get();
Global scope - застосовується автоматично до всіх запитів моделі:
#[ScopedBy([TenantScope::class])]
class Invoice extends Model {}
SoftDeletes - приклад глобального scope (автоматично додає where deleted_at is null). Глобальний scope можна обійти через withoutGlobalScope().
Chunking обробляє великі набори даних порціями, щоб не тримати всі рядки в пам'яті одразу.
Post::chunk(200, function ($posts) {
foreach ($posts as $post) { /* ... */ }
});
chunkById(200, ...)- безпечніший, коли під час обробки змінюються записи (нумерує заid, а не за offset).lazy()/cursor()- повертають LazyCollection: ще менше пам'яті, але один активний запит.
Без chunking Post::all() на мільйонній таблиці впаде з браку пам'яті.
Custom Cast інкапсулює логіку перетворення атрибута між форматом БД та об'єктом PHP.
class Money implements CastsAttributes
{
public function get($model, $key, $value, $attributes): MoneyValue
{
return new MoneyValue($value); // з БД → Value Object
}
public function set($model, $key, $value, $attributes): array
{
return ['price' => $value->cents]; // VO → у БД
}
}
protected $casts = ['price' => Money::class];
Застосування: робота з Value Objects, шифрування полів, JSON-структури. Вбудовані касти: array, encrypted, datetime, enum-класи, AsCollection.
| Query Builder | Eloquent | |
|---|---|---|
| Повертає | stdClass / масиви |
моделі |
| Рівень | близько до SQL | ORM поверх QB |
| Зв'язки, події, касти | ні | так |
| Оверхед | мінімальний | невеликий |
// Query Builder
DB::table('users')->where('active', 1)->get();
// Eloquent
User::where('active', 1)->get();
Eloquent виразніший і зручніший для бізнес-логіки. Для важких масових операцій (мільйони рядків, складні агрегати) інколи свідомо обирають чистий Query Builder заради швидкості та меншого споживання пам'яті.
У belongsToMany проміжна (pivot) таблиця зберігає зв'язки. Додаткові стовпці на ній оголошують через withPivot():
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)
->withPivot('assigned_at', 'is_primary')
->withTimestamps();
}
Доступ і керування:
$user->roles->first()->pivot->assigned_at; // читання pivot-даних
$user->roles()->attach($roleId, ['is_primary' => true]); // додати
$user->roles()->detach($roleId); // прибрати
$user->roles()->sync([1, 2, 3]);// привести до набору
$user->roles()->updateExistingPivot($roleId, [...]); // оновити pivot
Для окремої моделі pivot використовують ->using(RoleUser::class).