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.