Salt la conținut

Hook-uri de salvare

Hook-uri de salvare (Lifecycle Hooks)

ORM-ul Leto oferă hook-uri de ciclu de viață prin trait-ul HookCapability, deja inclus în clasa Model. Nu este necesar (și nici recomandat) să adaugi manual use HookCapability pe modelele tale — hook-urile funcționează direct, fără nicio configurare suplimentară. Hook-urile se definesc prin atribute PHP 8 pe metode — nu prin nume magice de metode.

Hook-uri disponibile

AtributCând se execută
#[BeforeCreate]Înainte de INSERT
#[AfterCreate]După INSERT (ID-ul e deja populat)
#[BeforeUpdate]Înainte de UPDATE (doar mod single-entity)
#[AfterUpdate]După UPDATE (doar mod single-entity)
#[BeforeDelete]Înainte de DELETE (doar mod single-entity)
#[AfterDelete]După DELETE (doar mod single-entity)

Fiecare atribut acceptă un parametru opțional priority (int, implicit 10). Hook-urile cu prioritate mai mică se execută primele.

use Leto\Database\ORM\Functionality\Hooks\Attributes\BeforeCreate;
use Leto\Database\ORM\Functionality\Hooks\Attributes\AfterCreate;
use Leto\Database\ORM\Functionality\Hooks\Attributes\BeforeUpdate;
use Leto\Database\ORM\Functionality\Hooks\Attributes\AfterUpdate;
use Leto\Database\ORM\Functionality\Hooks\Attributes\BeforeDelete;
use Leto\Database\ORM\Functionality\Hooks\Attributes\AfterDelete;
Operațiunile bulk sar peste hook-uri (și peste ACL). Hook-urile de update și delete se execută doar în modul single-entity, adică atunci când modifici o instanță de model. Operațiunile multi-entitate (User::where(...)->update(), User::where(...)->delete()) nu declanșează niciun hook și, în plus, nu trec prin verificările de Access Control. Dacă ai nevoie de logica din hook-uri sau de ACL, iterează instanțele și salvează-le individual.
Nu scrie hook-uri manuale pentru lucruri pe care le fac deja trait-urile funcționale. Dacă ai nevoie de created_at / updated_at, folosește HasTimestamps; pentru recalcularea valorilor derivate folosește HasRecompute; pentru validare la salvare, HasValidation. Hook-urile sunt pentru logica proprie a aplicației tale care nu e acoperită de un trait (generare de slug-uri, notificări, reguli de business).

Utilizare

Aplici atributele pe metode cu orice nume și orice vizibilitate (public, protected sau private). Singura restricție: hook-ul trebuie să fie o metodă de instanță, non-statică.

Un hook aplicat pe o metodă static este respins la runtime cu o excepție (A hook may not be applied onto static method ...). Hook-urile lucrează cu $this, deci au nevoie de o instanță.
use Leto\Database\ORM\Attributes\Table;
use Leto\Database\ORM\Attributes\PrimaryKey;
use Leto\Database\ORM\Attributes\AutoIncrement;
use Leto\Database\ORM\Attributes\DataStorage\Varchar;
use Leto\Database\ORM\Functionality\Hooks\Attributes\BeforeCreate;
use Leto\Database\ORM\Functionality\Hooks\Attributes\AfterCreate;
use Leto\Database\ORM\Functionality\Hooks\Attributes\BeforeDelete;
use Leto\Database\ORM\Model;

#[Table('articles')]
class Article extends Model
{
    #[PrimaryKey, AutoIncrement]
    public int $id;

    #[Varchar(150)]
    public string $title;

    #[Varchar(150)]
    public string $slug;

    #[Varchar(100)]
    public string $author_email;

    #[Varchar(20)]
    public string $status;

    #[BeforeCreate(priority: 5)]
    protected function generateSlug(): void
    {
        // Logică proprie: derivă slug-ul din titlu înainte de INSERT
        $this->slug = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $this->title), '-'));
    }

    #[BeforeCreate]
    protected function normalizeEmail(): void
    {
        // Prioritate implicită 10, deci rulează după generateSlug
        $this->author_email = strtolower(trim($this->author_email));
    }

    #[AfterCreate]
    protected function notifyEditors(): void
    {
        // ID-ul e deja populat aici
        NotificationService::announceNewArticle($this);
    }

    #[BeforeDelete(priority: 1)]
    protected function preventDeletePublished(): void
    {
        if ($this->status === 'published') {
            throw new \Exception('Nu poți șterge un articol publicat.');
        }
    }
}

Prioritatea

Hook-urile sunt sortate după priority (crescător), în cadrul aceluiași tip de hook (ex. toate hook-urile #[BeforeCreate] formează o coadă separată de cele #[BeforeUpdate]). Valoarea implicită este 10.

În exemplul de mai sus, hook-urile #[BeforeCreate] se execută în ordinea:

  1. generateSlug (priority: 5) — se execută primul
  2. normalizeEmail și alte hook-uri #[BeforeCreate] cu prioritatea implicită 10 — după

Ordinea de execuție

create():
  #[BeforeCreate] → INSERT → #[AfterCreate]

update() (single-entity):
  #[BeforeUpdate] → UPDATE → #[AfterUpdate]

delete() (single-entity):
  #[BeforeDelete] → DELETE → #[AfterDelete]

Limitări

  • Hook-urile nu rulează în operațiunile bulk (vezi avertismentul de mai sus).
  • Nu există un bloc try/finally între hook-ul before* și after*. Dacă apare o excepție între ele (de exemplu la $query->execute()), hook-ul after* nu se mai execută, deci trebuie să gestionezi această situație în codul tău.