INF.03 · Moduł 46 · Klasa 4 TI

PHP + PDO + MySQL

// Komunikacja serwera aplikacji z bazą danych

new PDO() query() prepare() execute() fetch() SQL Injection
📘

Teoria — kluczowe pojęcia

MySQL / MariaDB

Relacyjna baza danych (RDBMS). Przechowuje dane w tabelach powiązanych kluczami obcymi. Dostępna w XAMPP jako MariaDB.

SQL

Język komunikacji z bazą. SELECT, INSERT, UPDATE, DELETE — przez PHP wysyłamy te zapytania do bazy i odbieramy wyniki.

PDO

PHP Data Objects — nowoczesny, bezpieczny interfejs do baz danych. Zalecany zamiast starszego mysqli_*.

Prepared Statements

Przygotowane zapytania z placeholderami (? lub :nazwa). Chronią przed SQL Injection.

🔗

Przepływ danych: PHP → PDO → MySQL

WARSTWA
PHP
INTERFEJS
PDO
JĘZYK
SQL
SERWER
MariaDB

PHP wysyła zapytanie SQL przez PDO → MariaDB przetwarza → zwraca wynik jako tablicę PHP

🗄️

Baza danych GameVault — inicjalizacja i struktura

Zanim zaczniesz pisać PHP, musisz mieć działającą bazę danych. Przejdź przez wszystkie 4 kroki poniżej — od uruchomienia XAMPP aż do gotowej bazy z danymi testowymi.

KROK PO KROKU — XAMPP + phpMyAdmin
01
URUCHOM XAMPP

Otwórz XAMPP Control Panel. Kliknij Start przy Apache i MySQL. Oba muszą świecić na zielono.

02
OTWÓRZ phpMyAdmin

W przeglądarce wejdź na http://localhost/phpmyadmin. Zaloguj się: użytkownik root, hasło puste.

03
UTWÓRZ BAZĘ

Kliknij Nowa w lewym panelu. Wpisz nazwę gamevault, kodowanie utf8mb4_unicode_ci, kliknij Utwórz.

04
WKLEJ SKRYPT SQL

Wybierz bazę gamevault, kliknij zakładkę SQL, wklej skrypt z sekcji poniżej i kliknij Wykonaj.

SCHEMAT RELACJI (ERD)
🧑 gracze
🔑 gracz_id INT PK AI
nick VARCHAR(50) UQ
email VARCHAR(100)
kraj VARCHAR(3)
1 gracz
─────
1 : wiele
🛒 zakupy
🔑 zakup_id INT PK AI
🔗 gracz_id FK → gracze
🔗 gra_id FK → gry
data_zakupu DATE
cena_zakupu DECIMAL(7,2)
tabela łącząca
─────
wiele : 1
🎮 gry
🔑 gra_id INT PK AI
tytul VARCHAR(100)
gatunek VARCHAR(50)
cena DECIMAL(7,2)
wydawca_id FK → wydawcy
1 gra
🔑 PK = klucz główny (PRIMARY KEY)
🔗 FK = klucz obcy (FOREIGN KEY)
AI = AUTO_INCREMENT   UQ = UNIQUE
OPIS TABEL I RELACJI
🧑 gracze

Przechowuje konta graczy. gracz_id to klucz główny — unikalny identyfikator każdego gracza. nick jest UNIQUE — dwóch graczy nie może mieć tego samego nicku.

🎮 gry

Katalog gier w sklepie. Każda gra ma swój gra_id. gatunek pozwala filtrować gry (RPG, FPS, Puzzle...). wydawca_id to klucz obcy do tabeli wydawcy.

🛒 zakupy

Tabela łącząca graczy z grami — realizuje relację wiele-do-wielu. Jeden gracz może kupić wiele gier, jedna gra może być kupiona przez wielu graczy. Przechowuje też historyczną cenę zakupu.

SKRYPT SQL — WKLEJ DO phpMyAdmin → zakładka SQL
SQL · gamevault_init.sql
-- ═══════════════════════════════════════════════════
-- GAMEVAULT — inicjalizacja bazy danych
-- Wklej CAŁY ten kod do phpMyAdmin → zakładka SQL
-- ═══════════════════════════════════════════════════

-- Wybierz bazę (musi być wcześniej utworzona w phpMyAdmin)
USE gamevault;

-- ─── Czyszczenie (na wypadek ponownego uruchomienia skryptu) ───
DROP TABLE IF EXISTS zakupy;    -- najpierw tabele podrzędne (mają FK)
DROP TABLE IF EXISTS gry;
DROP TABLE IF EXISTS gracze;
DROP TABLE IF EXISTS wydawcy;

-- ─── Tabela: wydawcy ───────────────────────────────────────────
-- Przechowuje firmy wydające gry (CD Projekt, Valve itd.)
-- wydawca_id: klucz główny, AUTO_INCREMENT = baza nadaje ID sama
CREATE TABLE wydawcy (
    wydawca_id INT          PRIMARY KEY AUTO_INCREMENT,
    nazwa      VARCHAR(100) NOT NULL,
    kraj       VARCHAR(3)
);

-- ─── Tabela: gracze ────────────────────────────────────────────
-- Konta użytkowników sklepu GameVault
-- nick: UNIQUE — nie można mieć dwóch graczy z tym samym nickiem
CREATE TABLE gracze (
    gracz_id INT          PRIMARY KEY AUTO_INCREMENT,
    nick     VARCHAR(50)  NOT NULL UNIQUE,
    email    VARCHAR(100),
    kraj     VARCHAR(3)
);

-- ─── Tabela: gry ───────────────────────────────────────────────
-- Katalog gier w sklepie
-- wydawca_id: FOREIGN KEY — musi istnieć w tabeli wydawcy
-- CHECK: cena nie może być ujemna
CREATE TABLE gry (
    gra_id     INT           PRIMARY KEY AUTO_INCREMENT,
    tytul      VARCHAR(100)  NOT NULL,
    gatunek    VARCHAR(50),
    cena       DECIMAL(7,2)  NOT NULL CHECK (cena >= 0),
    wydawca_id INT,
    FOREIGN KEY (wydawca_id) REFERENCES wydawcy(wydawca_id)
);

-- ─── Tabela: zakupy ────────────────────────────────────────────
-- Tabela łącząca gracze ↔ gry (relacja wiele-do-wielu)
-- Jeden gracz może kupić wiele gier, jedna gra — wielu graczy
-- ON DELETE CASCADE: usunięcie gracza usuwa też jego zakupy
CREATE TABLE zakupy (
    zakup_id    INT         PRIMARY KEY AUTO_INCREMENT,
    gracz_id    INT         NOT NULL,
    gra_id      INT         NOT NULL,
    data_zakupu DATE        DEFAULT (CURRENT_DATE),
    cena_zakupu DECIMAL(7,2) NOT NULL,
    FOREIGN KEY (gracz_id) REFERENCES gracze(gracz_id) ON DELETE CASCADE,
    FOREIGN KEY (gra_id)   REFERENCES gry(gra_id)
);

-- ─── Dane testowe ──────────────────────────────────────────────
INSERT INTO wydawcy (nazwa, kraj) VALUES
    ('CD Projekt',  'PL'),
    ('Valve',        'US'),
    ('IndieSoft',    'DE');

INSERT INTO gry (tytul, gatunek, cena, wydawca_id) VALUES
    ('Cyberpunk 2077',  'RPG',    199.99, 1),
    ('Half-Life: Alyx', 'FPS',    149.99, 2),
    ('Portal 2',        'Puzzle',  39.99, 2),
    ('PixelDungeon',    'RPG',     19.99, 3),
    ('SpeedRacer VR',   'Racing',  89.99, 3);

INSERT INTO gracze (nick, email, kraj) VALUES
    ('ShadowWolf',  'wolf@gv.pl',  'PL'),
    ('NeonByte',    'neon@gv.us',  'US'),
    ('PixelQueen',  'queen@gv.de', 'DE'),
    ('NoobMaster',  'noob@gv.pl',  'PL');   -- ten gracz nie ma zakupów

INSERT INTO zakupy (gracz_id, gra_id, data_zakupu, cena_zakupu) VALUES
    (1, 1, '2024-11-01', 199.99),   -- ShadowWolf → Cyberpunk
    (1, 3, '2024-11-15',  39.99),   -- ShadowWolf → Portal 2
    (2, 2, '2024-12-01', 149.99),   -- NeonByte   → Half-Life
    (3, 4, '2025-01-10',  19.99),   -- PixelQueen → PixelDungeon
    (3, 1, '2025-01-20', 199.99);  -- PixelQueen → Cyberpunk

-- ─── Weryfikacja (opcjonalne — sprawdź czy dane są poprawne) ───
SELECT * FROM gracze;
SELECT * FROM gry;
SELECT * FROM zakupy;
💡 Po wklejeniu skryptu kliknij Wykonaj w phpMyAdmin. W lewym panelu pojawią się tabele: gracze, gry, zakupy, wydawcy.
⌨️

Ćwiczenia krok po kroku

01
Nawiązanie połączenia PDO z bazą danych
new PDO()
ZADANIE Utwórz plik polaczenie.php w folderze XAMPP (htdocs/gamevault/). Nawiąż połączenie z bazą gamevault przez PDO i wyświetl potwierdzenie.
PHP · polaczenie.php
<?php
// Dane połączeniowe — XAMPP domyślnie
$host    = 'localhost';
$baza    = 'gamevault';
$user    = 'root';
$haslo   = '';         // XAMPP: domyślnie puste
$charset = 'utf8mb4';

// DSN = Data Source Name
$dsn = "mysql:host=$host;dbname=$baza;charset=$charset";

// Opcje PDO — tryb błędów i format wyników
$opcje = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
    $pdo = new PDO($dsn, $user, $haslo, $opcje);
    echo "✅ Połączono z bazą gamevault!";

} catch (PDOException $e) {
    echo "❌ Błąd: " . $e->getMessage();
}
?>
💡 Zapisz plik w C:\xampp\htdocs\gamevault\polaczenie.php i otwórz http://localhost/gamevault/polaczenie.php w przeglądarce.
🔧 Wskazówka: plik polaczenie.php możesz dołączać do innych plików przez require_once 'polaczenie.php'; — wtedy $pdo będzie dostępne w każdym pliku.
02
Proste zapytanie SELECT — lista graczy
query() · fetch()
ZADANIE Pobierz wszystkich graczy z bazy i wyświetl ich nick oraz kraj jako listę HTML. Użyj metody query() i pętli while(fetch()).
PHP · gracze.php
<?php
require_once 'polaczenie.php'; // $pdo gotowy do użycia

// Zapytanie SQL — pobieramy wszystkich graczy
$stmt = $pdo->query('SELECT nick, kraj FROM gracze ORDER BY nick');

echo "<h2>🎮 Lista graczy GameVault</h2>";
echo "<ul>";

// Pobieramy wiersz po wierszu — każdy $row to tablica asocjacyjna
while ($row = $stmt->fetch()) {
    echo "<li>{$row['nick']} ({$row['kraj']})</li>";
}

echo "</ul>";
?>
💡 FETCH_ASSOC (ustawione w połączeniu) sprawia, że $row['nick'] działa — klucze to nazwy kolumn SQL.
03
Prepared Statement — bezpieczne zapytanie z parametrem
prepare() · execute()
ZADANIE Pobierz gry z wybranego gatunku przekazanego jako parametr URL (?gatunek=RPG). Użyj Prepared Statement żeby zabezpieczyć zapytanie przed SQL Injection.
🛡️ NIGDY nie wstawiaj danych od użytkownika bezpośrednio do zapytania! "SELECT * WHERE gatunek='" . $_GET['g'] . "'" — to SQL Injection!
PHP · gry_gatunku.php
<?php
require_once 'polaczenie.php';

// Pobieramy parametr z URL: ?gatunek=RPG
// Domyślna wartość 'RPG' jeśli parametru nie ma
$gatunek = $_GET['gatunek'] ?? 'RPG';

// Krok 1: PRZYGOTUJ zapytanie z placeholderem ?
$stmt = $pdo->prepare(
    'SELECT tytul, cena FROM gry WHERE gatunek = ? ORDER BY cena DESC'
);

// Krok 2: WYKONAJ i przekaż wartość — PDO samo zabezpieczy dane
$stmt->execute([$gatunek]);

// Krok 3: POBIERZ wyniki
$gry = $stmt->fetchAll();

echo "<h2>Gry z gatunku: {$gatunek}</h2>";

if (empty($gry)) {
    echo "<p>Brak gier w tym gatunku.</p>";
} else {
    echo "<ul>";
    foreach ($gry as $gra) {
        echo "<li>{$gra['tytul']} — {$gra['cena']} zł</li>";
    }
    echo "</ul>";
}
?>
💡 Adres testowy: http://localhost/gamevault/gry_gatunku.php?gatunek=RPG
Zmień gatunek na FPS, Puzzle itp.
04
JOIN w PHP — zakupy graczy z tytułami gier
JOIN · fetchAll()
ZADANIE Pobierz listę zakupów — dla każdego zakupu wyświetl nick gracza, tytuł gry i datę zakupu. Użyj JOIN łączącego trzy tabele.
PHP · zakupy.php
<?php
require_once 'polaczenie.php';

// Zapytanie SQL z JOIN — łączymy 3 tabele
$sql = '
    SELECT g.nick          AS gracz,
           gr.tytul         AS gra,
           z.data_zakupu    AS data,
           z.cena_zakupu    AS cena
    FROM   gracze  g
    JOIN   zakupy  z  ON g.gracz_id = z.gracz_id
    JOIN   gry     gr ON z.gra_id   = gr.gra_id
    ORDER BY z.data_zakupu DESC
';

$stmt    = $pdo->query($sql);
$zakupy = $stmt->fetchAll();   // pobiera WSZYSTKIE wiersze naraz

echo '<table border="1" cellpadding="8">';
echo '<tr><th>Gracz</th><th>Gra</th><th>Data</th><th>Cena</th></tr>';

foreach ($zakupy as $r) {
    echo "<tr>
        <td>{$r['gracz']}</td>
        <td>{$r['gra']}</td>
        <td>{$r['data']}</td>
        <td>{$r['cena']} zł</td>
    </tr>";
}

echo '</table>';
?>
💡 fetchAll() pobiera wszystkie wiersze na raz jako tablicę tablic — wygodne gdy chcesz liczyć wiersze lub wielokrotnie iterować po danych.
05
INSERT przez PHP — dodawanie nowego gracza
prepare() · INSERT
ZADANIE Dodaj nowego gracza do bazy przez PHP używając Prepared Statement z nazwanymi parametrami (:nazwa). Po dodaniu pobierz jego ID.
PHP · dodaj_gracza.php
<?php
require_once 'polaczenie.php';

// Dane nowego gracza
$nowyGracz = [
    'nick'  => 'MegaBot',
    'email' => 'bot@gamevault.pl',
    'kraj'  => 'PL',
];

// Przygotowanie zapytania z nazwanymi parametrami :nick :email :kraj
$stmt = $pdo->prepare(
    'INSERT INTO gracze (nick, email, kraj) VALUES (:nick, :email, :kraj)'
);

// Wykonanie — tablica kluczy musi pasować do :parametrów
$stmt->execute($nowyGracz);

// Pobranie ID ostatnio wstawionego rekordu
$noweId = $pdo->lastInsertId();
echo "✅ Dodano gracza! ID = {$noweId}";
?>
💡 Nazwane parametry :nick są czytelniejsze niż znaki zapytania ? — szczególnie przy wielu kolumnach.
06
rowCount() i obsługa błędów try/catch
rowCount() · try/catch
ZADANIE Zaktualizuj ceny gier RPG o 10% i sprawdź ile rekordów zostało zmienionych. Opakuj operację w try/catch żeby przechwycić ewentualny błąd.
PHP · aktualizuj_ceny.php
<?php
require_once 'polaczenie.php';

try {
    // Przygotowanie zapytania UPDATE z parametrem gatunku
    $stmt = $pdo->prepare(
        'UPDATE gry SET cena = cena * 1.10 WHERE gatunek = ?'
    );

    $stmt->execute(['RPG']);

    // rowCount() — liczba zmodyfikowanych wierszy
    $ile = $stmt->rowCount();
    echo "✅ Zaktualizowano {$ile} gier RPG (+10% ceny)";

} catch (PDOException $e) {
    // W razie błędu — wyświetl komunikat (nie pokazuj w produkcji!)
    echo "❌ Błąd zapytania: " . $e->getMessage();
}
?>
💡 rowCount() zwraca liczbę wierszy dotkniętych przez UPDATE lub DELETE. Dla SELECT — użyj count($stmt->fetchAll()).
🚀

Zadanie projektowe

● INF.03 · PROJEKT KOŃCOWY

Mini-aplikacja PHP — panel GameVault

Utwórz folder htdocs/gamevault/ i stwórz pliki PHP realizujące poniższe funkcje. Każdy plik ma mieć obsługę błędów (try/catch).

Każdy plik otwieraj przez http://localhost/gamevault/nazwa.php

🧠

Quiz — sprawdź wiedzę

Q1Dlaczego PDO jest zalecane zamiast starszego mysqli_*?
APDO jest szybsze i zużywa mniej pamięci RAM
BPDO obsługuje wiele baz danych (MySQL, PostgreSQL, SQLite...) i oferuje Prepared Statements z lepszą obsługą błędów
CPDO automatycznie tworzy bazy danych i tabele
DPDO nie wymaga znajomości SQL
Q2Czym jest SQL Injection i jak PDO przed nim chroni?
AAtak polegający na przeciążeniu serwera — PDO limituje liczbę zapytań
BWstrzyknięcie złośliwego kodu SQL przez dane wejściowe — PDO chroni przez Prepared Statements, które oddzielają kod SQL od danych
CBłąd składni SQL — PDO automatycznie go poprawia
DKradzież pliku .php — PDO szyfruje kod PHP
Q3Jaka jest różnica między fetch() a fetchAll()?
Afetch() pobiera wszystkie wiersze, fetchAll() tylko pierwszy
Bfetch() pobiera jeden wiersz przy każdym wywołaniu (używane w pętli while), fetchAll() pobiera wszystkie wiersze naraz jako tablicę
CfetchAll() jest szybszy od fetch() przy małych zbiorach danych
DSą identyczne — fetchAll() to alias fetch()
Q4Do czego służy metoda lastInsertId()?
AZwraca liczbę wierszy w tabeli
BZwraca ID ostatnio wstawionego rekordu (po INSERT z AUTO_INCREMENT)
CZwraca ostatnie zapytanie SQL jako tekst
DSprawdza czy INSERT się powiódł
Q5Co to jest DSN w kontekście PDO?
ASerwer DNS do rozwiązywania nazw domenowych bazy danych
BData Source Name — ciąg znaków opisujący jak połączyć się z bazą (typ, host, nazwa bazy, kodowanie)
CNazwa użytkownika bazy danych
DHasło zakodowane w base64
0/5

Checklista umiejętności

Postęp0 / 8
Tworzę połączenie PDO z bazą danych
Wykonuję zapytania SELECT przez query()
Iteruję wyniki przez while(fetch())
Używam Prepared Statements (prepare + execute)
Zabezpieczam dane z $_GET/$_POST przed SQL Injection
Wykonuję INSERT/UPDATE przez PDO z parametrami
Obsługuję błędy przez try/catch PDOException
Piszę zapytania JOIN w PHP i wyświetlam wyniki jako HTML