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():
HasRecompute— prioritate5(recalculează valori derivate) 2/3.HasTimestampsșiHasValidation— prioritate10ambele; 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șiupdated_atdeclarate îndescribe() - Tipul acestor coloane trebuie să implementeze
\DateTimeInterface(ex.DateTimeImmutable)
Cum funcționează:
- La
create():updateCreatedAtTimestamp()seteazăcreated_atcu o instanță nouă a tipului declarat - La
create()șiupdate()(single-entity):updateUpdatedAtTimestamp()seteazăupdated_atcu 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
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):
| Ordine | Trait | Hook | Prioritate |
|---|---|---|---|
| 1 | HasRecompute | #[BeforeCreate(5)], #[BeforeUpdate(5)] | 5 |
| 2/3 | HasTimestamps | #[BeforeCreate], #[BeforeUpdate] | 10 (implicit) |
| 2/3 | HasValidation | #[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);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), decirecompute()se execută înaintea luivalidate() - Flag-ul static
$__enableValidationcontrolează dacă validarea e activă (implicittrue)
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(): string | Numele coloanei care ține poziția (ex. 'order_index') |
abstract protected static getSiblingColumns(): array | Coloanele care definesc grupul de frați (ex. ['parent_id']) |
Metode disponibile:
| Metodă | Descriere |
|---|---|
static getNextIndex(mixed $siblingsIdentifier): int | Returnează următorul index disponibil în grup |
setOrder(int $newOrder): void | Mută entitatea la o poziție și recalculează pozițiile fraților |
moveUp(): void | Mută entitatea cu o poziție mai sus |
moveDown(): void | Mută 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);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
- Hook-uri de salvare — sistemul de priorități și limitări
- Operațiuni CRUD — urmărirea modificărilor (Dirty Tracking)
- Access Control (ACL) — cum interacționează ACL-ul cu hook-urile