Jak zrealizować wzorzec Active Record w PHP na PDO
Wzorzec Active Record to jeden z najpopularniejszych sposobów pracy z bazą danych w aplikacjach PHP. Polega na tym, że jeden obiekt reprezentuje jeden rekord w tabeli, a sama klasa zawiera zarówno dane, jak i logikę ich zapisu, aktualizacji czy usuwania.
Przykładowo:
- obiekt
Userodpowiada rekordowi z tabeliusers - obiekt
Productodpowiada rekordowi z tabeliproducts
Każdy obiekt potrafi:
- wczytać siebie z bazy
- zapisać zmiany
- usunąć rekord
- pobierać listy rekordów
W tym artykule pokażę, jak stworzyć uniwersalny Active Record w PHP oparty o PDO, który zadziała dla dowolnej tabeli.
Dlaczego PDO?
PDO (PHP Data Objects) to nowoczesna warstwa dostępu do baz danych:
Struktura projektu
/project
├── Database.php
├── ActiveRecord.php
├── User.php
├── Product.php
1. Klasa połączenia z bazą
Database.php
<?php
class Database
{
private static ?PDO $pdo = null;
public static function connect(): PDO
{
if (self::$pdo === null) {
self::$pdo = new PDO(
'mysql:host=localhost;dbname=test;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
}
return self::$pdo;
}
}
2. Bazowa klasa Active Record
To najważniejszy element systemu.
ActiveRecord.php
<?php
abstract class ActiveRecord
{
protected static string $table;
protected static string $primaryKey = 'id';
protected array $attributes = [];
public function __construct(array $data = [])
{
$this->attributes = $data;
}
public function __get($name)
{
return $this->attributes[$name] ?? null;
}
public function __set($name, $value)
{
$this->attributes[$name] = $value;
}
protected static function db(): PDO
{
return Database::connect();
}
public function save(): bool
{
$pk = static::$primaryKey;
if (!empty($this->attributes[$pk])) {
return $this->update();
}
return $this->insert();
}
protected function insert(): bool
{
$columns = array_keys($this->attributes);
$placeholders = array_map(fn($c) => ':' . $c, $columns);
$sql = "INSERT INTO " . static::$table .
" (" . implode(',', $columns) . ")
VALUES (" . implode(',', $placeholders) . ")";
$stmt = self::db()->prepare($sql);
$result = $stmt->execute($this->attributes);
if ($result) {
$this->attributes[static::$primaryKey] =
self::db()->lastInsertId();
}
return $result;
}
protected function update(): bool
{
$pk = static::$primaryKey;
$fields = [];
foreach ($this->attributes as $column => $value) {
if ($column !== $pk) {
$fields[] = "$column = :$column";
}
}
$sql = "UPDATE " . static::$table .
" SET " . implode(',', $fields) .
" WHERE $pk = :$pk";
$stmt = self::db()->prepare($sql);
return $stmt->execute($this->attributes);
}
public function delete(): bool
{
$pk = static::$primaryKey;
$sql = "DELETE FROM " . static::$table .
" WHERE $pk = ?";
$stmt = self::db()->prepare($sql);
return $stmt->execute([$this->attributes[$pk]]);
}
public static function find($id): ?static
{
$pk = static::$primaryKey;
$sql = "SELECT * FROM " . static::$table .
" WHERE $pk = ? LIMIT 1";
$stmt = self::db()->prepare($sql);
$stmt->execute([$id]);
$row = $stmt->fetch();
return $row ? new static($row) : null;
}
public static function all(): array
{
$sql = "SELECT * FROM " . static::$table;
$stmt = self::db()->query($sql);
$rows = $stmt->fetchAll();
return array_map(fn($row) => new static($row), $rows);
}
}
3. Klasy konkretnych tabel
Tabela users
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(150)
);
User.php
<?php
class User extends ActiveRecord
{
protected static string $table = 'users';
}
Tabela products
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
price DECIMAL(10,2)
);
Product.php
<?php
class Product extends ActiveRecord
{
protected static string $table = 'products';
}
4. Użycie w praktyce
Dodanie użytkownika
$user = new User();
$user->name = 'Jan';
$user->email = 'jan@test.pl';
$user->save();
Pobranie użytkownika
$user = User::find(1);
echo $user->name;
Aktualizacja
$user = User::find(1);
$user->name = 'Jan Kowalski';
$user->save();
Usuwanie
$user = User::find(1);
$user->delete();
Lista rekordów
$users = User::all();
foreach ($users as $user) {
echo $user->name . PHP_EOL;
}
Dlaczego to działa dla dowolnej tabeli?
Każda klasa dziedziczy po ActiveRecord i podaje tylko:
protected static string $table = 'nazwa_tabeli';
Reszta logiki działa automatycznie:
save()find()all()delete()
Możesz dodać:
class Orders extends ActiveRecord
{
protected static string $table = 'orders';
}
I gotowe.
Jak ulepszyć ten system?
1. Walidacja danych
$user->email = 'zlymail';
$user->validate();
2. Relacje
$user->posts();
$order->items();
3. Soft delete
Zamiast usuwać rekord:
deleted_at DATETIME NULL
4. Query Builder
User::where('status', 'active')->orderBy('id DESC')->limit(10);
Zalety Active Record
- prosty kod
- szybkie CRUD
- mało SQL w aplikacji
- idealny do małych i średnich projektów
Wady
- przy dużych systemach może mieszać logikę biznesową z bazą danych
- trudniejszy w testowaniu niż Data Mapper
- skomplikowane relacje bywają problematyczne
Kiedy używać?
Idealnie gdy:
- tworzysz CMS
- panel admina
- sklep
- REST API
- MVP startupu
Lepiej nie gdy:
- system enterprise
- rozbudowana domena biznesowa
- setki tabel i skomplikowane zależności
Podsumowanie
Aby stworzyć uniwersalny Active Record w PHP na PDO, wystarczy:
- jedna klasa
Database - jedna klasa bazowa
ActiveRecord - klasy modeli dziedziczące po niej
Dzięki temu dla dowolnej tabeli możesz zrobić:
$user = User::find(1);
$user->name = 'Nowe Imię';
$user->save();
bez pisania SQL za każdym razem.