Тема
Eloquent Models
Расположение
app/Models/{ModelName}.php — одна модель на файл, одна модель на таблицу.
Именование
| Сущность | Формат | Пример |
|---|---|---|
| Модель | Singular PascalCase | ServiceCategory |
| Таблица | Plural snake_case | service_categories |
| FK столбец | {model_singular}_id | category_id |
| Pivot таблица | Алфавитный порядок, singular | brand_category |
| Зарезервированные слова PHP | Альтернативное имя | CaseStudy вместо Case |
Casts — обязательны
Каждый столбец с типом, отличным от string/int, ДОЛЖЕН быть в $casts:
| Тип столбца | Cast |
|---|---|
| json / jsonb | 'array' или cast class |
| boolean / tinyint(1) | 'boolean' |
| datetime / timestamp | 'datetime' |
| date | 'date' |
| decimal | 'decimal:N' |
| enum в миграции | PHP Enum class |
Без cast JSON-столбец вернёт строку вместо массива — частый источник багов.
Relations
- Всегда определять обе стороны: parent
hasMany↔ childbelongsTo - Many-to-many:
belongsToManyс явным именем pivot-таблицы - Self-referential:
parent_id→belongsTo('parent')+hasMany('children') - Polymorphic — только когда реально нужен полиморфизм, не "на будущее"
Eager Loading
- НИКОГДА не использовать
protected $withв модели — загрузит relation при каждом запросе - Всегда
->with([...])явно в месте вызова (контроллер/сервис) - Если relation используется в цикле или Blade
@foreach— это N+1, нуженwith() - Для подсчёта без загрузки данных —
withCount()
Scopes
Создавать scope когда одно и то же условие WHERE используется в 2+ местах:
scopeActive—where('is_active', true)scopeOrdered—orderBy('sort_order')scopeForCity—where('city_id', $id)(если модель привязана к городу)scopePublished—where('status', 'published')->where('published_at', '<=', now())
Не создавать scope для одноразового условия.
Accessors / Mutators
- Использовать
Attributecast (Laravel 11+), не старыйgetXxxAttribute() - Accessor НЕ должен делать запросы к БД или вызывать внешние сервисы
- Mutator — для нормализации данных при записи (телефон, slug, trim)
fillable vs guarded
$fillable— явный whitelist, предпочтительный вариант$guarded = []— допустимо только в моделях без пользовательского ввода (данные из seeders, internal jobs)
Soft Deletes
Только когда есть бизнес-требование восстановления удалённых записей. Не использовать "на всякий случай" — добавляет скрытую сложность во все запросы.
Traits
Создавать trait когда одинаковое поведение нужно в 3+ моделях. Один метод в одной модели — не trait, а метод модели.
Типичные проектные traits:
HasDeclensions— склонения (если проект русскоязычный с падежами)BelongsToCity— мультигород scope + FKSortable— sort_order логика
Anti-patterns
| Что | Почему плохо | Как правильно |
|---|---|---|
$with в модели | Загружает relation ВСЕГДА | ->with() в запросе |
| Логика в accessor с запросами | Скрытые N+1, невозможно профилировать | Вынести в Service |
Model::all() без условий | Загружает ВСЮ таблицу в память | Условия, limit(), paginate() |
| Raw SQL с конкатенацией переменных | SQL injection | Eloquent / bindings |
request() или auth() в модели | Модель не должна знать про HTTP-контекст | Передавать данные явно |
| Бизнес-логика в модели (расчёты, API вызовы) | God Object, невозможно тестировать изолированно | Вынести в Service |
| Trait для одной модели | Лишняя абстракция, усложняет навигацию | Метод в модели |