Salt la conținut
Trait-uri funcționale

Trait-uri funcționale

Pe lângă hook-urile de ciclu de viață și query builder, ORM-ul Leto pune la dispoziție un set de trait-uri funcționale gata de utilizat. Adaugă-le pe modelele tale pentru a obține automat timestamp-uri, validare, recomputare și ordonare între entități.

Trei dintre trait-urile funcționale (HasTimestamps, HasRecompute, HasValidation) își atașează logica prin hook-uri cu prioritate (număr mai mic = executat mai întâi). Prioritatea implicită a atributelor de hook este 10, dar unele trait-uri o suprascriu (ex. HasRecompute folosește 5). DirtyTracking și HasOrder nu folosesc hook-uri.

Ordinea de execuție la create() și update():

  1. HasRecompute — prioritate 5 (recalculează valori derivate) 2/3. HasTimestamps și HasValidation — prioritate 10 ambele; ordinea relativă între ele este nedeterministă

Pentru detalii complete despre sistemul de hook-uri și priorități, vezi Hook-uri de salvare.

HasTimestamps

Trait-ul Leto\Database\ORM\Functionality\HasTimestamps setează automat câmpurile created_at și updated_at prin hook-uri #[BeforeCreate] și #[BeforeUpdate].

Cerințe:

  • Modelul trebuie să aibă coloanele created_at și updated_at declarate în describe()
  • Tipul acestor coloane trebuie să implementeze \DateTimeInterface (ex. DateTimeImmutable)

Cum funcționează:

  • La create(): updateCreatedAtTimestamp() setează created_at cu o instanță nouă a tipului declarat
  • La create() și update() (single-entity): updateUpdatedAtTimestamp() setează updated_at cu o instanță nouă
use Leto\Database\ORM\Model;
use Leto\Database\ORM\Attributes\Table;
use Leto\Database\ORM\Attributes\PrimaryKey;
use Leto\Database\ORM\Attributes\AutoIncrement;
use Leto\Database\ORM\Attributes\DataStorage\DateTime;
use Leto\Database\ORM\Functionality\HasTimestamps;

#[Table('articles')]
class Article extends Model
{
    use HasTimestamps;

    #[PrimaryKey, AutoIncrement]
    public int $id;

    #[DateTime]
    public \DateTimeImmutable $created_at;

    #[DateTime]
    public ?\DateTimeImmutable $updated_at = null;

    public string $title;
}

$article = new Article();
$article->title = 'Noutăți';
$article->create();
// $article->created_at și $article->updated_at sunt populate automat
Dacă modelul nu declară coloanele created_at sau updated_at, sau tipul nu e DateTimeInterface, trait-ul aruncă o excepție.

Ordinea de execuție când combini mai multe trait-uri

Când folosești mai multe trait-uri funcționale pe același model, hook-urile se execută în ordinea priorității (număr mai mic = mai devreme):

OrdineTraitHookPrioritate
1HasRecompute#[BeforeCreate(5)], #[BeforeUpdate(5)]5
2/3HasTimestamps#[BeforeCreate], #[BeforeUpdate]10 (implicit)
2/3HasValidation#[BeforeCreate(10)], #[BeforeUpdate(10)]10

HasTimestamps și HasValidation au aceeași prioritate (10) — ordinea relativă între ele este nedeterministă. Dacă ai nevoie de o ordine strictă, ajustează manual prioritățile cu #[BeforeCreate(priority: N)].

HasRecompute

Trait-ul Leto\Database\ORM\Functionality\Integrity\HasRecompute execută automat o metodă recompute() înainte de fiecare create() și update() (single-entity).

Contractul:

  • Trebuie să implementezi metoda abstractă protected function recompute(): void
  • Prin hook-urile #[BeforeCreate(5)] și #[BeforeUpdate(5)], metoda se apelează automat
  • Poți dezactiva temporar recomputarea cu setRecompute(false)
use BcMath\Number;
use Leto\Database\ORM\Model;
use Leto\Database\ORM\Attributes\DataStorage\Decimal;
use Leto\Database\ORM\Functionality\Integrity\HasRecompute;

class Invoice extends Model
{
    use HasRecompute;

    #[Decimal(10, 2)]
    public Number $subtotal;

    #[Decimal(5, 2)]
    public Number $tax_rate;

    #[Decimal(10, 2)]
    public Number $total;

    protected function recompute(): void
    {
        // BcMath\Number face aritmetică exactă, fără erori de virgulă flotantă
        $this->total = $this->subtotal * (new Number('1') + $this->tax_rate);
    }
}

$invoice = new Invoice();
$invoice->subtotal = new Number('100.00');
$invoice->tax_rate = new Number('0.19');
$invoice->create();
// $invoice->total === 119.00 (calculat automat de recompute())

// Dezactivează temporar recomputarea
Invoice::setRecompute(false);
Pentru valori monetare folosește BcMath\Number cu #[Decimal], niciodată float. Vezi Best practices.

Toggle global:

  • setRecompute(bool) este o metodă statică — afectează toate instanțele clasei
  • Implicit, recomputarea este activată (true)

HasValidation

Trait-ul Leto\Database\ORM\Functionality\Integrity\HasValidation execută automat o metodă validate() înainte de fiecare create() și update() (single-entity).

Contractul:

  • Trebuie să implementezi metoda abstractă protected function validate(): void
  • Prin hook-urile #[BeforeCreate(10)] și #[BeforeUpdate(10)], metoda se apelează automat
  • Dacă validarea eșuează, aruncă ValidationFailedException

Comportament:

  • Validarea rulează după HasRecompute (prioritate 10 vs. 5), deci recompute() se execută înaintea lui validate()
  • Flag-ul static $__enableValidation controlează dacă validarea e activă (implicit true)
Spre deosebire de HasRecompute care expune metoda publică setRecompute(bool), HasValidation nu are o metodă publică pentru a dezactiva validarea. Flag-ul $__enableValidation este privat — nu poate fi modificat din exterior. Dacă ai nevoie să sari peste validare în anumite scenarii, creează o subclasă care suprascrie metoda validate() sau nu folosi trait-ul HasValidation și implementează manual logica de validare.
use Leto\Database\ORM\Model;
use Leto\Database\ORM\Functionality\Integrity\HasValidation;
use Leto\Data\Validation\Exceptions\ValidationFailedException;

class User extends Model
{
    use HasValidation;

    public string $email;
    public string $name;

    protected function validate(): void
    {
        if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
            throw new ValidationFailedException(['email' => ['Adresa de email nu este validă.']]);
        }

        if (strlen($this->name) < 3) {
            throw new ValidationFailedException(['name' => ['Numele trebuie să aibă minim 3 caractere.']]);
        }
    }
}

HasOrder

Trait-ul Leto\Database\ORM\Functionality\HasOrder oferă metode pentru gestionarea ordinii entităților într-un grup de “frați” (siblings). Este util pentru entități care au o poziție ordonabilă: slide-uri într-o prezentare, pași într-un workflow, secțiuni într-un document.

Contractul — trebuie să implementezi două metode abstracte:

MetodăDescriere
abstract protected static getOrderColumn(): stringNumele coloanei care ține poziția (ex. 'order_index')
abstract protected static getSiblingColumns(): arrayColoanele care definesc grupul de frați (ex. ['parent_id'])

Metode disponibile:

MetodăDescriere
static getNextIndex(mixed $siblingsIdentifier): intReturnează următorul index disponibil în grup
setOrder(int $newOrder): voidMută entitatea la o poziție și recalculează pozițiile fraților
moveUp(): voidMută entitatea cu o poziție mai sus
moveDown(): voidMută entitatea cu o poziție mai jos
use Leto\Database\ORM\Model;
use Leto\Database\ORM\Attributes\Table;
use Leto\Database\ORM\Attributes\PrimaryKey;
use Leto\Database\ORM\Attributes\AutoIncrement;
use Leto\Database\ORM\Functionality\HasOrder;

#[Table('slides')]
class Slide extends Model
{
    use HasOrder;

    #[PrimaryKey, AutoIncrement]
    public int $id;

    public int $presentation_id;
    public int $order_index;
    public string $content;

    protected static function getOrderColumn(): string
    {
        return 'order_index';
    }

    protected static function getSiblingColumns(): array
    {
        return ['presentation_id'];
    }
}

// Adaugă un slide nou la final
$slide = new Slide();
$slide->presentation_id = 5;
$slide->content = 'Introducere';
$slide->order_index = Slide::getNextIndex(5);  // 1 dacă e primul
$slide->create();

// Mută un slide mai sus
$slide->moveUp();

// Mută un slide mai jos
$slide->moveDown();

// Setează manual poziția
$slide->setOrder(3);
Important: HasOrder nu setează automat valoarea de ordine la create(). Trebuie să atribui manual order_index folosind getNextIndex() înainte de a apela create(). Dacă nu o faci, entitatea va fi salvată cu valoarea default (de obicei 0), ceea ce poate strica ordinea fraților.

HasOrder folosește cheile primare pentru a identifica entitatea curentă în lista de frați. Dacă modelul are chei primare compuse, toate sunt verificate.

DirtyTracking

Trait-ul DirtyTracking este de asemenea inclus automat în Model și îți spune ce câmpuri s-au schimbat de la ultima sincronizare (isDirty(), getDirty(), getOriginal()). Fiind strâns legat de fluxul de salvare, este documentat complet în secțiunea Dirty Tracking din Operațiuni CRUD.

Vezi și