Salt la conținut

Operațiuni CRUD

După ce ai definit un model, îl folosești pentru a citi și a scrie date. Această pagină acoperă operațiunile de bază (find, where, get, paginate, create, update, delete), tranzacțiile și urmărirea modificărilor (dirty tracking).

Operațiuni CRUD pe modele

Dacă modelul definește reguli ACL, toate operațiunile de mai jos le respectă automat: citirile (find, get, first) verifică permisiunea de citire și filtrează câmpurile fără acces, iar create, update și delete verifică permisiunile corespunzătoare. Operațiunile bulk sar peste ACL și peste hook-uri. Detalii în Access Control.

find()

Găsește o înregistrare după cheia primară:

$user = User::find(1);                  // Returnează User sau null

Pentru chei primare compuse, folosește forma array (exemplu real din proiectul Triatlon):

$score = Score::find([
    'student_id' => $student->id,
    'event_id'   => $event->id
]);

where() & friends

where() este o metodă statică care pornește o interogare nouă. Metodele înlănțuite (andWhere, orWhere, andWhereGroup, orWhereGroup) continuă interogarea curentă.

use Leto\Database\ORM\Enums\Order;

// Condiții simple
$users = User::where('active', 1)->get();

// Multiple condiții (AND)
$users = User::where('active', 1)
    ->andWhere('role_id', 2)
    ->get();

// OR
$users = User::where('role', 'admin')
    ->orWhere('role', 'moderator')
    ->get();

// Grupare cu paranteze (în interiorul unei interogări existente)
$users = User::where('active', 1)
    ->andWhereGroup(function ($q) {
        $q->where('role', 'admin')
          ->orWhere('permissions', 'LIKE', '%manage%');
    })
    ->get();

all(), get(), first(), count()

// Toate înregistrările
$users = User::all()->get();

// Colecție filtrată
$users = User::where('active', 1)->get();

// Prima înregistrare (sau null dacă nu există)
$first = User::where('email', $email)->first();

// Numără
$count = User::where('active', 1)->count();

orderBy, limit, offset

use Leto\Database\ORM\Enums\Order;

User::where('active', 1)
    ->orderBy('name', Order::ASC)
    ->orderBy('created_at', Order::DESC)
    ->limit(20)
    ->offset(0)
    ->get();

Poți folosi și orderBy('name') fără al doilea parametru — implicit e Order::ASC.

paginate()

Semnătură: paginate(int $page, int $resultsPerPage, ?array &$paginationInfo = null)

// Pagina 1, câte 20 pe pagină
$page = User::where('active', 1)
    ->orderBy('name', Order::ASC)
    ->paginate(1, 20);

// Cu informații de paginare
$paginationInfo = null;
$users = User::all()->paginate(1, 20, $paginationInfo);

// $paginationInfo conține:
//   first_result, last_result, total_results,
//   total_pages, current_page, results_per_page

Important: Protecția împotriva paginii prea mari (page-overflow guard) funcționează doar când transmiți parametrul $paginationInfo prin referință. Dacă apelezi paginate() fără al treilea argument și ceri o pagină mai mare decât total_pages, metoda returnează un array gol fără a clamp-a automat pagina.

create()

create() este o metodă de instanță, nu statică. Creezi un obiect, îi populezi proprietățile, apoi apelezi create():

$user = new User();
$user->name = 'Maria Popescu';
$user->email = 'maria@example.com';
$user->role_id = 2;
$user->active = true;
$user->create();

Metoda create() populează automat câmpul auto-increment după salvare și face un refresh(false) pentru a reîncărca rândul complet din baza de date — astfel captează și valorile DEFAULT generate de serverul SQL (de exemplu, current_timestamp() definit prin #[DefaultExpression]).

update()

Actualizare individuală (prin modificarea instanței):

$user = User::find(1);
$user->name = 'Nume nou';
$user->update();

Actualizare bulk (mai multe înregistrări deodată):

User::where('active', 0)
    ->set('archived', true)
    ->update();

Atenție! În modul bulk, hook-urile de ciclu de viață (#[BeforeUpdate], #[AfterUpdate], etc.) NU se execută. Asta înseamnă că:

  • HasTimestamps nu actualizează updated_at
  • HasRecompute nu rulează recompute()
  • HasValidation nu rulează validate()
  • Hook-urile tale custom sunt ignorate

Pentru actualizări cu hook-uri, folosește modul single-entity:

$users = User::where('active', 0)->get();
foreach ($users as $user) {
    $user->archived = true;
    $user->update();  // ← hook-urile rulează
}

Protecție PK auto-increment: Coloanele marcate cu #[AutoIncrement] nu pot fi modificate prin update(). Orice tentativă de a schimba valoarea unui câmp auto-increment aruncă o excepție \RuntimeException.

delete()

Ștergere individuală:

$user = User::find(1);
$user->delete();

Ștergere bulk:

User::where('created_at', '<', '2020-01-01')->delete();
La fel ca la update(), hook-urile (#[BeforeDelete], #[AfterDelete]) nu se execută în modul bulk. Folosește ștergerea individuală dacă ai nevoie de hook-uri.

refresh()

Reîncarcă modelul din baza de date, anulând modificările locale:

$user = User::find(1);
$user->name = 'Modificare temporară';
$user->refresh();  // $user->name revine la valoarea din DB

Dirty Tracking

Orice model are acces la metodele de dirty tracking — poți verifica ce câmpuri s-au modificat de la ultima încărcare din baza de date:

$user = User::find(1);
$user->name = 'Nume nou';

$user->isDirty();           // true — s-a modificat ceva
$user->isDirty('name');     // true — câmpul 'name' s-a modificat
$user->isDirty('email');    // false — 'email' e neschimbat

$user->getDirty();          // ['name' => 'Nume nou']
$user->getOriginal('name'); // 'Nume vechi' (valoarea de la încărcare)

Metodele disponibile:

MetodăReturneazăDescriere
isDirty(?string $field = null)boolS-a modificat vreun câmp? Sau câmpul specificat?
getDirty()array<string, mixed>Array cu [câmp => valoare_nouă] doar pentru câmpurile modificate
getOriginal(?string $field = null)mixedValoarea originală (de la încărcarea din DB). null = toate

Comparația se face intern pe valori serializate (forma stocată în DB), deci detectează corect mutațiile in-place pe DateTime și alte obiecte.

create() apelează automat refresh(false) după INSERT pentru a reîncărca rândul complet (inclusiv valorile DEFAULT generate de serverul SQL), apoi capturează starea originală. Astfel, după create(), isDirty() returnează întotdeauna false.

După update(), starea originală este resincronizată automat — isDirty() revine la false.

Atenție! Dacă apelezi isDirty() sau getOriginal() cu un câmp care nu există în $__original (de exemplu, pe un model creat cu new dar niciodată încărcat din DB), metodele aruncă \ValueError.

Tranzacții

db()->withTransaction(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();
    }
    // Dacă orice operațiune eșuează, totul se anulează
});

whereUnsafe / setUnsafe

Pentru cazurile rare când ai nevoie de SQL raw neescapuit. Ambele metode funcționează doar în modul bulk (interogare fluentă), nu pe o instanță de model.

// whereUnsafe pornește o interogare raw
User::whereUnsafe('YEAR(created_at)', 2025)->get();

// setUnsafe — un singur argument, expresia completă
// Folosește modul bulk, NU pe instanță!
User::where('id', 1)
    ->setUnsafe('login_count = login_count + 1')
    ->update();

La whereUnsafe, doar expresia din stânga (numele coloanei sau funcția SQL) este inserată raw în interogare; valoarea de comparație rămâne legată ca parametru. Riscul vine, așadar, din expresia din stânga, nu din valoare.

Atenție! whereUnsafe și setUnsafe pot cauza SQL injection dacă expresia raw vine din inputul utilizatorului. Folosește-le doar cu expresii hardcodate.