Exemple din practică
Această pagină colectează pattern-uri reale din proiectele Workleto (School, Jobs, Triatlon) care ilustrează utilizări avansate și “gotcha”-uri ale ORM-ului.
Chei primare compuse
Când un model are mai multe coloane ca PK, find() primește un array asociativ:
// Modelul Score din proiectul Triatlon
#[Table('score')]
class Score extends Model
{
#[PrimaryKey]
#[BelongsTo('student', Student::class, 'id')]
public int $student_id;
#[PrimaryKey]
#[BelongsTo('event', Event::class, 'id')]
public int $event_id;
public float $result;
public float $points;
}
// Găsire după cheie compusă
$score = Score::find([
'student_id' => $student->id,
'event_id' => $event->id
]);
if ($score) {
$score->result = $noulRezultat;
$score->update();
} else {
$score = new Score();
$score->student_id = $student->id;
$score->event_id = $event->id;
$score->create();
}Mai multe HasMany pe aceeași cheie
HasMany este repetabil — poți pune mai multe pe aceeași proprietate (de obicei pe $id):
// Modelul RegistrationRequest din School
class RegistrationRequest extends Model
{
#[PrimaryKey, AutoIncrement]
#[HasMany('contact_info', RegistrationRequestContact::class, 'request_id')]
#[HasMany('candidates', RegistrationRequestStudent::class, 'request_id')]
#[HasMany('payments', RegistrationRequestPayment::class, 'request_id')]
public int $id;
}DefaultOrder cu atribute multiple
Poți defini mai multe ordonări implicite pe același model:
#[Table("school_students")]
#[DefaultOrder("surname", Order::ASC)]
#[DefaultOrder("name", Order::ASC)]
class Student extends Model
{
#[PrimaryKey, AutoIncrement]
public int $id;
public ?string $surname = null;
public ?string $name = null;
}BelongsTo pe cheie primară
Un câmp poate fi și PK și FK în același timp:
class Contract extends Model
{
// Cheie primară compusă: school_year_id + number
#[PrimaryKey]
#[BelongsTo('school_year', SchoolYear::class, 'id')]
public int $school_year_id;
#[PrimaryKey]
public int $number;
#[BelongsTo('parent', ParentModel::class, 'id')]
public int $parent_id;
}Construire dinamică de interogări
Filtrează condiționat fără a sparge lanțul fluent:
$jobs = JobModel
::where('parent_id', null)
->with(['user', 'description', 'warning_count', 'error_count'])
->orderBy('id', Order::DESC);
// Filtru opțional
if ($filter !== 'global') {
$jobs->andWhere('namespace', Company::get('namespace'));
if (!User::isAdmin())
$jobs->andWhere('user_id', User::getId());
}
// Filtru pe status
if ($filter === 'running')
$jobs->andWhere('status', 'IN', [
JobStatus::SCHEDULED->value,
JobStatus::RUNNING->value,
JobStatus::SLEEPING->value,
]);
$jobs = $jobs->paginate($data['p'], $data['rpp'], $paginationInfo);Paginare cu informații complete
Al treilea parametru al lui paginate() primește un array populat automat:
$paginationInfo = null;
$results = Model::all()
->orderBy('id', Order::DESC)
->paginate($page, $perPage, $paginationInfo);
// $paginationInfo conține:
// first_result, last_result, total_results,
// total_pages, current_page, results_per_page
Upsert (find-or-create)
Un pattern frecvent: găsești după un criteriu unic, actualizezi dacă există, creezi dacă nu:
public static function createFromRegistrationRequestStudent(
RegistrationRequestStudent $candidate,
int $parentId
): self {
$student = self::where('nin', $candidate->nin)->first();
$isNew = !$student;
if ($isNew) {
$student = new self();
}
$student->parent_id = $parentId;
$student->surname = NameHelper::sanitize($candidate->surname);
$student->name = NameHelper::sanitize($candidate->name);
$student->nin = $candidate->nin;
// ... more fields ...
if ($isNew) {
$student->enrolled_date = new \DateTimeImmutable();
$student->create();
} else {
$student->update();
}
return $student;
}Auto-detectare tip — fără atribute explicite
Multe câmpuri nu au nevoie de atribute de stocare explicite — ORM-ul deduce tipul din tipul PHP:
class Student extends Model
{
// string → VARCHAR(255) auto-detectat
public ?string $surname = null;
public ?string $name = null;
public ?string $nin = null;
// enum → VARCHAR cu valori din enum
public ?Gender $gender = null;
// DateTimeImmutable → DATETIME (fără #[DateTime] explicit)
public ?\DateTimeImmutable $birth_date = null;
// int → INT (fără #[Integer] explicit)
public int $parent_id;
}Totuși, e recomandat să folosești atribute explicite pentru claritate și control (ex: #[Varchar(100)] vs VARCHAR(255) implicit).
Tipuri de stocare mai puțin comune
#[PHPSerializable]
Serializare PHP pentru array-uri arbitrare (folosit când ai nevoie de flexibilitate dar nu ai nevoie de query pe conținut):
class JobModel extends Model
{
#[PHPSerializable]
public ?array $state = null;
}#[Other('BIGINT(20)')]
Pentru tipuri SQL care nu sunt acoperite de atributele standard:
#[Other('BIGINT(20)')]
#[PrimaryKey, AutoIncrement]
public int $id;Computable — bulk cross-namespace
Exemplu real de #[Computable] care încarcă date utilizator din namespace-uri diferite:
// JobModel — încarcă informațiile utilizatorului per job
#[Computable('user', 'array', true)]
protected static function getUsers(array $models): array
{
$userIds = [];
foreach ($models as $model) {
if (!$model->namespace || !$model->user_id)
continue;
$userIds[$model->namespace][$model->user_id] = true;
}
// Încarcă useri din fiecare namespace
$results = [];
foreach ($userIds as $namespace => $data) {
$namespaceUserIds = array_keys($data);
Company::executeInCompanyContext($namespace, function () use ($namespace, $namespaceUserIds, &$results) {
$results[$namespace] = Company::users(filter: ['users_in' => $namespaceUserIds]);
});
}
// Returnează în aceeași ordine ca modelele primite
$resultArray = [];
foreach ($models as $model) {
if (!$model->namespace || !$model->user_id) {
$resultArray[] = null;
continue;
}
$resultArray[] = $results[$model->namespace][$model->user_id] ?? null;
}
return $resultArray;
}Enum cu valoare implicită la nivel de proprietate
enum RequestStatus: string
{
case WORKING = 'working';
case FINISHED = 'finished';
}
class RegistrationRequest extends Model
{
public RequestStatus $status = RequestStatus::WORKING;
}Valoarea implicită e setată direct pe proprietate — nu prin #[DefaultValue].
BcMath\Number cu #[Decimal]
use BcMath\Number;
class Contract extends Model
{
#[Decimal(10, 2)]
public Number $total_amount;
#[Decimal(10, 2)]
public Number $discount_amount;
#[Decimal(10, 2)]
public Number $payable_amount;
}
// Operații cu BcMath
$contract->payable_amount = $contract->total_amount - $contract->discount_amount;