Salt la conținut
Exemple din practică

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;