Eloquent реалізує патерн Active Record: модель одночасно і представляє рядок таблиці, і вміє себе зберігати. Звідси спокуса класти в модель усе - і з часом вона перетворюється на «божественний клас» на тисячі рядків.
Що природно тримати в моделі:
- опис даних:
casts()(дати, енуми, JSON, об'єкти-значення),$fillable,$hidden; - зв'язки (
hasMany,belongsTo); - локальні scopes - повторювані умови запитів:
scopePublished,scopeForTeam; - аксесори й мутатори - представлення атрибутів (
Attribute::make(...)); - прості доменні методи, що стосуються лише цієї моделі й її стану:
$order->isPaid(),$post->publish(),$subscription->isActive().
Що краще винести:
- бізнес-операції, що зачіпають кілька моделей і зовнішні системи (оформлення замовлення, оплата, повернення) - в action-класи чи сервіси;
- складні запити й звіти - у query-класи чи окремі класи звітів;
- виклики сторонніх API, відправка листів - у сервіси, слухачі, джоби;
- логіка, залежна від HTTP (поточний користувач, запит) - моделі не повинні знати про
request()чиauth(); - форматування для API - в API Resources.
Пастки:
- події й спостерігачі моделей з бізнес-логікою: «при збереженні замовлення списати товар зі складу» - спрацює і в сидері, і при імпорті, і в тестах, і не спрацює при масовому
update(); - глобальні scopes, що неявно змінюють усі запити (наприклад, мультиорендність) - потужно, але легко забути про них і отримати несподіваний результат;
- ледаче завантаження у методах моделі (
$this->items->sum(...)у циклі) - N+1; - моделі як DTO між шарами - зміни в таблиці миттєво стають змінами в усіх шарах.
Корисні інструменти для «схуднення» моделей: власні колекції (newCollection, атрибут #[CollectedBy]), власні будівники запитів (#[UseEloquentBuilder]) для складних scopes, касти для об'єктів-значень, трейти для поведінки, спільної кільком моделям.
Баланс: Active Record - свідомий вибір Laravel заради простоти. Боротися з ним, будуючи повний шар репозиторіїв і доменних сутностей поверх Eloquent, зазвичай дорожче, ніж тримати моделі охайними за правилами вище.