ORM w PHP - Active Record z Property Hooks PHP 8.4

24.09.2025

ORM w PHP - Active Record z Property Hooks PHP 8.4

Czym jest ORM?

Object-Relational Mapping (ORM) to wzorzec pozwalający na pracę z bazą danych przy użyciu obiektów PHP zamiast surowego SQL. Zamiast pisać zapytania SELECT/INSERT/UPDATE, operujesz na obiektach, a ORM automatycznie generuje odpowiednie zapytania.

Active Record - obiekt = wiersz w tabeli

W wzorcu Active Record każdy obiekt reprezentuje pojedynczy wiersz w bazie danych. Obiekt wie, jak się zapisać, zaktualizować i usunąć:

<?php
class User extends Model
{
    public string $table { get => 'users'; }

    public int $id;
    public string $name;
    public string $email;
    public DateTime $created_at;
}

// Tworzenie nowego użytkownika
$user = new User();
$user->name = 'Jan Kowalski';
$user->email = 'jan@example.com';
$user->save(); // INSERT INTO users...

// Aktualizacja
$user->name = 'Anna Kowalska';
$user->save(); // UPDATE users SET name = 'Anna Kowalska' WHERE id = 1

Property Hooks - automatyczne śledzenie zmian

PHP 8.4 wprowadza Property Hooks - funkcje wywoływane przy odczycie/zapisie właściwości. ORM wykorzystuje je do automatycznego śledzenia, które pola zostały zmienione:

<?php
class Model
{
    private array $changedProperties = [];

    public string $name {
        get => $this->name;
        set {
            $this->name = $value;
            $this->markPropertyAsChanged('name');
        }
    }

    public string $email {
        get => $this->email;
        set {
            $this->email = $value;
            $this->markPropertyAsChanged('email');
        }
    }

    protected function markPropertyAsChanged(string $property): void
    {
        $this->changedProperties[$property] = true;
    }
}

Dlaczego to ważne?

Dzięki Property Hooks, ORM generuje optymalne zapytania UPDATE zawierające tylko zmienione pola:

<?php
$user = User::find(1);
$user->name = 'Nowa nazwa'; // tylko name jest w changedProperties

// Generuje: UPDATE users SET name = 'Nowa nazwa' WHERE id = 1
// Zamiast: UPDATE users SET name = '...', email = '...', created_at = '...' WHERE id = 1

PropertyObserver - wzorzec Observer

PropertyObserver śledzi zmiany w obiektach wykorzystując Property Hooks:

<?php
class PropertyObserver
{
    private array $originalValues = [];
    private array $changedProperties = [];

    public function startObserving(Model $model): void
    {
        foreach ($model->getProperties() as $property => $value) {
            $this->originalValues[$property] = $value;
        }
    }

    public function propertyChanged(string $property, mixed $newValue): void
    {
        if ($this->originalValues[$property] !== $newValue) {
            $this->changedProperties[$property] = $newValue;
        }
    }

    public function getChangedProperties(): array
    {
        return $this->changedProperties;
    }
}

Relacje między tabelami

ORM wspiera 4 typy relacji: HasMany, BelongsTo, BelongsToMany i HasManyThrough:

HasMany - jeden do wielu

<?php
class User extends Model
{
    #[HasMany(Post::class, foreign_key: 'user_id')]
    public array $posts {
        get => $this->relations->getRelation('posts');
    }
}

// Użycie - lazy loading
$user = User::find(1);
$posts = $user->posts; // SELECT * FROM posts WHERE user_id = 1

BelongsTo - wiele do jednego

<?php
class Post extends Model
{
    #[BelongsTo(User::class, foreign_key: 'user_id')]
    public ?User $user {
        get => $this->relations->getRelation('user');
    }
}

// Użycie
$post = Post::find(1);
$author = $post->user; // SELECT * FROM users WHERE id = ?

BelongsToMany - wiele do wielu

<?php
class Post extends Model
{
    #[BelongsToMany(
        Tag::class,
        pivot: 'post_tags',
        foreign_pivot_key: 'post_id',
        related_pivot_key: 'tag_id'
    )]
    public array $tags {
        get => $this->relations->getRelation('tags');
    }
}

// Użycie
$post = Post::find(1);
$tags = $post->tags;
// SELECT tags.* FROM tags
// INNER JOIN post_tags ON tags.id = post_tags.tag_id
// WHERE post_tags.post_id = 1

HasManyThrough - przez pośrednika

<?php
class Country extends Model
{
    #[HasManyThrough(
        Post::class,
        through: User::class,
        first_key: 'country_id',
        second_key: 'user_id'
    )]
    public array $posts {
        get => $this->relations->getRelation('posts');
    }
}

// Użycie - posty wszystkich użytkowników z danego kraju
$country = Country::find(1);
$posts = $country->posts;
// SELECT posts.* FROM posts
// INNER JOIN users ON posts.user_id = users.id
// WHERE users.country_id = 1

EntityManager - zarządzanie cyklem życia obiektów

EntityManager odpowiada za persystowanie obiektów w bazie danych:

<?php
class EntityManager
{
    public function persist(Model $entity): void
    {
        if ($entity->exists()) {
            $this->entityUpdater->update($entity);
        } else {
            $this->entityInserter->insert($entity);
        }
    }

    public function find(string $class, int $id): ?Model
    {
        $table = (new $class)->table;
        $data = $this->queryBuilder
            ->select($table)
            ->where('id', '=', $id)
            ->first();

        return $data ? $this->hydrate($class, $data) : null;
    }

    private function hydrate(string $class, array $data): Model
    {
        $entity = new $class();
        foreach ($data as $key => $value) {
            $entity->$key = $value;
        }
        return $entity;
    }
}

EntityUpdater - optymalne UPDATE

EntityUpdater generuje zapytanie UPDATE zawierające tylko zmienione pola:

<?php
class EntityUpdater
{
    public function update(Model $entity): void
    {
        $changedProperties = $entity->getChangedProperties();

        if (empty($changedProperties)) {
            return; // nic się nie zmieniło
        }

        $this->queryBuilder
            ->update($entity->table)
            ->set($changedProperties)
            ->where('id', '=', $entity->id)
            ->execute();
    }
}

🎓 Zbuduj ORM z Property Hooks od podstaw!

W kursie PHP 8.4 implementujesz zaawansowany ORM z wykorzystaniem Property Hooks - od PropertyObserver przez Model base class, wszystkie typy relacji (HasMany, BelongsTo, BelongsToMany, HasManyThrough), aż po EntityManager z change tracking. Pełna implementacja Active Record.

Kup pełny kurs PHP 8.4 🚀 Pobierz darmowy fragment 📥

Zobacz też inne artykuły o DBAL