Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Eloquent

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

31 питань

Eloquent - це ORM (object-relational mapping) Laravel, реалізація патерну Active Record. Кожній таблиці відповідає модель (зазвичай у app/Models), рядок таблиці - це екземпляр моделі, а робота з даними виглядає як робота зі звичайними PHP-об'єктами замість написання SQL.

Створити модель (за конвенцією однина: Post → таблиця posts):

php artisan make:model Post -mf   # одразу з міграцією та фабрикою

Базові операції (CRUD):

$post = Post::create(['title' => 'Привіт']); // create
$post = Post::find(1);                       // read
$post->update(['title' => 'Оновлено']);      // update
$post->delete();                             // delete

Post::where('is_published', true)->latest()->get();

Що дає Eloquent понад Query Builder:

  • Зв'язки - hasOne, hasMany, belongsTo, belongsToMany, поліморфні.
  • Аксесори/мутатори та касти атрибутів (наприклад, дати, enum, JSON).
  • Події моделі та обзервери (creating, saved, deleted).
  • Scopes для перевикористання умов запитів.

Eloquent побудований поверх Query Builder, тож ті самі методи (where, orderBy, join) доступні і на моделях.

Докладніше в документації: Eloquent ORM

Обидва методи виконують запит, але повертають різне:

  • get() повертає колекцію (Illuminate\Database\Eloquent\Collection) усіх відповідних моделей. Якщо нічого не знайдено - порожню колекцію, а не null. Підходить, коли треба перебрати кілька записів.
  • first() повертає одну першу модель або null, якщо нічого не знайдено. Підходить, коли очікуєш один запис.
$posts = Post::where('active', true)->get();   // Collection (0..N моделей)
$post  = Post::where('slug', $slug)->first();  // Post|null

foreach ($posts as $post) { /* ... */ }        // get() - ітеруємо
echo $post?->title;                            // first() - перевіряємо на null

Споріднені методи:

  • find($id) - пошук за первинним ключем.
  • firstOrFail() / findOrFail() - як first()/find(), але кидають ModelNotFoundException (HTTP 404), якщо запис відсутній.
  • pluck('email') - колекція значень одного стовпця.
  • value('email') - одне скалярне значення з першого рядка.

Підсумок: get() - багато рядків (колекція), first() - один рядок (модель або null).

Докладніше в документації: Eloquent: отримання моделей

Eloquent підтримує всі поширені типи зв'язків між таблицями, кожен оголошується методом на моделі:

  • One To One - hasOne / belongsTo. Приклад: User ↔ Profile.
  • One To Many - hasMany / belongsTo. Приклад: Post → багато Comment.
  • Many To Many - belongsToMany через проміжну (pivot) таблицю. Приклад: User ↔ Role.
  • Has One/Many Through - доступ до віддаленого зв'язку через проміжну модель.
  • Polymorphic - morphTo / morphMany: модель належить кільком типам (наприклад, Comment може належати і Post, і Video).

Оголошення зв'язку:

class Post extends Model
{
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}

class Comment extends Model
{
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

Використання:

$post->comments;               // колекція коментарів
$comment->post->title;         // зворотний бік
Post::with('comments')->get(); // eager loading проти N+1

Завжди завантажуйте потрібні зв'язки через with(), щоб уникнути проблеми N+1.

Докладніше в документації: Зв’язки Eloquent

Soft Deletes - «м'яке» видалення: запис не стирається фізично, а отримує мітку часу в колонці deleted_at. Такі записи автоматично виключаються з усіх запитів.

class Post extends Model
{
    use SoftDeletes; // + $table->softDeletes() у міграції
}

$post->delete(); // ставить deleted_at
Post::withTrashed()->get(); // включно з видаленими
$post->restore(); // відновити
$post->forceDelete(); // видалити назавжди

Навіщо: можливість відновлення, аудит, збереження посилальної цілісності.

Докладніше в документації: Soft Deletes

Вони перетворюють атрибути моделі «на льоту». У сучасному Laravel обидва описуються одним методом, що повертає Attribute:

protected function name(): Attribute
{
    return Attribute::make(
        get: fn (string $value) => ucfirst($value), // accessor (читання)
        set: fn (string $value) => strtolower($value), // mutator (запис)
    );
}
  • Accessor форматує значення при отриманні ($user->name).
  • Mutator форматує значення перед збереженням у БД.

Корисно для форматування, нормалізації або роботи з Value Objects.

Докладніше в документації: Accessors та Mutators

  • find($id) повертає модель за первинним ключем або null.
  • findOrFail($id) повертає модель або кидає ModelNotFoundException, яку Laravel автоматично перетворює на HTTP 404.
$post = Post::find($id);
if (! $post) { abort(404); } // ручна перевірка

$post = Post::findOrFail($id); // те саме одним рядком

findOrFail робить контролери чистішими. Аналогічна пара для запитів - first() / firstOrFail().

Докладніше в документації: Eloquent: не знайдено / findOrFail

Це «upsert»-методи, що позбавляють від ручних перевірок «існує / не існує».

// знайти за email; якщо нема - створити з усіма атрибутами
User::firstOrCreate(
    ['email' => $email],
    ['name' => $name]
);

// знайти за email; оновити name; якщо нема - створити
User::updateOrCreate(
    ['email' => $email],
    ['name' => $name]
);

Перший масив - умови пошуку, другий - значення для створення/оновлення. Споріднений firstOrNew() повертає незбережений екземпляр.

Докладніше в документації: Eloquent: upsert-методи

Зовнішній ключ завжди лежить у таблиці тієї моделі, що належить іншій. Звідси й вибір методу.

// posts.user_id посилається на users.id
class User extends Model
{
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);   // ключ у чужій таблиці
    }
}

class Post extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class); // ключ у своїй таблиці
    }
}

Просте правило: у якій моделі колонка *_id, та модель і пише belongsTo. Інша сторона - hasOne чи hasMany.

Імена за замовчуванням:

  • hasMany шукає в posts колонку user_id - ім'я моделі-власника в snake_case плюс _id;
  • belongsTo бере ім'я методу плюс _id: метод author() шукатиме author_id, і тоді ключ доведеться передати явно, якщо колонка називається інакше.
public function author(): BelongsTo
{
    return $this->belongsTo(User::class, 'user_id');
}

Часта помилка новачків - написати hasOne там, де потрібен belongsTo, бо «в пості є один автор». Запит тоді шукатиме users.post_id, якого не існує.

Докладніше в документації: Зворотний бік один до багатьох

Коли модель повертається з контролера чи перетворюється через toArray() / toJson(), у результат потрапляють усі атрибути з бази, а також завантажені зв'язки.

$hidden - атрибути, яких не має бути в масиві чи JSON:

class User extends Model
{
    protected $hidden = ['password', 'remember_token', 'two_factor_secret'];
}

У Laravel 13 те саме можна записати атрибутом класу:

#[Hidden(['password', 'remember_token'])]
class User extends Model {}

$visible - навпаки, білий список: лише ці атрибути. Безпечніше, бо нова колонка в таблиці не з'явиться у відповіді автоматично.

$appends - додати обчислене значення з аксесора:

protected $appends = ['is_admin'];

protected function isAdmin(): Attribute
{
    return Attribute::get(fn (): bool => $this->role === 'admin');
}

Без $appends аксесор працює в коді ($user->is_admin), але в JSON не потрапляє.

Тимчасово змінити видимість:

$user->makeVisible('email')->toArray();
$user->makeHidden('phone')->toArray();
$users->append('is_admin');      // для колекції
$user->setVisible(['id', 'name']);

Зв'язки: завантажені зв'язки серіалізуються разом з моделлю (ключ у snake_case). Щоб приховати зв'язок, його назву додають у $hidden.

Дати серіалізуються в ISO 8601 з UTC (2026-10-04T09:00:00.000000Z). Формат змінюють перевизначенням serializeDate() чи в касті: 'published_at' => 'datetime:Y-m-d'.

Підводні камені:

  • $appends з аксесором, що звертається до зв'язку, дає N+1 при серіалізації колекції - кожна модель завантажує зв'язок окремо;
  • $hidden - не захист: у коді атрибут доступний, і його можна вивести в шаблоні чи лог;
  • для API надійніше API Resources: формат описано явно, і він не залежить від того, які колонки є в таблиці.

Докладніше в документації: Серіалізація: приховування атрибутів

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.

Докладніше в документації: Eloquent: Eager Loading

Поліморфний зв'язок дозволяє моделі належати кільком різним типам моделей через один зв'язок.

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, очищення пов'язаних ресурсів, аудиту.

Докладніше в документації: Observers

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().

Докладніше в документації: Query Scopes

Chunking обробляє великі набори даних порціями, щоб не тримати всі рядки в пам'яті одразу.

Post::chunk(200, function ($posts) {
    foreach ($posts as $post) { /* ... */ }
});
  • chunkById(200, ...) - безпечніший, коли під час обробки змінюються записи (нумерує за id, а не за offset).
  • lazy() / cursor() - повертають LazyCollection: ще менше пам'яті, але один активний запит.

Без chunking Post::all() на мільйонній таблиці впаде з браку пам'яті.

Докладніше в документації: Eloquent: chunk

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.

Докладніше в документації: Custom Casts