Skip to content

Eloquent Models

Расположение

app/Models/{ModelName}.php — одна модель на файл, одна модель на таблицу.

Именование

СущностьФорматПример
МодельSingular PascalCaseServiceCategory
ТаблицаPlural snake_caseservice_categories
FK столбец{model_singular}_idcategory_id
Pivot таблицаАлфавитный порядок, singularbrand_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 ↔ child belongsTo
  • Many-to-many: belongsToMany с явным именем pivot-таблицы
  • Self-referential: parent_idbelongsTo('parent') + hasMany('children')
  • Polymorphic — только когда реально нужен полиморфизм, не "на будущее"

Eager Loading

  • НИКОГДА не использовать protected $with в модели — загрузит relation при каждом запросе
  • Всегда ->with([...]) явно в месте вызова (контроллер/сервис)
  • Если relation используется в цикле или Blade @foreach — это N+1, нужен with()
  • Для подсчёта без загрузки данных — withCount()

Scopes

Создавать scope когда одно и то же условие WHERE используется в 2+ местах:

  • scopeActivewhere('is_active', true)
  • scopeOrderedorderBy('sort_order')
  • scopeForCitywhere('city_id', $id) (если модель привязана к городу)
  • scopePublishedwhere('status', 'published')->where('published_at', '<=', now())

Не создавать scope для одноразового условия.

Accessors / Mutators

  • Использовать Attribute cast (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 + FK
  • Sortable — sort_order логика

Anti-patterns

ЧтоПочему плохоКак правильно
$with в моделиЗагружает relation ВСЕГДА->with() в запросе
Логика в accessor с запросамиСкрытые N+1, невозможно профилироватьВынести в Service
Model::all() без условийЗагружает ВСЮ таблицу в памятьУсловия, limit(), paginate()
Raw SQL с конкатенацией переменныхSQL injectionEloquent / bindings
request() или auth() в моделиМодель не должна знать про HTTP-контекстПередавать данные явно
Бизнес-логика в модели (расчёты, API вызовы)God Object, невозможно тестировать изолированноВынести в Service
Trait для одной моделиЛишняя абстракция, усложняет навигациюМетод в модели

Внутренняя база знаний студии