Eloquent: Active Record vs Data Mapper
Два подхода к ORM
- Active Record — модель знает о БД. Eloquent, Rails AR.
- Data Mapper — модель о БД не знает. Doctrine, Hibernate.
Active Record (Eloquent)
Модель = строка БД + методы:
$user = User::find(1);
$user->name = 'Иван';
$user->save();
Модель умеет:
- Загружать себя.
- Сохранять себя.
- Удалять себя.
- Связи.
Data Mapper (Doctrine)
Модель — POJO. Менеджер — сохраняет:
$user = new User('Иван');
$entityManager->persist($user);
$entityManager->flush();
Модель:
- Не знает о БД.
- Не имеет save().
- Просто данные + логика.
Плюсы Active Record
- Просто. Один объект.
- Кратко. Меньше boilerplate.
- Интуитивно.
$user->save(). - Идеально для CRUD.
Минусы Active Record
- Связанность с БД.
- Тяжело тестировать без БД.
- Логика + персистентность в одном классе.
- SRP — спорно.
Плюсы Data Mapper
- Разделение. Модель — данные + логика. Mapper — БД.
- Гибкость. Смена БД.
- Тестируемость. Модель без БД.
- DDD — идеально.
Минусы Data Mapper
- Сложнее. Больше кода.
- Много boilerplate.
- Учить дольше.
Когда что
Active Record (Eloquent):
- Laravel-проекты.
- CRUD-приложения.
- Средняя сложность.
- Быстрый старт.
Data Mapper (Doctrine):
- Symfony-проекты.
- DDD / сложная предметная область.
- Много бизнес-логики.
- Смена хранилищ.
Как Eloquent реализован
Illuminate\Database\Eloquent\Model — базовый класс.
Внутри:
__get,__set— магические свойства.__call,__callStatic— методы запроса.- Builder — fluent-интерфейс.
- Relations — связи.
Пример:
User::where('active', true)->orderBy('name')->get();
where — через __callStatic → Builder → SQL.
Магия Eloquent
Свойства берутся из БД:
$user = User::find(1);
echo $user->name; // SELECT * FROM users WHERE id=1, затем ->attributes['name']
__get('name') → $this->attributes['name'].
Скрытые запросы
⚠️ N+1:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // SELECT * FROM users WHERE id=... для каждого поста
}
Решение: with():
$posts = Post::with('user')->get();
Аксессоры и мутаторы
Старый стиль:
public function getNameAttribute(): string
{
return ucfirst($this->attributes['name']);
}
public function setNameAttribute(string $value): void
{
$this->attributes['name'] = strtolower($value);
}
Новый стиль (Laravel 9+):
protected function name(): Attribute
{
return Attribute::make(
get: fn(string $value) => ucfirst($value),
set: fn(string $value) => strtolower($value),
);
}
Casts
Типы:
protected $casts = [
'is_active' => 'boolean',
'created_at' => 'datetime',
'meta' => 'array',
'status' => OrderStatus::class,
];
Что даёт: типизация, автоматические преобразования.
Events модели
Hooks:
protected static function booted(): void
{
static::creating(function (User $user) {
$user->email = strtolower($user->email);
});
static::deleted(function (User $user) {
$user->comments()->delete();
});
}
Observer:
class UserObserver
{
public function created(User $user): void
{
Mail::to($user)->send(new Welcome());
}
}
User::observe(UserObserver::class);
Scopes
Переиспользуемые условия:
public function scopeActive(Builder $query): Builder
{
return $query->where('active', true);
}
User::active()->get();
Параметризованные:
public function scopeRole(Builder $query, string $role): Builder
{
return $query->where('role', $role);
}
User::role('admin')->get();
Связи
Разные типы:
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
Repository поверх Eloquent
Дополнительный слой:
interface UserRepository
{
public function find(int $id): ?User;
public function save(User $user): void;
}
final class EloquentUserRepository implements UserRepository
{
public function find(int $id): ?User
{
return User::with('posts')->find($id);
}
public function save(User $user): void
{
$user->save();
}
}
Плюс: абстракция. Минус: дублирование.
Когда НЕ нужен Repository
⚠️ В Laravel часто без Repository:
- Eloquent уже даёт абстракцию.
- CRUD простой.
- YAGNI.
Repository — для сложной логики и тестов.
Практика: Eloquent разбор
<?php
// Модель
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
protected $fillable = ['name', 'email', 'is_active'];
protected $casts = [
'is_active' => 'boolean',
'created_at' => 'datetime',
];
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
protected function name(): Attribute
{
return Attribute::make(
get: fn(string $value) => ucfirst($value),
set: fn(string $value) => trim($value),
);
}
protected static function booted(): void
{
static::creating(function (User $user) {
$user->email = strtolower($user->email);
});
}
}
Использование:
$user = User::active()->find(1);
$user->name = 'иван'; // сохранится как 'иван'
echo $user->name; // 'Иван' (аксессор)
$user->is_active; // true/false
$user->created_at; // Carbon instance
$posts = $user->posts; // SELECT * FROM posts WHERE user_id=1
Итог
- Active Record — модель знает о БД.
- Data Mapper — модель не знает.
- Eloquent — Active Record.
- Магия — через
__get,__call. - N+1 — используй
with(). - Аксессоры/мутаторы — преобразования.
- Casts — типы.
- Events — hooks.
- Scopes — переиспользуемые условия.
- Repository — не всегда нужен.
Дальше — Eloquent: связи через ООП.