Best practices
Păstrează modelele slabe
Un model e un DTO + metode simple. Logica grea de business (calcule multi-model, orchestrări, integrări externe) trăiește în servicii / use-case-uri, nu în model. Excepție: metode de domeniu care operează doar pe datele modelului curent (ex. Invoice::computePayable() care folosește $this->lines).
// ✅ BINE: Model curat
#[Table('users')]
class User extends Model
{
#[PrimaryKey, AutoIncrement]
public int $id;
#[Varchar(100)]
public string $name;
#[Computable('initials', 'string')]
public static function computeInitials(array $models): array
{
return array_map(fn($m) => strtoupper(substr($m->name, 0, 1)), $models);
}
}
// ❌ RĂU: Logică de business în model
class User extends Model
{
public function sendWelcomeEmail() { /* ... */ }
public function createInvoice() { /* ... */ }
}Declară @property în PHPDoc
Relațiile și atributele computable sunt proprietăți virtuale — nu sunt declarate explicit în clasă. Pentru autocomplete în IDE, declară-le în PHPDoc:
/**
* @property ParentModel $parent // relație BelongsTo
* @property Student[] $students // relație HasMany
* @property string $full_name // computable
* @property int $warning_count // computable
*/
#[Table('school_students')]
class Student extends Model
{
#[PrimaryKey, AutoIncrement]
public int $id;
#[BelongsTo('parent', ParentModel::class, 'id')]
public int $parent_id;
public ?string $name = null;
// ...
}Tipuri nullable explicit
Dacă o coloană din BD poate fi NULL, declară tipul ca ?type cu = null:
#[Varchar(50)]
public ?string $middle_name = null;Dacă e non-null în BD, lasă proprietatea fără null și neinițializată (fără = null) — ORM-ul verifică tipurile la serializare și va semnala dacă lipsește o valoare obligatorie:
#[Varchar(100)]
public string $name; // fără = null, fără valoare implicită
BackedEnum pentru statusuri
enum UserStatus: string
{
case Active = 'active';
case Inactive = 'inactive';
case Banned = 'banned';
}
class User extends Model
{
// BackedEnum-urile sunt auto-detectate — ORM-ul creează un ENUM din cazuri
#[DefaultValue('active')]
public UserStatus $status;
}DateTimeImmutable pentru date
class Event extends Model
{
#[DateTime]
public \DateTimeImmutable $starts_at;
#[Date]
public \DateTimeImmutable $event_date;
}BcMath\Number pentru valori monetare
Nu folosi float pentru prețuri sau sume — folosește BcMath\Number cu #[Decimal]. Aritmetica e exactă, neafectată de precizia IEEE 754:
use BcMath\Number;
class Invoice extends Model
{
#[Decimal(10, 2)]
public Number $total_amount;
#[Decimal(10, 2)]
public Number $discount_amount;
#[Decimal(10, 2)]
public Number $payable_amount;
}
// Operații exacte (BcMath\Number suportă operatorii aritmetici în PHP 8.4)
$invoice->payable_amount = $invoice->total_amount - $invoice->discount_amount;Eager loading — evită N+1
Ori de câte ori iterezi peste o colecție și accesezi relații sau atribute computable, folosește with() ca să eviți problema N+1. Explicația completă și exemplul înainte/după sunt în secțiunea de eager loading din Relații.
Reguli practice:
- Orice dată accesezi o relație într-un
foreach, cheamăwith()înainte deget(). - Orice
#[Computable]care face interogări, folosit într-o listă: eager cuwith(). - Pentru listări mari (mii de rânduri), paginează cu
paginate(). - Dacă performanța e critică și ai nevoie doar de câteva coloane, folosește
db()direct cuSELECTțintit.
Înfășoară operațiile multi-entity în tranzacții
Orice operațiune care atinge mai multe rânduri sau tabele merită înfășurată într-o tranzacție (withTransaction()), ca să rămână atomică. Vezi exemplul complet în Operațiuni CRUD.
Nu atribui proprietăți pe model în controller
// ✅ BINE — instanțiere + salvare
$user = new User();
$user->name = $request->input('name');
$user->email = $request->input('email');
$user->create();orderBy cu enum-ul Order
La nivelul modelului, al doilea argument al lui orderBy() este enum-ul Order (Order::ASC / Order::DESC), nu un string. Detalii și exemplu în secțiunea de sortare din Operațiuni CRUD.