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
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
$paginationInfoprin referință. Dacă apelezipaginate()fără al treilea argument și ceri o pagină mai mare decâttotal_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ă:
HasTimestampsnu actualizeazăupdated_atHasRecomputenu ruleazărecompute()HasValidationnu 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 prinupdate(). 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();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) | bool | S-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) | mixed | Valoarea 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.
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.
whereUnsafe și setUnsafe pot cauza SQL injection dacă expresia raw vine din inputul utilizatorului. Folosește-le doar cu expresii hardcodate.