PHP – Programowanie obiektowe, bazy danych i wzorce CMS
1. PHP: Język, który napędza połowę internetu
Na Wykładzie 1 widzieliśmy, że Backend to “mózg” operacji — kod ukryty przed użytkownikiem, działający na serwerze. Pokazywaliśmy przykłady w Pythonie (Flask), Node.js i Julii. Dziś skupimy się na PHP.
Fakty: Według danych W3Techs, PHP jest używany przez ok. 75% wszystkich stron internetowych z rozpoznanym językiem server-side. WordPress (43% wszystkich stron w internecie), Facebook (początkowo), Wikipedia — wszystkie zbudowane na PHP.
PHP nie jest “starym, brzydkim językiem”, jak niektórzy twierdzą. PHP 8.x to nowoczesny język z typowaniem, enum-ami, match expression i wydajnością porównywalną z innymi rozwiązaniami. Klucz to pisanie nowoczesnego PHP, a nie kodu jak w 2005 roku.
Jak PHP działa?
Przypomnijmy schemat z Wykładu 1:
- Przeglądarka wysyła żądanie HTTP (
GET /strona.php) - Serwer WWW (Apache/Nginx) rozpoznaje plik
.phpi przekazuje go do interpretera PHP - PHP wykonuje kod, generuje HTML (lub JSON)
- Serwer odsyła wynik do przeglądarki
- Przeglądarka renderuje otrzymany HTML
<?php
// Prosty skrypt — generuje HTML dynamicznie
$godzina = date('H');
$powitanie = ($godzina < 12) ? 'Dzień dobry' : 'Dobry wieczór';
?>
<!DOCTYPE html>
<html lang="pl">
<head><title>Powitanie</title></head>
<body>
<h1><?= $powitanie ?>, użytkowniku!</h1>
<p>Jest godzina <?= date('H:i') ?></p>
</body>
</html>Znak <?= ... ?> to skrót od <?php echo ... ?>. Powyższy skrypt generuje inny HTML o 8 rano niż o 20 wieczorem — to samo “R” w akronimie REST: serwer wysyła reprezentację, nie sam zasób.
2. PHP 8.x: Nowoczesna składnia
Zanim przejdziemy do OOP, poznajmy kluczowe cechy współczesnego PHP.
Strict typing
<?php
declare(strict_types=1); // Włącza ścisłe typowanie — na początku KAŻDEGO pliku
function dodaj(int $a, int $b): int {
return $a + $b;
}
echo dodaj(2, 3); // ✅ 5
echo dodaj("2", "3"); // ❌ TypeError! Bez strict_types zwróciłby 5declare(strict_types=1) to odpowiednik TypeScript dla JavaScriptu. Bez tego PHP cicho konwertuje typy (np. string “42” na int 42), co prowadzi do trudnych do wykrycia błędów.
Typy złożone i nowe konstrukcje
<?php
declare(strict_types=1);
// Union types — zmienna akceptuje kilka typów
function znajdz(int|string $id): ?array {
// ?array oznacza "array albo null"
return null;
}
// Named arguments — czytelniejsze wywołania
function stworzUzytkownika(
string $imie,
string $email,
int $wiek = 25,
bool $aktywny = true
): array {
return compact('imie', 'email', 'wiek', 'aktywny');
}
// Zamiast pamiętać kolejność argumentów:
$user = stworzUzytkownika(
email: 'jan@wp.pl', // Kolejność nie ma znaczenia!
imie: 'Jan',
aktywny: false
);
// Match expression — czytelniejsza alternatywa dla switch
$status = 'admin';
$rola = match($status) {
'admin' => 'Pełen dostęp',
'editor' => 'Edycja treści',
'viewer' => 'Tylko odczyt',
default => 'Brak uprawnień',
};
// Enum (PHP 8.1)
enum StatusZamowienia: string {
case Nowe = 'nowe';
case Oplacone = 'oplacone';
case Wyslane = 'wyslane';
case Dostarczone = 'dostarczone';
}
$zamowienie = StatusZamowienia::Oplacone;
echo $zamowienie->value; // 'oplacone'3. Programowanie obiektowe (OOP) w PHP
Proceduralne PHP (same funkcje i zmienne globalne) to przepis na katastrofę w większym projekcie. OOP pozwala organizować kod w logiczne, hermetyczne jednostki.
Klasa i obiekt
Klasa to przepis (blueprint), obiekt to konkretne ciasto upieczone z tego przepisu (analogia z Dockera — Obraz vs Kontener).
<?php
declare(strict_types=1);
class Produkt {
// Właściwości — dane obiektu
private string $nazwa;
private float $cena;
private int $ilosc;
// Konstruktor — wywoływany przy tworzeniu obiektu (new Produkt(...))
public function __construct(string $nazwa, float $cena, int $ilosc = 0) {
$this->nazwa = $nazwa;
$this->cena = $cena;
$this->ilosc = $ilosc;
}
// Metody publiczne — interfejs obiektu
public function getNazwa(): string {
return $this->nazwa;
}
public function getCena(): float {
return $this->cena;
}
public function jestDostepny(): bool {
return $this->ilosc > 0;
}
public function sprzedaj(int $ile = 1): void {
if ($ile > $this->ilosc) {
throw new \RuntimeException("Brak wystarczającej ilości na stanie");
}
$this->ilosc -= $ile;
}
// Metoda do serializacji — przydatne przy API (JSON)
public function toArray(): array {
return [
'nazwa' => $this->nazwa,
'cena' => $this->cena,
'ilosc' => $this->ilosc,
'dostepny' => $this->jestDostepny(),
];
}
}
// Użycie:
$laptop = new Produkt('ThinkPad X1', 5499.99, 10);
echo $laptop->getNazwa(); // "ThinkPad X1"
echo $laptop->jestDostepny(); // true
$laptop->sprzedaj(2);
echo json_encode($laptop->toArray());
// {"nazwa":"ThinkPad X1","cena":5499.99,"ilosc":8,"dostepny":true}Constructor Promotion (PHP 8.0)
Pisanie $this->nazwa = $nazwa w konstruktorze jest powtarzalne. PHP 8 upraszcza to drastycznie:
<?php
declare(strict_types=1);
class Produkt {
// Konstruktor z promocją — właściwości deklarowane bezpośrednio w parametrach
public function __construct(
private string $nazwa,
private float $cena,
private int $ilosc = 0
) {
// Ciało może być puste — PHP sam przypisze wartości
}
public function getNazwa(): string {
return $this->nazwa;
}
// ... reszta metod jak wcześniej
}Mniej kodu, ten sam efekt. private string $nazwa w parametrze konstruktora automatycznie tworzy właściwość i przypisuje wartość.
Enkapsulacja i modyfikatory dostępu
<?php
class KontoBankowe {
public function __construct(
private string $wlasciciel,
private float $saldo = 0.0 // ← private: nikt z zewnątrz nie zmieni bezpośrednio
) {}
public function wplac(float $kwota): void {
if ($kwota <= 0) {
throw new \InvalidArgumentException("Kwota musi być dodatnia");
}
$this->saldo += $kwota;
}
public function getSaldo(): float {
return $this->saldo;
}
// Metoda prywatna — używana tylko wewnątrz klasy
private function naliczOdsetki(): void {
$this->saldo *= 1.05;
}
}
$konto = new KontoBankowe('Jan Kowalski', 1000);
$konto->wplac(500);
echo $konto->getSaldo(); // 1500
// $konto->saldo = 999999; ❌ Fatal Error! saldo jest private
// $konto->naliczOdsetki(); ❌ Fatal Error! metoda jest private| Modyfikator | Widoczność | Użycie |
|---|---|---|
public |
Wszędzie | Interfejs obiektu (metody do użytku zewnętrznego) |
protected |
Klasa + klasy dziedziczące | Metody/właściwości dla podklas |
private |
Tylko wewnątrz tej klasy | Dane wewnętrzne, metody pomocnicze |
Dziedziczenie i interfejsy
<?php
// Interfejs — kontrakt: "każda klasa implementująca MUSI mieć te metody"
interface Exportowalny {
public function toArray(): array;
public function toJSON(): string;
}
// Klasa abstrakcyjna — nie można stworzyć obiektu z niej bezpośrednio
abstract class ElementSklepu implements Exportowalny {
public function __construct(
protected string $nazwa,
protected float $cena
) {}
// Metoda abstrakcyjna — MUSI być zaimplementowana przez podklasę
abstract public function obliczPodatek(): float;
// Metoda konkretna — dziedziczona automatycznie
public function toJSON(): string {
return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
}
}
class ProduktFizyczny extends ElementSklepu {
public function __construct(
string $nazwa,
float $cena,
private float $waga // kg
) {
parent::__construct($nazwa, $cena);
}
public function obliczPodatek(): float {
return $this->cena * 0.23; // 23% VAT
}
public function toArray(): array {
return [
'nazwa' => $this->nazwa,
'cena' => $this->cena,
'podatek' => $this->obliczPodatek(),
'waga' => $this->waga,
];
}
}
class ProduktCyfrowy extends ElementSklepu {
public function obliczPodatek(): float {
return $this->cena * 0.08; // 8% VAT na e-booki
}
public function toArray(): array {
return [
'nazwa' => $this->nazwa,
'cena' => $this->cena,
'podatek' => $this->obliczPodatek(),
];
}
}4. PDO: Bezpieczne połączenie z bazą danych
Na Wykładzie 1 serwer “udawał” bazę danych prostym słownikiem w Pythonie. W rzeczywistości dane mieszkają w systemie bazodanowym (MySQL, PostgreSQL, SQLite).
PHP łączy się z bazą przez PDO (PHP Data Objects) — uniwersalny interfejs, który działa z każdym silnikiem SQL.
Wzorzec Singleton: Jedno połączenie na żądanie
Tworzenie nowego połączenia z bazą przy każdym zapytaniu to marnotrawstwo zasobów. Wzorzec Singleton gwarantuje, że w ramach jednego żądania HTTP istnieje dokładnie jedno połączenie:
<?php
declare(strict_types=1);
class Database {
private static ?PDO $instance = null;
// Prywatny konstruktor — nie można wywołać new Database()
private function __construct() {}
public static function getInstance(): PDO {
if (self::$instance === null) {
$dsn = 'mysql:host=localhost;dbname=sklep;charset=utf8mb4';
self::$instance = new PDO($dsn, 'app_user', 'tajne_haslo', [
// Błędy rzucane jako wyjątki — łapiesz je try/catch
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
// Wyniki jako tablice asocjacyjne (klucz => wartość)
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// Wyłącz emulację prepared statements — prawdziwa ochrona
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return self::$instance;
}
}
// Użycie w dowolnym miejscu kodu:
$db = Database::getInstance();
// Drugie wywołanie ZWRACA TEN SAM OBIEKT — nie tworzy nowego połączenia
$db2 = Database::getInstance();
var_dump($db === $db2); // trueDlaczego charset=utf8mb4? Obsługuje pełen Unicode (w tym emoji 🎓). Samo utf8 w MySQL to okrojona wersja UTF-8.
Prepared Statements: Ochrona przed SQL Injection
Na Wykładzie 1 mówiliśmy: klientowi nigdy nie ufamy. Oto dlaczego:
<?php
// ❌ NIGDY TAK NIE RÓB — SQL Injection
$id = $_GET['id']; // Użytkownik wpisał: "1; DROP TABLE users; --"
$db->query("SELECT * FROM users WHERE id = $id");
// Efekt: DROP TABLE users — baza danych usunięta!
// ✅ ZAWSZE Prepared Statements
$stmt = $db->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $_GET['id']]);
$user = $stmt->fetch();
// PDO automatycznie "ucieka" niebezpieczne znaki — zapytanie jest bezpiecznePrepared Statements działają w dwóch krokach:
prepare()— wysyła do bazy “szablon” zapytania z “dziurami” (:id)execute()— wypełnia “dziury” danymi, ale baza traktuje je wyłącznie jako dane, nie jako kod SQL
Pełny CRUD (Create, Read, Update, Delete)
<?php
$db = Database::getInstance();
// ═══ CREATE ═══
$stmt = $db->prepare("
INSERT INTO produkty (nazwa, cena, ilosc)
VALUES (:nazwa, :cena, :ilosc)
");
$stmt->execute([
':nazwa' => 'Laptop ThinkPad',
':cena' => 5499.99,
':ilosc' => 10
]);
$noweId = $db->lastInsertId(); // ID wstawionego rekordu
// ═══ READ — jeden rekord ═══
$stmt = $db->prepare("SELECT * FROM produkty WHERE id = :id");
$stmt->execute([':id' => $noweId]);
$produkt = $stmt->fetch(); // Tablica asocjacyjna lub false
// ═══ READ — wiele rekordów z filtrem ═══
$stmt = $db->prepare("
SELECT * FROM produkty
WHERE cena BETWEEN :min AND :max
ORDER BY cena ASC
");
$stmt->execute([':min' => 1000, ':max' => 6000]);
$produkty = $stmt->fetchAll(); // Tablica tablic asocjacyjnych
// ═══ UPDATE ═══
$stmt = $db->prepare("
UPDATE produkty SET cena = :cena, ilosc = :ilosc
WHERE id = :id
");
$stmt->execute([':cena' => 4999.99, ':ilosc' => 8, ':id' => $noweId]);
echo $stmt->rowCount(); // Liczba zmienionych wierszy
// ═══ DELETE ═══
$stmt = $db->prepare("DELETE FROM produkty WHERE id = :id");
$stmt->execute([':id' => $noweId]);5. Klasa bazowa Model — Active Record dla CMS
W systemach CMS (WordPress, Drupal, własne) każda “rzecz” (post, użytkownik, komentarz) to osobna tabela w bazie. Zamiast powtarzać kod CRUD w każdym pliku, tworzymy abstrakcyjną klasę bazową:
<?php
declare(strict_types=1);
abstract class Model {
protected PDO $db;
protected string $table; // Każda podklasa definiuje swoją tabelę
public function __construct() {
$this->db = Database::getInstance();
}
// ─── READ ───
public function findAll(string $orderBy = 'id ASC'): array {
$stmt = $this->db->query(
"SELECT * FROM {$this->table} ORDER BY {$orderBy}"
);
return $stmt->fetchAll();
}
public function findById(int $id): ?array {
$stmt = $this->db->prepare(
"SELECT * FROM {$this->table} WHERE id = :id"
);
$stmt->execute([':id' => $id]);
$result = $stmt->fetch();
return $result ?: null;
}
// ─── CREATE ───
public function create(array $data): int {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$stmt = $this->db->prepare(
"INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})"
);
$stmt->execute($data);
return (int) $this->db->lastInsertId();
}
// ─── UPDATE ───
public function update(int $id, array $data): bool {
$sets = [];
foreach ($data as $key => $value) {
$sets[] = "{$key} = :{$key}";
}
$setString = implode(', ', $sets);
$stmt = $this->db->prepare(
"UPDATE {$this->table} SET {$setString} WHERE id = :id"
);
$data['id'] = $id;
return $stmt->execute($data);
}
// ─── DELETE ───
public function delete(int $id): bool {
$stmt = $this->db->prepare(
"DELETE FROM {$this->table} WHERE id = :id"
);
return $stmt->execute([':id' => $id]);
}
// ─── COUNT ───
public function count(): int {
$stmt = $this->db->query("SELECT COUNT(*) FROM {$this->table}");
return (int) $stmt->fetchColumn();
}
}Teraz tworzenie modeli dla konkretnych tabel jest trywialne:
<?php
class Post extends Model {
protected string $table = 'posts';
// Metody specyficzne dla postów
public function findPublished(): array {
$stmt = $this->db->prepare("
SELECT p.*, u.imie AS autor
FROM {$this->table} p
JOIN users u ON p.autor_id = u.id
WHERE p.status = :status
ORDER BY p.data_publikacji DESC
");
$stmt->execute([':status' => 'published']);
return $stmt->fetchAll();
}
public function findBySlug(string $slug): ?array {
$stmt = $this->db->prepare(
"SELECT * FROM {$this->table} WHERE slug = :slug"
);
$stmt->execute([':slug' => $slug]);
$result = $stmt->fetch();
return $result ?: null;
}
}
class User extends Model {
protected string $table = 'users';
public function findByEmail(string $email): ?array {
$stmt = $this->db->prepare(
"SELECT * FROM {$this->table} WHERE email = :email"
);
$stmt->execute([':email' => $email]);
$result = $stmt->fetch();
return $result ?: null;
}
// ⚠️ Nigdy nie przechowuj haseł w czystym tekście!
public function createWithPassword(string $imie, string $email, string $haslo): int {
return $this->create([
'imie' => $imie,
'email' => $email,
'haslo' => password_hash($haslo, PASSWORD_BCRYPT),
]);
}
public function weryfikujHaslo(string $email, string $haslo): ?array {
$user = $this->findByEmail($email);
if ($user && password_verify($haslo, $user['haslo'])) {
return $user;
}
return null;
}
}
// ═══ Użycie ═══
$postModel = new Post();
$userModel = new User();
// Stwórz użytkownika
$userId = $userModel->createWithPassword('Jan', 'jan@example.com', 'bezpieczneHaslo123');
// Stwórz post
$postId = $postModel->create([
'tytul' => 'Mój pierwszy wpis',
'slug' => 'moj-pierwszy-wpis',
'tresc' => 'Treść artykułu...',
'autor_id' => $userId,
'status' => 'published',
'data_publikacji' => date('Y-m-d H:i:s'),
]);
// Pobierz opublikowane posty
$posty = $postModel->findPublished();6. Architektura MVC: Porządek w kodzie CMS
Kiedy piszesz cały kod w jednym pliku (index.php z SQL-em, HTML-em i logiką pomieszanymi), powstaje tzw. spaghetti code. Przy 500 linijkach nikt tego nie ogarnie.
Wzorzec MVC (Model-View-Controller) dzieli kod na trzy warstwy:
- Model — dane i logika biznesowa (klasy, które właśnie napisaliśmy)
- View — widok, szablony HTML
- Controller — “dyżurny” — odbiera żądanie HTTP, woła odpowiedni Model, przekazuje dane do Widoku
Struktura katalogów prostego CMS
moj-cms/
├── public/ # Jedyny folder widoczny z internetu!
│ ├── index.php # Front Controller — KAŻDE żądanie przechodzi przez ten plik
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── app.js
├── src/
│ ├── Database.php # Klasa połączenia z bazą (Singleton)
│ ├── Model.php # Abstrakcyjna klasa bazowa
│ ├── models/
│ │ ├── Post.php
│ │ └── User.php
│ ├── controllers/
│ │ ├── PostController.php
│ │ └── UserController.php
│ └── Router.php # Prosty router URL → Controller
├── templates/ # Szablony HTML (Widoki)
│ ├── layout.php # Główny szablon (header + footer)
│ ├── home.php
│ ├── post/
│ │ ├── index.php # Lista postów
│ │ └── show.php # Pojedynczy post
│ └── errors/
│ └── 404.php
└── config/
└── database.php # Ustawienia bazy (host, login, hasło)
Kluczowa zasada bezpieczeństwa: Tylko folder public/ jest dostępny z przeglądarki. Pliki w src/, templates/, config/ NIE MOGĄ być dostępne bezpośrednio — zawierają hasła do bazy, logikę biznesową itp.
Front Controller: Jeden punkt wejścia
Zamiast osobnych plików posts.php, users.php, kontakt.php, wszystkie żądania idą przez public/index.php:
<?php
// public/index.php — Front Controller
declare(strict_types=1);
// Autoloader — automatycznie ładuje klasy z src/
spl_autoload_register(function (string $class) {
$path = __DIR__ . '/../src/' . str_replace('\\', '/', $class) . '.php';
if (file_exists($path)) {
require $path;
}
});
// Załaduj konfigurację
require __DIR__ . '/../config/database.php';
// Prosty routing
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
// Wywołaj odpowiedni controller na podstawie URL
$router = new Router();
$router->get('/', 'PostController@index');
$router->get('/post/{slug}','PostController@show');
$router->get('/admin/posts','PostController@adminList');
$router->post('/admin/posts','PostController@store');
$router->dispatch($method, $uri);Prosty Router
<?php
declare(strict_types=1);
class Router {
private array $routes = [];
public function get(string $path, string $action): void {
$this->routes['GET'][$path] = $action;
}
public function post(string $path, string $action): void {
$this->routes['POST'][$path] = $action;
}
public function dispatch(string $method, string $uri): void {
foreach ($this->routes[$method] ?? [] as $route => $action) {
// Zamień {slug} na regex
$pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $route);
$pattern = '#^' . $pattern . '$#';
if (preg_match($pattern, $uri, $matches)) {
[$controllerName, $methodName] = explode('@', $action);
$controller = new $controllerName();
// Zbierz nazwane parametry (np. slug)
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
$controller->$methodName(...$params);
return;
}
}
// 404 — nie znaleziono trasy
http_response_code(404);
require __DIR__ . '/../templates/errors/404.php';
}
}Controller
<?php
declare(strict_types=1);
class PostController {
private Post $postModel;
public function __construct() {
$this->postModel = new Post();
}
// GET / — strona główna z listą postów
public function index(): void {
$posty = $this->postModel->findPublished();
$title = 'Strona główna';
// Przekazujemy dane do widoku
require __DIR__ . '/../templates/layout.php';
}
// GET /post/{slug} — pojedynczy post
public function show(string $slug): void {
$post = $this->postModel->findBySlug($slug);
if (!$post) {
http_response_code(404);
require __DIR__ . '/../templates/errors/404.php';
return;
}
$title = $post['tytul'];
require __DIR__ . '/../templates/layout.php';
}
// POST /admin/posts — zapisz nowy post
public function store(): void {
// Walidacja danych z formularza
$tytul = trim($_POST['tytul'] ?? '');
$tresc = trim($_POST['tresc'] ?? '');
if (empty($tytul) || empty($tresc)) {
// Przekieruj z powrotem z błędem
header('Location: /admin/posts?error=empty');
return;
}
$this->postModel->create([
'tytul' => $tytul,
'slug' => $this->slugify($tytul),
'tresc' => $tresc,
'autor_id' => 1, // TODO: pobrać z sesji
'status' => 'published',
'data_publikacji' => date('Y-m-d H:i:s'),
]);
header('Location: /?success=created');
}
private function slugify(string $text): string {
$text = iconv('UTF-8', 'ASCII//TRANSLIT', $text);
$text = preg_replace('/[^a-zA-Z0-9]/', '-', strtolower($text));
return trim(preg_replace('/-+/', '-', $text), '-');
}
}Widok (Template)
<!-- templates/layout.php -->
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($title) ?> — Mój CMS</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<nav>
<a href="/">Start</a>
<a href="/admin/posts">Panel</a>
</nav>
</header>
<main>
<?php
// Wybierz odpowiedni fragment widoku
$page = $post ?? null;
if ($page) {
require __DIR__ . '/post/show.php';
} else {
require __DIR__ . '/home.php';
}
?>
</main>
<footer>
<p>© <?= date('Y') ?> Mój CMS</p>
</footer>
</body>
</html><!-- templates/home.php -->
<h1>Najnowsze artykuły</h1>
<?php foreach ($posty as $p): ?>
<article>
<h2><a href="/post/<?= htmlspecialchars($p['slug']) ?>">
<?= htmlspecialchars($p['tytul']) ?>
</a></h2>
<time><?= $p['data_publikacji'] ?></time>
<p><?= htmlspecialchars(mb_substr($p['tresc'], 0, 200)) ?>...</p>
</article>
<?php endforeach; ?>Ważne: htmlspecialchars() — funkcja, która zamienia znaki specjalne HTML (<, >, ") na bezpieczne encje (<, >). Chroni przed atakami XSS (Cross-Site Scripting), gdzie złośliwy użytkownik wstrzykuje <script> do treści.
7. Hashowanie haseł: password_hash i password_verify
Nigdy, przenigdy nie przechowuj haseł w czystym tekście. Nawet nie szyfruj ich (szyfrowanie jest odwracalne). Używaj hashowania — funkcji jednokierunkowej:
<?php
// Przy rejestracji:
$haslo_czyste = 'MojeSuperHaslo123';
$haslo_zahashowane = password_hash($haslo_czyste, PASSWORD_BCRYPT);
// Wynik: "$2y$10$xN5Fz..." — nawet administrator bazy nie odczyta hasła
// Przy logowaniu:
$czy_poprawne = password_verify('MojeSuperHaslo123', $haslo_zahashowane);
// true — hasło się zgadza
$czy_poprawne = password_verify('zle_haslo', $haslo_zahashowane);
// falsePASSWORD_BCRYPT automatycznie dodaje losową “sól” (salt) — dwa identyczne hasła dadzą różne hashe. Atakujący nie może użyć gotowych tablic rainbow.
Podsumowanie Wykładu 4
| Temat | Co zapamiętać |
|---|---|
| PHP 8.x | declare(strict_types=1), union types, named args, enum, match |
| OOP | Klasa = przepis, Obiekt = ciasto. public / private / protected |
| Constructor Promotion | public function __construct(private string $nazwa) |
| PDO Singleton | Jedno połączenie na żądanie, ERRMODE_EXCEPTION, EMULATE_PREPARES => false |
| Prepared Statements | ZAWSZE :parametry + execute(). NIGDY zmienne w SQL |
| Model abstrakcyjny | Bazowy CRUD, podklasy definiują $table i metody specyficzne |
| MVC | Model (dane) → Controller (logika) → View (HTML). Front Controller + Router |
| Bezpieczeństwo | htmlspecialchars() vs XSS, password_hash() vs plaintext |
Zadanie do przećwiczenia: Zbuduj prosty blog z panelem admina. Struktura MVC, dwa modele (Post, User), logowanie przez password_verify, lista postów na stronie głównej, formularz dodawania nowego postu. Bonus: zamknij wszystko w kontenerze Docker z Wykładu 2.