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]
public string $initials;
public function computeInitials(): string
{
return strtoupper(substr($this->name, 0, 1));
}
}
// ❌ 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
{
#[Enum(UserStatus::class)]
#[DefaultValue(UserStatus::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;
#[Decimal(5, 2)]
public Number $tax_rate;
}
// Operații exacte
$contract->payable_amount = $contract->total_amount - $contract->discount_amount;Eager loading — evită N+1
Ori de câte ori iterezi peste o colecție și accesezi relații, folosește with():
// ❌ N+1 interogări: 1 pentru listă + N pentru fiecare client
$invoices = Invoice::where('status', 'paid')->get();
foreach ($invoices as $inv) {
echo $inv->client->name;
}
// ✅ 2 interogări: SELECT invoices + SELECT clients WHERE id IN (...)
$invoices = Invoice::where('status', 'paid')
->with(['client', 'lines'])
->get();Reguli practice:
- Orice dată accesezi o relație într-un
foreach, cheamăwith()înainte deget(). - Orice
#[Computable]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:
db('company')->transaction(function () {
$order = new Order();
$order->customer_id = 1;
$order->create();
foreach ($items as $item) {
$oi = new OrderItem();
$oi->order_id = $order->id;
$oi->product_id = $item['product_id'];
$oi->quantity = $item['quantity'];
$oi->create();
}
});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
use Leto\Database\ORM\Enums\Order;
// ✅ Clar
User::orderBy('name', Order::ASC)->get();
// Evită string-uri hardcodate
User::orderBy('name', 'asc')->get(); // Funcționează dar nu e recomandat