Традиційно модель налаштовують захищеними властивостями й методами booted(). Laravel поступово додає PHP-атрибути класу, що описують ту саму конфігурацію декларативно:
use Illuminate\Database\Eloquent\Attributes\{Table, Fillable, Hidden, ObservedBy, ScopedBy, UseFactory, CollectedBy};
#[Table('legacy_orders', key: 'order_id', timestamps: false)]
#[Fillable(['customer_id', 'total', 'status'])]
#[Hidden(['internal_note'])]
#[ObservedBy(OrderObserver::class)]
#[ScopedBy(TenantScope::class)]
#[UseFactory(OrderFactory::class)]
#[CollectedBy(OrderCollection::class)]
final class Order extends Model {}
Те саме через властивості й методи:
class Order extends Model
{
protected $table = 'legacy_orders';
protected $primaryKey = 'order_id';
public $timestamps = false;
protected $fillable = ['customer_id', 'total', 'status'];
protected $hidden = ['internal_note'];
protected static function booted(): void
{
static::observe(OrderObserver::class);
static::addGlobalScope(new TenantScope);
}
}
Що дають атрибути:
- конфігурація відокремлена від поведінки: метадані над класом, а в тілі лишаються зв'язки, касти, бізнес-методи;
- видно одразу, які спостерігачі й глобальні області видимості підключені до моделі - не треба шукати по
booted()і сервіс-провайдерах; - немає побічних ефектів у
booted()і ризику забутиparent::booted()у спадкоємцях; - статичний аналіз: IDE й інструменти читають атрибути через Reflection без запуску коду.
Що варто знати:
- атрибути читаються через Reflection при першому зверненні й кешуються - на швидкість це практично не впливає;
- успадкування: сам PHP атрибути не наслідує, але Eloquent шукає атрибут у класі моделі, її трейтах і далі в батьківських класах - тож конфігурація з базової моделі чи трейту діє в спадкоємцях, а найближчий атрибут перекриває дальші;
- змішування стилів в одному проєкті (частина моделей з властивостями, частина з атрибутами) ускладнює читання - варто домовитися про один стиль;
- не все має атрибут: касти, зв'язки, аксесори лишаються методами.
Інші атрибути Laravel у тому самому стилі: #[UsePolicy] для зв'язку моделі з політикою, #[UseResource] для API Resource, #[Scope] для позначення методу локальною областю видимості без префікса scope.
Практично: для нових моделей атрибути зручніші; переписувати старі заради стилю немає сенсу - поведінка однакова.