Salt la conținut

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 de get().
  • Orice #[Computable] folosit într-o listă — eager cu with().
  • 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 cu SELECT ț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