Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Jak zrealizować wzorzec Active Record w PHP na PDO

Wizualizacja tematu 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 User odpowiada rekordowi z tabeli users
  • obiekt Product odpowiada rekordowi z tabeli products

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:

  1. jedna klasa Database
  2. jedna klasa bazowa ActiveRecord
  3. 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.

11 maja 2026 5

Kategorie

programowanie

Tagi

pdo php

Dziękujemy!
()

Powiązane wpisy

Wizualizacja tematu Automatyczny motyw Bootstrap 53 na bazie pory dnia
4 stycznia 2025 6 min 71

Automatyczny motyw Bootstrap 5.3 na bazie pory dnia

php
Czytaj więcej
Grafika przedstawia Long Polling vs Short Polling Porwnanie
21 stycznia 2025 5 min 26

Long Polling vs. Short Polling: Porównanie

php
Czytaj więcej
Wizualizacja tematu Wyraenia regularne i ich obsuga w PHP oraz JavaScript
1 lutego 2025 5 min 19

Wyrażenia regularne i ich obsługa w PHP oraz JavaScript

php
Czytaj więcej
Wymiana doświadczeń

Masz podobne doświadczenia?

Chętnie poznam Twoją perspektywę i porozmawiam o tym temacie szerzej.

Napisz do mnie

Każda perspektywa może wnieść coś wartościowego do dyskusji.

Twoja prywatność i pliki cookies

  1. Ta strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania – m.in. do poprawnego wyświetlania treści, zapamiętania podstawowych ustawień przeglądarki oraz zapewnienia stabilności serwisu.
  2. Nie stosuję plików cookies w celach marketingowych, reklamowych ani analitycznych.
  3. Strona ma charakter wyłącznie informacyjny i nie zawiera formularzy kontaktowych, rejestracyjnych ani zakupowych, przez które dane mogłyby być przesyłane na serwer.
  4. Nie zbieram danych osobowych podczas zwykłego korzystania z witryny.
  5. Serwis nie korzysta z certyfikatu SSL, jednak ze względu na informacyjny charakter strony nie jest wymagane przesyłanie poufnych danych. Zalecam jednak, aby nigdy nie wpisywać haseł ani danych osobowych na stronach bez szyfrowanego połączenia.
  6. Korzystając z tej strony, wyrażasz zgodę na używanie wyłącznie niezbędnych plików cookies.

Więcej informacji znajdziesz w mojej polityce prywatności.