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.
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;
}
Podsumowanie modułu
Przeszedłeś długą drogę w tym module:
- Relacyjne bazy danych i SQL — tabele, kolumny,
CREATE TABLE,INSERT,SELECT. - PDO — bezpieczne połączenie z
ERRMODE_EXCEPTION,FETCH_ASSOC,utf8mb4. - SELECT przez PDO —
query(),fetch(),fetchAll(), bezpieczne wyświetlanie przezhtmlspecialchars. - Prepared statements —
prepare()+execute()jako tarcza przed SQL injection. - Pełny CRUD —
INSERT+lastInsertId(),UPDATE+rowCount(),DELETE. - 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?
Klasa, która sama tworzy swoje zależności (new PDO() w środku), jest trudna do testowania i sztywno powiązana z konkretną bazą. Wstrzyknięcie przez konstruktor (dependency injection) pozwala podać dowolny obiekt PDO z zewnątrz — produkcyjny, testowy, mockowy.
Co oznacza private readonly PDO $pdo w konstruktorze z property promotion?
private ogranicza dostęp do wnętrza klasy — nikt z zewnątrz nie może odczytać ani zmienić $pdo. readonly (PHP 8.1) gwarantuje, że wartość zostanie ustawiona tylko raz — w konstruktorze. Razem dają bezpieczną, niezmienną zależność.
Metoda znajdz(int $id): ?array zwraca ?array. Co oznacza znak zapytania przed array?
?array to nullable typ — skrót od 'array|null'. Metoda znajdz() zwraca tablicę asocjacyjną gdy rekord istnieje w bazie, lub null gdy go nie ma. Dzięki temu wywołujący może sprawdzić if ($z === null) zamiast if ($z === false).
Ć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.';