Wzorzec repozytorium — klasa ZadanieRepozytorium

🎯 Po tej lekcji będziesz tworzyć klasę repozytorium opakowującą PDO, wstrzykiwać zależności przez konstruktor i pisać metody CRUD enkapsulujące SQL.

Do tej pory cały kod SQL był rozrzucony po plikach PHP — kilka linii tu, kilka tam. W miarę rozrastania się aplikacji taki kod staje się trudny do utrzymania: gdy zmienia się struktura tabeli, musisz szukać zapytań w wielu miejscach naraz.

Wzorzec repozytorium rozwiązuje ten problem: zamykasz całą logikę dostępu do bazy w jedną klasę. Reszta aplikacji nie wie nic o SQL — wywołuje metody repozytorium i dostaje dane.

Czym jest repozytorium?

Repozytorium to klasa, która:

  • opakowuje PDO — trzyma je jako prywatną właściwość.
  • udostępnia metody zamiast surowych zapytań: wszystkie(), znajdz($id), dodaj(...), usun($id).
  • enkapsuluje SQL — zmieniasz zapytanie w jednym miejscu, a nie w dziesiątkach plików.
  • jest łatwa do testowania — możesz wstrzyknąć testową bazę bez zmiany kodu.

To połączenie OOP (Moduł 6) z PDO.

Klasa ZadanieRepozytorium

Oto pełna implementacja repozytorium dla tabeli zadania:

<?php
declare(strict_types=1);

final class ZadanieRepozytorium
{
    // Property promotion z PHP 8 — $pdo jest prywatne i readonly
    public function __construct(
        private readonly PDO $pdo,
    ) {}

    /**
     * Zwraca wszystkie zadania, posortowane od najnowszego.
     *
     * @return array<int,array<string,mixed>>
     */
    public function wszystkie(): array
    {
        $stmt = $this->pdo->query(
            'SELECT id, tytul, opis, ukonczone, created_at FROM zadania ORDER BY id DESC'
        );

        return $stmt->fetchAll();
    }

    /**
     * Zwraca jedno zadanie po id lub null gdy nie istnieje.
     *
     * @return array<string,mixed>|null
     */
    public function znajdz(int $id): ?array
    {
        $stmt = $this->pdo->prepare(
            'SELECT id, tytul, opis, ukonczone, created_at FROM zadania WHERE id = :id'
        );
        $stmt->execute([':id' => $id]);

        $wiersz = $stmt->fetch();

        return $wiersz === false ? null : $wiersz;
    }

    /**
     * Dodaje nowe zadanie i zwraca jego id.
     */
    public function dodaj(string $tytul, string $opis = ''): int
    {
        $stmt = $this->pdo->prepare(
            'INSERT INTO zadania (tytul, opis) VALUES (:tytul, :opis)'
        );
        $stmt->execute([':tytul' => $tytul, ':opis' => $opis]);

        return (int) $this->pdo->lastInsertId();
    }

    /**
     * Usuwa zadanie o podanym id. Zwraca true jeśli wiersz istniał.
     */
    public function usun(int $id): bool
    {
        $stmt = $this->pdo->prepare('DELETE FROM zadania WHERE id = :id');
        $stmt->execute([':id' => $id]);

        return $stmt->rowCount() > 0;
    }
}

Co tu się dzieje — krok po kroku

Property promotion (private readonly PDO $pdo) — PHP 8 automatycznie tworzy prywatną właściwość $pdo i przypisuje jej wartość przekazaną przy new ZadanieRepozytorium($pdo). Słowo readonly gwarantuje, że nikt nie nadpisze referencji po inicjalizacji.

wszystkie() używa query() — zapytanie nie zawiera żadnych danych od użytkownika, więc query() jest bezpieczne.

znajdz() używa prepare() — filtruje po id, które pochodzi z zewnątrz. Prepared statement, bez wyjątku.

dodaj() i usun() — zawsze prepared statements — modyfikują dane, więc bezwzględnie wymagają parametryzacji.

Użycie repozytorium

<?php
declare(strict_types=1);

require 'db.php';           // $pdo
require 'ZadanieRepozytorium.php';

$repo = new ZadanieRepozytorium($pdo);

// Dodaj zadanie
$id = $repo->dodaj('Nauczyć się repozytorium', 'Wzorzec opakowujący PDO');
echo 'Dodano zadanie #' . $id;

// Wszystkie zadania
$zadania = $repo->wszystkie();
foreach ($zadania as $z) {
    echo htmlspecialchars($z['tytul'], ENT_QUOTES, 'UTF-8') . "\n";
}

// Jedno zadanie
$zadanie = $repo->znajdz($id);
if ($zadanie !== null) {
    echo htmlspecialchars($zadanie['tytul'], ENT_QUOTES, 'UTF-8');
}

// Usuń
$usunieto = $repo->usun($id);
echo $usunieto ? 'Usunięto.' : 'Nie istniało.';

Kod strony nie widzi ani jednej linii SQL — tylko czyste metody z polskimi nazwami.

Wstrzykiwanie zależności — dependency injection

Zauważ, że ZadanieRepozytorium nie tworzy PDO samodzielnie (np. przez new PDO(...) wewnątrz konstruktora). Dostaje je z zewnątrz:

$repo = new ZadanieRepozytorium($pdo); // $pdo przekazujesz ty

To wzorzec dependency injection (wstrzykiwanie zależności). Korzyści:

  • Elastyczność — możesz podać PDO podłączone do testowej bazy SQLite lub prawdziwego MySQL.
  • Separacja odpowiedzialności — repozytorium nie wie, jak powstało połączenie, tylko używa go.
  • Testowalność — w testach możesz użyć innej bazy bez zmiany kodu repozytorium.
💡 Jedno repozytorium na tabelę: Dobrą praktyką jest tworzenie osobnej klasy repozytorium dla każdej logicznej encji: ZadanieRepozytorium, UzytkownikRepozytorium, KomentarzRepozytorium. Każda klasa zna SQL tylko swojej tabeli. Wzorzec doskonale skaluje się z rozrastającą się aplikacją.

Rozszerzanie repozytorium

Repozytorium łatwo rozszerzysz o nowe metody, np. filtrowanie po statusie:

<?php
declare(strict_types=1);

// Wewnątrz klasy ZadanieRepozytorium:

/**
 * @return array<int,array<string,mixed>>
 */
public function nieukonczone(): array
{
    $stmt = $this->pdo->query(
        'SELECT id, tytul FROM zadania WHERE ukonczone = 0 ORDER BY id DESC'
    );

    return $stmt->fetchAll();
}

/**
 * Oznacza zadanie jako ukończone. Zwraca true jeśli istniało.
 */
public function oznaczUkonczone(int $id): bool
{
    $stmt = $this->pdo->prepare(
        'UPDATE zadania SET ukonczone = 1 WHERE id = :id'
    );
    $stmt->execute([':id' => $id]);

    return $stmt->rowCount() > 0;
}
⚠️ Repozytorium nie zastępuje walidacji: Metoda dodaj() powinna otrzymywać już zwalidowane dane. Sprawdzanie, czy tytul nie jest pusty czy ma odpowiednią długość, należy do warstwy wyżej — kontrolera lub serwisu. Repozytorium odpowiada tylko za zapis do bazy.

Podsumowanie modułu

Przeszedłeś długą drogę w tym module:

  1. Relacyjne bazy danych i SQL — tabele, kolumny, CREATE TABLE, INSERT, SELECT.
  2. PDO — bezpieczne połączenie z ERRMODE_EXCEPTION, FETCH_ASSOC, utf8mb4.
  3. SELECT przez PDOquery(), fetch(), fetchAll(), bezpieczne wyświetlanie przez htmlspecialchars.
  4. Prepared statementsprepare() + execute() jako tarcza przed SQL injection.
  5. Pełny CRUDINSERT + lastInsertId(), UPDATE + rowCount(), DELETE.
  6. Repozytorium — enkapsulacja SQL w klasę, dependency injection, czyste API.

Masz teraz solidny zestaw umiejętności do budowania aplikacji PHP z bazą danych. Każde zapytanie z danymi od użytkownika przechodzi przez prepared statement — i to jest fundamentem bezpiecznego kodu.

Sprawdź się

Dlaczego PDO wstrzykujemy przez konstruktor zamiast tworzyć je wewnątrz klasy repozytorium?

Co oznacza private readonly PDO $pdo w konstruktorze z property promotion?

Metoda znajdz(int $id): ?array zwraca ?array. Co oznacza znak zapytania przed array?

Ćwiczenie

Dodaj do klasy ZadanieRepozytorium metodę aktualizujTytul(int $id, string $nowyTytul): bool, która zmienia tytul zadania o danym id. Zwróć true jeśli wiersz istniał i został zmieniony (rowCount() > 0), false w przeciwnym razie. Użyj prepared statement.

Pokaż rozwiązanie
<?php
declare(strict_types=1);

// Dodaj wewnątrz klasy ZadanieRepozytorium:

public function aktualizujTytul(int $id, string $nowyTytul): bool
{
    $stmt = $this->pdo->prepare(
        'UPDATE zadania SET tytul = :tytul WHERE id = :id'
    );
    $stmt->execute([':tytul' => $nowyTytul, ':id' => $id]);
    return $stmt->rowCount() > 0;
}

// Użycie:
// $repo = new ZadanieRepozytorium($pdo);
// $sukces = $repo->aktualizujTytul(3, 'Nowy tytuł');
// echo $sukces ? 'Zaktualizowano.' : 'Nie znaleziono.';