Eloquent ORM
Модели, отношения, N+1, скоупы, аксессоры, события моделей.
Junior Как устроена Eloquent-модель? Основные свойства и конвенции.
class Post extends Model
{
protected $table = 'posts'; // по умолчанию — snake_case множественное от класса
protected $primaryKey = 'id'; // по умолчанию id, автоинкремент
public $timestamps = true; // created_at / updated_at
protected $fillable = ['title', 'body']; // разрешённые для mass-assignment
protected $guarded = ['id']; // наоборот — запрещённые (остальные можно)
protected $hidden = ['secret_token']; // скрыть из toArray()/JSON
protected $casts = [ // приведение типов
'published_at' => 'datetime',
'meta' => 'array',
'is_admin' => 'boolean',
];
}
Основные операции:
Post::all(); Post::find(1); Post::findOrFail(1);
Post::where('published', true)->orderByDesc('id')->get();
$post = Post::create(['title' => 'Hi']); // требует $fillable
$post->update(['title' => 'New']);
$post->delete();
Post::firstOrCreate(['slug' => 'x'], ['title' => 'X']);
Post::updateOrCreate(['id' => 1], ['title' => 'Y']);
findOrFail / firstOrFail кидают ModelNotFoundException → 404 в HTTP-контексте.
Middle Перечислите типы отношений Eloquent с примерами.
// Один к одному
public function profile() { return $this->hasOne(Profile::class); }
// Один ко многим (и обратное)
public function posts() { return $this->hasMany(Post::class); }
public function user() { return $this->belongsTo(User::class); }
// Многие ко многим — через pivot-таблицу post_tag
public function tags()
{
return $this->belongsToMany(Tag::class)
->withPivot('sort') // доп. колонки pivot
->withTimestamps();
}
$post->tags()->attach($tagId, ['sort' => 1]);
$post->tags()->detach($tagId);
$post->tags()->sync([1, 2, 3]); // привести к точному набору
$post->tags()->syncWithoutDetaching([4]);
// Сквозные и полиморфные
public function comments() { return $this->morphMany(Comment::class, 'commentable'); }
public function posts() { return $this->hasManyThrough(Post::class, Country::class); } // hasManyThrough
Ключи по конвенции: user_id, commentable_id + commentable_type. Проверка на собеседовании: уметь нарисовать схему таблиц для belongsToMany и morphMany.
Middle Что такое проблема N+1 и как её решают в Laravel?
Классика собеседований. Ленивая загрузка отношений в цикле порождает запрос на каждую итерацию:
$posts = Post::all(); // 1 запрос
foreach ($posts as $post) {
echo $post->user->name; // +N запросов — по одному на пост!
}
Решение — eager loading:
Post::with('user')->get(); // 2 запроса: posts + users WHERE id IN (...)
Post::with(['user', 'comments.author']); // вложенные
Post::with(['comments' => fn ($q) => $q->latest()->limit(5)]); // с ограничениями
Post::withCount('comments'); // comments_count без загрузки моделей
Post::withSum('orders', 'total');
Дополнительно:
$post->load('user')— eager loading для уже полученной коллекции;Model::preventLazyLoading()— в dev-окружении запретить ленивую загрузку, чтобы N+1 падала ошибкой:
Model::preventLazyLoading(! app()->isProduction());
- инструменты поиска N+1: Laravel Telescope, Debugbar,
DB::listen().
Middle Что такое Query Scopes (локальные и глобальные)?
Локальный скоуп — переиспользуемое ограничение запроса:
class Post extends Model
{
public function scopePublished(Builder $query): void
{
$query->where('published', true)->whereNotNull('published_at');
}
public function scopeOfAuthor(Builder $query, User $user): void // с параметром
{
$query->whereBelongsTo($user, 'author');
}
}
Post::published()->ofAuthor($user)->get();
Глобальный скоуп — применяется ко ВСЕМ запросам модели:
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('tenant_id', auth()->user()->tenant_id);
}
}
protected static function booted(): void
{
static::addGlobalScope(new TenantScope);
// или анонимно:
static::addGlobalScope('active', fn (Builder $q) => $q->where('active', true));
}
Снять глобальный скоуп: Post::withoutGlobalScope(TenantScope::class)->get(). SoftDeletes — тоже глобальный скоуп; withTrashed() его отменяет.
Middle Аксессоры и мутаторы: как переопределять чтение и запись атрибутов?
Современный синтаксис (Laravel 9+) — единый метод Attribute:
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function fullName(): Attribute
{
return Attribute::make(
get: fn ($value, $attributes) => $attributes['first_name'].' '.$attributes['last_name'],
);
}
protected function title(): Attribute
{
return Attribute::make(
get: fn (string $value) => ucfirst($value),
set: fn (string $value) => strtolower($value), // пишется в БД в lowercase
);
}
$post->full_name; // аксессор
Старый синтаксис (встречается в легаси): getFullNameAttribute() / setTitleAttribute().
Когда что использовать:
- аксессор/мутатор — сложная логика преобразования;
$casts— простое приведение типов (datetime,array,encrypted);Attribute::shouldCache()— если аксессор дорогой и возвращает объект.
Осторожно: мутатор на set срабатывает только через модель, массовый DB::table()->update() его обходит.
Middle Как работают Soft Deletes и события моделей?
Soft Deletes — запись не удаляется, а помечается deleted_at:
class Post extends Model { use SoftDeletes; }
// + $table->softDeletes(); в миграции
$post->delete(); // deleted_at = now()
Post::withTrashed()->get(); // включая удалённые
Post::onlyTrashed()->get();
$post->restore();
$post->forceDelete(); // удалить физически
Все запросы по умолчанию исключают «удалённые» (глобальный скоуп). Уникальные индексы с soft deletes — известная засада: unique(email) мешает создать запись после удаления — решают составным индексом с deleted_at или генерацией значения при удалении.
События моделей: creating/created, updating/updated, saving/saved, deleting/deleted, restoring/restored, retrieved.
protected static function booted(): void
{
static::creating(fn (Post $post) => $post->slug = Str::slug($post->title));
}
// или через Observer: php artisan make:observer PostObserver --model=Post
Важно: массовые update()/delete() через Query Builder события не вызывают — события работают на уровне экземпляров моделей.
Middle Eloquent Collections: чем коллекция Laravel отличается от массива и какие методы полезны?
get() возвращает Illuminate\Database\Eloquent\Collection (наследует базовую Collection) — обёртку с десятками методов в функциональном стиле:
$users->filter(fn ($u) => $u->active)->map(fn ($u) => $u->name)->values();
$users->pluck('email'); // колонку в коллекцию
$users->sortByDesc('created_at');
$users->groupBy('role');
$users->sum('balance'); $users->avg(...); $users->count();
$users->firstWhere('id', 5);
$users->contains($user); $users->find(3); // по первичному ключу
$users->fresh(); // перечитать из БД
$users->toArray(); $users->toJson();
Ключевой момент для собеседования — разница между where() у Collection и у Builder: первый фильтрует уже загруженное в памяти, второй строит SQL. Загрузить всё и фильтровать коллекцией — антипаттерн.
Lazy-коллекции (cursor(), lazy()) — для больших выборок с обработкой по одной записи, не загружая всё в память. Higher-order сообщения: $users->each->markAsActive().
Middle Как сериализуются модели в API: $hidden, $appends, API Resources?
Модели умеют toArray()/toJson() — при return $post из роута Laravel сам сериализует в JSON.
protected $hidden = ['password', 'remember_token']; // скрыть
protected $visible = ['id', 'name']; // белый список
protected $appends = ['full_name']; // добавить аксессор в вывод
Для реальных API — API Resources (трансформация + мета-данные):
php artisan make:resource UserResource
class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->when($request->user()->isAdmin(), $this->email),
'posts_count' => $this->whenCounted('posts'),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
return new UserResource($user);
return UserResource::collection(User::paginate());
Преимущества: единый контракт ответа, условные поля, вложенные ресурсы (new PostResource($this->whenLoaded('post'))), автоматическая обёртка data.