Obiektowe Połączenie z Bazą (Singleton)

PHP
Priorytet: Normalny Szkic

Zadanie 100: Obiektowe połączenie z bazą danych (Singleton)

Wstęp

Programowanie obiektowe (OOP) to paradygmat, który pozwala nam na tworzenie czytelniejszego, modularnego i łatwego w rozbudowie kodu. Jednym z popularnych (choć należy stosować go z rozwagą) wzorców projektowych jest Singleton. Wyobraź sobie, że w swojej aplikacji w wielu miejscach musisz pobierać dane z bazy. Tworzenie nowego połączenia dla każdego zapytania jest bardzo nieoptymalne. Wzorzec Singleton gwarantuje nam, że w danym momencie istnieje tylko jedna instancja danej klasy, co idealnie sprawdza się w przypadku obiektu reprezentującego otwarte połączenie z bazą danych!

Cel zadania

Twoim zadaniem jest stworzenie klasy Database napisanej w PHP, która będzie łączyła się z MySQL wykorzystując wzorzec projektowy Singleton. Klasa ta powinna posiadać metody pozwalające m.in. na wysłanie zapytania (query()), odebranie wyników w postaci tablicy asocjacyjnej (fetch()) i wreszcie poprawne zakończenie połączenia (close()).

Wymagania techniczne

  1. Programowanie obiektowe (OOP) w PHP.
  2. Klasa implementująca wzorzec Singleton (prywatny konstruktor zapobiegający powoływaniu instancji słowem new, prywatna statyczna zmienna przechowująca instancję, publiczna metoda statyczna getInstance()).
  3. Wykorzystanie biblioteki PDO (lub mysqli) do komunikacji z bazą MySQL.

Kroki do wykonania

1. Podstawowy szkielet Singletona

Utwórz plik Database.php. Zbuduj w nim ogólny szkielet klasy. Pamiętaj, aby składowe i funkcje kluczowe dla wzorca były statyczne. Konstruktor zaś - prywatny.

<?php
class Database {
    private static $instance = null;
    private $connection;
    
    // Prywatny konstruktor zapobiega bezpośredniemu tworzeniu obiektów przez 'new'
    private function __construct() {
        // Tu zrealizujemy połączenie
    }
    
    // Metoda statyczna gwarantująca istnienie tylko jednej instancji
    public static function getInstance() {
        if (self::$instance == null) {
            self::$instance = new Database();
        }
        return self::$instance;
    }
}
?>

[!IMPORTANT] Commit 1: Utworzenie struktury klasy Database oraz patternu Singleton.

2. Logika połączenia z bazą

Uzupełnij prywatny konstruktor o połączenie z bazą PDO. Pamiętaj o wychwytywaniu błędów używając bloku try...catch. Załóż nową bazę lub przygotuj sobie w niej przykładową tabelę products (ID, name, price).

    // Przykład uzupełnionego konstruktora z PDO
    private function __construct() {
        $host = 'localhost';
        $db   = 'baza_testowa'; // Zmień na własną
        $user = 'root';
        $pass = '';

        try {
            $this->connection = new PDO("mysql:host=$host;dbname=$db;charset=utf8", $user, $pass);
            $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch(PDOException $e) {
            die("Błąd połączenia z bazą danych: " . $e->getMessage());
        }
    }

[!IMPORTANT] Commit 2: Ustanowienie logicznego łączenia z MySQL w strukturze OOP.

3. Metody zapytania (query) i pobierania (fetch)

Dodaj dwie metody do klasy Database:

  • query($sql): Wysyła podane w argumencie zapytanie SQL do bazy.
  • fetch($stmt): Zwraca pobrane za pomocą zapytania wyniki w postaci tablicy asocjacyjnej.
    public function query($sql) {
        // Wykonujemy zapytanie PDO i zwracamy tzw. PDOStatement
        $stmt = $this->connection->query($sql);
        return $stmt;
    }

    public function fetch($stmt) {
       // Funkcja pobiera wszystkie uzyskane wcześniej przy query() wiersze jako tablice asocjacyjne
       return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

[!IMPORTANT] Commit 3: Implementacja metod do manipulacji i czytania danych z tabel bazy danych.

4. Metoda close()

Dodaj również metodę close() służącą do formalnego zakończenia komunikacji. W przypadku interfejsu PDO zwalniamy zasoby bazy poprzez nadanie wartości null powołanej instancji nawiązanego złącza.

    public function close() {
        $this->connection = null;
    }

[!IMPORTANT] Commit 4: Dodano procedurę close(), która przerywa pracę aktywnego złącza PDO.

5. Praktyczny przykład użycia – plik index.php

Wykorzystaj stworzoną klasę w praktyce w pliku głównym. Przekonajmy się, czy to w ogóle działa!

<?php
require_once 'Database.php';

// Pobieramy naszą powołaną jedyną instancję bazy danych
$db = Database::getInstance();

// Wysyłamy nasze zapytanie korzystając ze stworzonej przez nas metody
$result = $db->query("SELECT * FROM products");

// Pobieramy dane zapytania zapisane w formie standardowej tablicy (array) 
$dane = $db->fetch($result);

// Prezentacja
echo "<pre>";
print_r($dane);
echo "</pre>";

// Na koniec – poprawne pozbycie się połączenia
$db->close();
?>

[!IMPORTANT] Commit 5: Praktyczne wywołanie Singletona oraz zapytania PDO w osobnym pliku index.php.


🔥 Dodatkowe zadania (dla ambitnych i rządnych wiedzy)

  1. Bezpieczeństwo (Prepared Statements): Zmodernizuj metodę query w taki sposób, aby przyjmowała dwa argumenty: pierwszy to zapytanie SQL ze znakami zapytania np. SELECT * FROM products WHERE id=?, a drugi to opcjonalna tablica parametrów do bindowania za pomocą pętli bezpieczeństwa execute($tablica). Ochroni to Twoją bazę m.in. przed atakami SQL Injection.
  2. Helper typu insert(): Stwórz uniwersalną metodę insert($tabela, $dane), dzięki której za pomocą jednego wywołania wkleisz rekordy do bazy stosując tablicę asocjacyjną np. ['nazwa_pola' => 'wartosc'].
  3. Złamanie reguł (klonowanie): Co jeśli programista w index.php napisze $nowa_baza = clone $db;? W domyślnej formie Singleton użyty wyżej na to pozwala! Dodaj w klasie prywatną metodę magiczną private function __clone() {} i sprawdź, czy wciąż można w sposób niedozwolony sklonować instancję Twojej Database.
  4. Złamanie reguł (unserialize): Analogicznie do klonowania, dodaj prywatną metodę magiczną private function __wakeup() {}, aby zabronić deserializacji z zewnętrznego źródła.

Git Help - Jak commitować?

Gdy zrobisz dany krok przedstawiony wyżej w liście zadań, pamiętaj o tworzeniu komitów (zapisywanie punktów Twojej pracy). Otwórz terminal wewnątrz folderu z zadaniem (w VSCode możesz to zrobić skrótem Ctrl + `) i wprowadź polecenie z nazwą danego etapu:

git add .
git commit -m "Commit 1 - Tworzymy strukturę klasy Singleton Database"

Jeżeli git wyrzuci błąd z uwierzytelnieniem (First-time user setup problem), po prostu ustaw usera w swojej konsoli:

git config --global user.name "Swoje Imię"
git config --global user.email "[email protected]"