Szybki quiz – przypomnienie poprzedniej lekcji
Lekcja 17: bazy danych, mysqli i PDO, projekt tabel TaskQuest. Odpowiedz z pamięci.
Masz projekt, który być może zostanie przeniesiony z MySQL na PostgreSQL. Które rozszerzenie wybierzesz i dlaczego?
Co w tej definicji jest niepoprawne i jaki będzie skutek?
CREATE TABLE zadania (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tytul VARCHAR(100) NOT NULL,
xp INT UNSIGNED NOT NULL DEFAULT 0,
) ENGINE=InnoDB;Kolumna
loginmaNOT NULL UNIQUE. Które z tych operacji baza odrzuci?
a) dodanie użytkownika bez podania loginu,
b) dodanie drugiego użytkownika o loginieOla_TQ,
c) dodanie użytkownika z loginemola_tq, gdy istniejeOla_TQ.Dlaczego kolumna
uzytkownik_idjest w tabelizadania, a nie kolumnazadania_idw tabeliuzytkownicy?Uczeń chce dodać do tabeli
uzytkownicykolumnępoziom, bo „łatwiej będzie ją wyświetlić”. Co mu odpowiesz i na jaką zasadę się powołasz?Dwa z tych wierszy
INSERTnie zapiszą tego, czego oczekuje autor. Które i dlaczego?INSERT INTO uzytkownicy (login, imie, xp, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 99, '2026-09-14'),
('Ola_TQ', 'Ola', '250', 'NULL'),
('Bartek_9', 'Bartek', 0, '01.09.2026');Baza ma kodowanie
utf8, a nieutf8mb4. Kiedy zobaczysz problem, a kiedy nie?
Odpowiedzi
- PDO — jako jedyne daje ten sam interfejs dla MySQL, PostgreSQL, SQLite i innych silników, więc zmiana bazy nie wymaga przepisywania kodu.
mysqlidziała wyłącznie z MySQL/MariaDB. - Przecinek po ostatniej kolumnie (
DEFAULT 0,) — błąd składni#1064, tabela w ogóle nie powstanie. - a) i b). Przypadek c) przejdzie lub nie w zależności od porównywania wielkości liter przez zestaw znaków, ale na domyślnym
utf8mb4_unicode_ciwielkość liter nie jest rozróżniana, więc baza też go odrzuci. - Bo w relacji 1:N klucz obcy trafia zawsze po stronie „N”. Jedno pole mieści jedną wartość — nie zapiszesz w nim listy „zadania 1, 2, 3”.
- Nie dodajemy, bo poziom da się obliczyć z XP. Zasada: w bazie trzymamy tylko dane, których nie da się wyliczyć z innych danych — inaczej przy
xp = 350, poziom = 2nie wiadomo, która wartość kłamie. Poziom liczyobliczPoziom()/ metoda klasyUzytkownik. - Wiersz Oli:
'NULL'w apostrofach zapisze tekst „NULL”, a nie brak wartości ('250'zadziała, bo baza przekonwertuje tekst na liczbę, ale to zła praktyka). Wiersz Bartka: data'01.09.2026'— baza przyjmuje wyłącznie formatRRRR-MM-DD. - Polskie litery zapiszą się poprawnie, bo mieszczą się w
utf8. Problem pojawi się dopiero przy emoji (4-bajtowe znaki) — wtedy błąd zapisu albo obcięte dane.
Bazy danych to fundament większości aplikacji webowych. Za każdym razem gdy:
- Przeglądasz posty na Instagramie
- Sprawdzasz ranking w grze online
- Szukasz wiadomości na Discordzie
- Oglądasz playlistę na Spotify
W tym materiale nauczysz się pobierać dane z bazy MySQL/MariaDB używając PHP. To podstawowa umiejętność, którą musisz opanować przed nauką modyfikowania danych (INSERT, UPDATE, DELETE – te operacje poznasz w następnym temacie).
Dane dostępowe do bazy
Niezależnie od wybranej metody, potrzebujesz 4 podstawowych informacji:
$serwer = 'localhost'; // Adres serwera (localhost lub 127.0.0.1)
$user = 'root'; // Nazwa użytkownika bazy
$pass = ''; // Hasło (domyślnie puste w XAMPP)
$baza = 'nazwa_bazy'; // Nazwa Twojej bazy danych
UWAGA: W środowisku produkcyjnym (na prawdziwym serwerze):
- Hasło NIE MOŻE być puste!
- Użytkownik NIE POWINIEN być
root - Dane logowania przechowuj w oddzielnym pliku (np.
config.php) - Nigdy nie commituj danych dostępowych do GitHuba!
Przykład pliku config.php:
<?php
// config.php - NIE COMMITUJ tego pliku do repozytorium!
define('DB_HOST', 'localhost');
define('DB_USER', 'twoj_user');
define('DB_PASS', 'twoje_silne_haslo');
define('DB_NAME', 'nazwa_bazy');
?>
Użycie w kodzie PDO:
<?php
require_once 'config.php';
... łączenie z bazą ...
?>
Przygotowanie – Utworzenie przykładowej bazy
Przed rozpoczęciem nauki potrzebujesz bazy z przykładowymi danymi. Stworzymy bazę gaming_hub z tabelą graczy – będzie bardziej interesująca niż klasyczne „imię i nazwisko”
Krok 1: Otwórz phpMyAdmin
W przeglądarce wejdź na:
http://localhost/phpmyadmin
Krok 2: Utwórz nową bazę danych
- Kliknij zakładkę „Bazy danych”
- W polu „Utwórz bazę danych” wpisz:
gaming_hub - Wybierz kodowanie: utf8mb4_unicode_ci (poprawniejsze sortowanie wielu języków, lepsza zgodność Unicode, obsługa polskich znaków;
mb4wutf8mb4oznacza „maximum bytes 4” — czyli kodowanie UTF-8 obsługujące znaki zajmujące do 4 bajtów.) - Kliknij „Utwórz”
Krok 3: Utwórz tabelę z danymi
Kliknij na bazę gaming_hub, przejdź do zakładki SQL i wykonaj poniższy kod:
-- Tabela graczy
CREATE TABLE players (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100),
level INT DEFAULT 1,
coins INT DEFAULT 0,
rank_name VARCHAR(20) DEFAULT 'Bronze',
games_played INT DEFAULT 0,
wins INT DEFAULT 0,
losses INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Przykładowe dane graczy (15 rekordów)
INSERT INTO players (username, email, level, coins, rank_name, games_played, wins, losses, last_login) VALUES
('ProGamer_Mike', 'mike@example.com', 45, 12500, 'Diamond', 850, 520, 330, NOW()),
('xX_Anna_Xx', 'anna@example.com', 32, 8900, 'Platinum', 620, 380, 240, NOW()),
('NinjaKiller', 'ninja@example.com', 67, 23400, 'Master', 1240, 890, 350, NOW()),
('ShadowHunter', 'shadow@example.com', 28, 6700, 'Gold', 490, 270, 220, DATE_SUB(NOW(), INTERVAL 2 DAY)),
('DragonSlayer99', 'dragon@example.com', 19, 4200, 'Silver', 320, 160, 160, DATE_SUB(NOW(), INTERVAL 5 DAY)),
('CoolGamer', 'cool@example.com', 52, 15600, 'Diamond', 980, 610, 370, NOW()),
('LegendaryPlayer', 'legend@example.com', 89, 45000, 'Grandmaster', 2100, 1560, 540, NOW()),
('NoobMaster', 'noob@example.com', 8, 1200, 'Bronze', 85, 30, 55, DATE_SUB(NOW(), INTERVAL 10 DAY)),
('FastFingers', 'fast@example.com', 41, 11200, 'Platinum', 740, 450, 290, NOW()),
('SilentAssassin', 'silent@example.com', 36, 9800, 'Platinum', 680, 410, 270, DATE_SUB(NOW(), INTERVAL 1 DAY)),
('ThunderStrike', 'thunder@example.com', 24, 5600, 'Gold', 410, 220, 190, DATE_SUB(NOW(), INTERVAL 3 DAY)),
('IceQueen', 'ice@example.com', 55, 17800, 'Diamond', 1050, 670, 380, NOW()),
('FireStorm', 'fire@example.com', 15, 3100, 'Silver', 250, 120, 130, DATE_SUB(NOW(), INTERVAL 7 DAY)),
('MysticWarrior', 'mystic@example.com', 63, 21000, 'Master', 1180, 800, 380, NOW()),
('SpeedDemon', 'speed@example.com', 29, 7200, 'Gold', 520, 290, 230, DATE_SUB(NOW(), INTERVAL 4 DAY));
Struktura tabeli players:
| Kolumna | Typ | Opis |
|---|---|---|
id | INT | Unikalny identyfikator (AUTO_INCREMENT) |
username | VARCHAR(50) | Nick gracza (unikalny) |
email | VARCHAR(100) | Adres email |
level | INT | Poziom gracza |
coins | INT | Wirtualna waluta |
rank_name | VARCHAR(20) | Ranga (Bronze, Silver, Gold, Platinum, Diamond, Master, Grandmaster) |
games_played | INT | Liczba rozegranych gier |
wins | INT | Liczba wygranych |
losses | INT | Liczba przegranych |
created_at | TIMESTAMP | Data utworzenia konta |
last_login | TIMESTAMP | Ostatnie logowanie |
Sposoby połączenia z bazą danych
PHP oferuje 2 nowoczesne sposoby komunikacji z MySQL, podejście proceduralne pomijamy jako przestarzałe.
1. MySQLi – Podejście obiektowe
Wykorzystujemy klasę mysqli.
$db = new mysqli($serwer, $user, $pass, $baza);
Podejście dobre, ale tylko dla MySQL/MariaDB
2. PDO – PHP Data Objects (ZALECANE!)
$pdo = new PDO("mysql:host=$serwer;dbname=$baza;charset=utf8mb4", $user, $pass);
Podejście uniwersalne, bezpieczne, nowoczesne
Łączenie z bazą danych
MySQLi – Podejście obiektowe
<?php
// Dane dostępowe
$serwer = 'localhost';
$user = 'root';
$pass = '';
$baza = 'gaming_hub';
// Wyłączamy automatyczne raportowanie błędów
mysqli_report(MYSQLI_REPORT_OFF);
// Połączenie obiektowe (@ ukrywa ostrzeżenia)
$db = @new mysqli($serwer, $user, $pass, $baza);
// Sprawdzenie połączenia
if ($db->connect_error) {
die("Błąd połączenia: {$db->connect_error}");
}
echo 'Połączenie nawiązano!<br>';
// Ustawienie kodowania (ważne dla polskich znaków i emoji!)
$db->set_charset('utf8mb4');
// ... wykonywanie zapytań ...
// Zamknięcie połączenia
$db->close();
?>
Dostępne właściwości do obsługi błędów:
$db->connect_error // Opis błędu połączenia
$db->connect_errno // Kod błędu połączenia
$db->error // Opis ostatniego błędu
$db->errno // Kod ostatniego błędu
PDO – PHP Data Objects
<?php
// Dane dostępowe
$serwer = 'localhost';
$user = 'root';
$pass = '';
$baza = 'gaming_hub';
// Opcje PDO dla bezpieczeństwa i wygody
$opcje = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // Włącz wyjątki
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // Domyślnie tablica asocjacyjna
PDO::ATTR_EMULATE_PREPARES => false, // Prawdziwe prepared statements
];
try {
// Połączenie PDO
$pdo = new PDO(
"mysql:host=$serwer;dbname=$baza;charset=utf8mb4",
$user,
$pass,
$opcje
);
echo 'Połączenie nawiązano!<br>';
// ... wykonywanie zapytań ...
} catch (PDOException $e) {
// W prawdziwej aplikacji NIE pokazuj szczegółów błędu użytkownikowi!
error_log("DB Error: {$e->getMessage()}");
die('Nie można połączyć się z bazą danych. Spróbuj później.');
}
// Zamknięcie połączenia
$pdo = null;
?>
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION– rzuca wyjątki przy błędach (łatwiej debugować)PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC– zwraca tablice asocjacyjne (czytelniejsze)PDO::ATTR_EMULATE_PREPARES => false– prawdziwe prepared statements (bezpieczniejsze)
Zamykanie połączenia z bazą
Operacja zamknięcia połączenia jest automatycznie wykonywana po zakończeniu skryptu. Jeżeli potrzebujemy zakończyć połączenie wcześniej:
MySQLi obiektowo:
$db->close();
PDO:
$pdo = null;
// lub
unset($pdo);
W praktyce: Rzadko musisz ręcznie zamykać połączenie – PHP robi to automatycznie po zakończeniu skryptu, ale na egzaminie zawodowym jest to wymagane.
SELECT – Pobieranie wszystkich rekordów
UWAGA: stosowanie query() nie zapewnia bezpieczeństwa w przypadku ataków SQJ Injection. Zamiast query należy używać parameterized prepared statements (opracowanie dalej w temacie). Aby query() było bezpieczne należy stosować mysqli_real_escape_string().
Użycie samego query() jest dopuszczalne na egzaminie zawodowym, jednak w prawdziwych projektach ZAWSZE używaj prepared statements.
Przykład 1: MySQLi obiektowo – PODATNE NA ATAKI! – tak nie rób
<?php
// Połączenie z bazą
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die("Błąd połączenia: {$db->connect_error}");
}
$db->set_charset('utf8mb4');
echo 'Połączenie nawiązano<br><br>';
// Zapytanie SQL - pobierz wszystkich graczy
$sql = "SELECT * FROM players";
$wynik = $db->query($sql);
// Sprawdzamy liczbę rekordów
$ile_wierszy = $wynik->num_rows;
echo "Znaleziono graczy: <strong>{$ile_wierszy}</strong><br><br>";
// Wyświetlamy w tabeli
echo '<table border="1" cellpadding="10" cellspacing="0">';
echo '<tr>
<th>ID</th>
<th>Username</th>
<th>Level</th>
<th>Coins</th>
<th>Rank</th>
<th>Wins/Losses</th>
</tr>';
// Pętla while - pobieramy kolejne rekordy
while ($wiersz = $wynik->fetch_assoc()) {
$username = htmlspecialchars($wiersz['username']);
$coins = number_format($wiersz['coins']);
echo "<tr>";
echo "<td>{$wiersz['id']}</td>";
echo "<td>{$username}</td>";
echo "<td>{$wiersz['level']}</td>";
echo "<td>{$coins}</td>";
echo "<td>{$wiersz['rank_name']}</td>";
echo "<td>{$wiersz['wins']}/{$wiersz['losses']}</td>";
echo "</tr>";
}
echo '</table>';
// Zamknięcie połączenia
$db->close();
?>
Alternatywnie – pętla foreach
<?php
// ... (połączenie jak wyżej)
$sql = "SELECT * FROM players";
$wynik = $db->query($sql);
echo "Znaleziono graczy: {$wynik->num_rows}<br><br>";
foreach ($wynik as $row) {
$username = htmlspecialchars($row['username']);
echo "{$row['id']} - {$username} (Level: {$row['level']})<br>";
}
$db->close();
?>
Przykład 1: PDO – PODATNE NA ATAKI! – tak nie rób
<?php
try {
// Połączenie PDO
$pdo = new PDO(
'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
echo 'Połączenie nawiązano<br><br>';
// Zapytanie SQL
$sql = "SELECT * FROM players";
$stmt = $pdo->query($sql);
// Pobranie wszystkich wyników
$wyniki = $stmt->fetchAll();
$liczba_graczy = count($wyniki);
echo "Znaleziono graczy: <strong>{$liczba_graczy}</strong><br><br>";
// Wyświetlanie w tabeli HTML
echo '<table border="1" cellpadding="10" cellspacing="0">';
echo '<tr>
<th>ID</th>
<th>Username</th>
<th>Level</th>
<th>Coins</th>
<th>Rank</th>
<th>Games</th>
<th>Win Rate</th>
</tr>';
foreach ($wyniki as $wiersz) {
// Obliczamy Win Rate (procent wygranych)
$winRate = $wiersz['games_played'] > 0
? round(($wiersz['wins'] / $wiersz['games_played']) * 100, 1)
: 0;
$username = htmlspecialchars($wiersz['username']);
$coins = number_format($wiersz['coins']);
echo "<tr>";
echo "<td>{$wiersz['id']}</td>";
echo "<td>{$username}</td>";
echo "<td>{$wiersz['level']}</td>";
echo "<td>{$coins}</td>";
echo "<td>{$wiersz['rank_name']}</td>";
echo "<td>{$wiersz['games_played']}</td>";
echo "<td>{$winRate}%</td>";
echo "</tr>";
}
echo '</table>';
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
// Zamknięcie połączenia
$pdo = null;
?>
BEZPIECZEŃSTWO – SQL Injection
SQL Injection to technika ataku, w której hacker wstrzykuje złośliwy kod SQL przez dane wejściowe (formularze, URL). To najczęstszy typ ataku na aplikacje webowe!
NIEBEZPIECZNY KOD (NIGDY TAK NIE RÓB!)
<?php
// PODATNE NA SQL INJECTION!
$username = $_POST['username'];
// Niebezpieczne zapytanie - łączenie stringów!
$sql = "SELECT * FROM players WHERE username = '$username'";
$wynik = $db->query($sql);
/*
PROBLEM: Użytkownik może wpisać złośliwy kod!
*/
?>
Jak działa atak SQL Injection?
Scenariusz 1: Normalny użytkownik
---------------------------------
Użytkownik wpisuje: ProGamer_Mike
Zapytanie SQL: SELECT * FROM players WHERE username = 'ProGamer_Mike'
Wynik: Zwraca dane gracza
Scenariusz 2: Hacker - ujawnienie wszystkich danych
---------------------------------------------------
Hacker wpisuje: ' OR '1'='1
Zapytanie SQL: SELECT * FROM players WHERE username = '' OR '1'='1'
Wynik: Zwraca WSZYSTKICH graczy!
Dlaczego? '1'='1' jest zawsze prawdą, więc warunek zawsze się spełnia
Scenariusz 3: Hacker - usunięcie tabeli
----------------------------------------
Hacker wpisuje: '; DROP TABLE players; --
Zapytanie SQL: SELECT * FROM players WHERE username = ''; DROP TABLE players; --'
Wynik: USUWA CAŁĄ TABELĘ!
Dlaczego? Średnik (;) kończy pierwsze zapytanie, DROP TABLE usuwa tabelę,
-- komentuje resztę zapytania
Scenariusz 4: Hacker - dodanie administratora
----------------------------------------------
Hacker wpisuje: '; INSERT INTO users (username, role) VALUES ('hacker', 'admin'); --
Wynik: Dodaje sobie konto z uprawnieniami admina!
Rzeczywiste konsekwencje SQL Injection:
- Kradzież danych – haker może pobrać hasła, emaile, dane kart kredytowych
- Usunięcie danych – cała baza może zostać skasowana
- Modyfikacja danych – zmiana cen produktów, uprawnień użytkowników
- Przejęcie kontroli – dodanie konta administratora
- Kara prawna i finansowa – naruszenie RODO, utrata reputacji firmy
BEZPIECZNY KOD – Prepared Statements
BEZPIECZNY KOD – Prepared Statements
Prepared Statements (zapytania przygotowane) to mechanizm, który rozdziela kod SQL od danych. Baza danych najpierw „przygotowuje” zapytanie, a potem osobno przekazuje dane.
Dlaczego są bezpieczne?
- Separacja kodu i danych – SQL i dane są wysyłane osobno
- Automatyczne escapowanie – baza wie, że to dane, nie kod
- Niemożliwy injection – dane nie mogą zmienić struktury zapytania
Jak działają Prepared Statements?
Krok 1: Przygotowanie zapytania (prepare)
------------------------------------------
PHP do MySQL: "Przygotuj zapytanie: SELECT * FROM players WHERE username = ?"
MySQL: "OK, czekam na dane do znaku zapytania"
Krok 2: Wysłanie danych (execute/bind_param)
---------------------------------------------
PHP do MySQL: "Podstaw wartość: ' OR '1'='1"
MySQL: "OK, traktuję to jako TEKST, nie jako KOD SQL"
Krok 3: Wykonanie
-----------------
MySQL wykonuje: SELECT * FROM players WHERE username = '\' OR \'1\'=\'1\''
↑ Escapowane apostrofy
Wynik: Szuka gracza o nicku "' OR '1'='1" (nie znajdzie takiego)
NIE WYKONUJE ataku!
Przykład działania – porównanie
// BEZ prepared statements (niebezpieczne):
$username = "' OR '1'='1";
$sql = "SELECT * FROM players WHERE username = '$username'";
// Zapytanie: SELECT * FROM players WHERE username = '' OR '1'='1'
// ATAK SKUTECZNY!
// Z prepared statements (bezpieczne):
$username = "' OR '1'='1";
$stmt = $pdo->prepare("SELECT * FROM players WHERE username = :username");
$stmt->execute(['username' => $username]);
// Zapytanie: SELECT * FROM players WHERE username = '\' OR \'1\'=\'1\''
// Szuka gracza o nicku "' OR '1'='1" - nie znajdzie
// ATAK ZABLOKOWANY!
SELECT z warunkiem WHERE – dane z formularza ($_POST)
Bezpieczne podejście z prepared statements – tak robimy.
Przykład 1: Pobierz wszystkich graczy (prepared statements) – MySQLi obiektowo:
<?php
// Połączenie z bazą
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die("Błąd połączenia: {$db->connect_error}");
}
$db->set_charset('utf8mb4');
echo 'Połączenie nawiązano<br><br>';
// BEZPIECZNE - Prepared statement
// Dla SELECT * bez parametrów też używamy prepare() (dobra praktyka)
$stmt = $db->prepare("SELECT * FROM players");
$stmt->execute();
// Pobranie wyniku
$wynik = $stmt->get_result();
// Sprawdzamy liczbę rekordów
$ile_wierszy = $wynik->num_rows;
echo "Znaleziono graczy: <strong>{$ile_wierszy}</strong><br><br>";
// Wyświetlamy w tabeli
echo '<table border="1" cellpadding="10" cellspacing="0">';
echo '<tr>
<th>ID</th>
<th>Username</th>
<th>Level</th>
<th>Coins</th>
<th>Rank</th>
<th>Wins/Losses</th>
</tr>';
// Pętla while - pobieramy kolejne rekordy
while ($wiersz = $wynik->fetch_assoc()) {
$username = htmlspecialchars($wiersz['username']);
$coins = number_format($wiersz['coins']);
echo "<tr>";
echo "<td>{$wiersz['id']}</td>";
echo "<td>{$username}</td>";
echo "<td>{$wiersz['level']}</td>";
echo "<td>{$coins}</td>";
echo "<td>{$wiersz['rank_name']}</td>";
echo "<td>{$wiersz['wins']}/{$wiersz['losses']}</td>";
echo "</tr>";
}
echo '</table>';
// Zamknięcie
$stmt->close();
$db->close();
?>
PDO:
<?php
try {
// Połączenie PDO
$pdo = new PDO(
'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
echo 'Połączenie nawiązano<br><br>';
// BEZPIECZNE - Prepared statement
$stmt = $pdo->prepare("SELECT * FROM players");
$stmt->execute();
// Pobranie wszystkich wyników
$wyniki = $stmt->fetchAll();
$liczba_graczy = count($wyniki);
echo "Znaleziono graczy: <strong>{$liczba_graczy}</strong><br><br>";
// Wyświetlanie w tabeli HTML
echo '<table border="1" cellpadding="10" cellspacing="0">';
echo '<tr>
<th>ID</th>
<th>Username</th>
<th>Level</th>
<th>Coins</th>
<th>Rank</th>
<th>Games</th>
<th>Win Rate</th>
</tr>';
foreach ($wyniki as $wiersz) {
// Obliczamy Win Rate (procent wygranych)
$winRate = $wiersz['games_played'] > 0
? round(($wiersz['wins'] / $wiersz['games_played']) * 100, 1)
: 0;
$username = htmlspecialchars($wiersz['username']);
$coins = number_format($wiersz['coins']);
echo "<tr>";
echo "<td>{$wiersz['id']}</td>";
echo "<td>{$username}</td>";
echo "<td>{$wiersz['level']}</td>";
echo "<td>{$coins}</td>";
echo "<td>{$wiersz['rank_name']}</td>";
echo "<td>{$wiersz['games_played']}</td>";
echo "<td>{$winRate}%</td>";
echo "</tr>";
}
echo '</table>';
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
// Zamknięcie połączenia
$pdo = null;
?>
- Używamy
htmlspecialchars()dla bezpieczeństwa (ochrona przed XSS) - Używamy
number_format()dla czytelności liczb (1234 → 1,234) - PDO automatycznie zwraca tablice asocjacyjne (ustawiliśmy w opcjach)
Przykład 2: Wyszukiwanie gracza po nicku (formularz POST)
MySQLi obiektowo
<?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die('Błąd połączenia');
}
$db->set_charset('utf8mb4');
// Sprawdzamy czy formularz został wysłany
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Pobieramy dane z formularza
$username = trim($_POST['username'] ?? '');
if (!empty($username)) {
// BEZPIECZNE - Prepared statement
$stmt = $db->prepare("SELECT * FROM players WHERE username = ?");
// Bindowanie parametru (s = string)
$stmt->bind_param("s", $username);
// Wykonanie zapytania
$stmt->execute();
// Pobranie wyniku
$wynik = $stmt->get_result();
if ($gracz = $wynik->fetch_assoc()) {
$safe_username = htmlspecialchars($gracz['username']);
$safe_email = htmlspecialchars($gracz['email']);
$coins = number_format($gracz['coins']);
echo "<h2>Profil Gracza</h2>";
echo "<strong>{$safe_username}</strong><br>";
echo "Email: {$safe_email}<br>";
echo "Level: {$gracz['level']}<br>";
echo "Rank: {$gracz['rank_name']}<br>";
echo "Coins: {$coins}<br>";
echo "Games: {$gracz['games_played']} (W: {$gracz['wins']}, L: {$gracz['losses']})<br>";
} else {
$safe_search = htmlspecialchars($username);
echo "Nie znaleziono gracza: {$safe_search}";
}
$stmt->close();
} else {
echo "Wpisz nick gracza!";
}
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Wyszukaj gracza</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
form {
background: #f4f4f4;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
input[type="text"] {
padding: 10px;
width: 70%;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<h1>Wyszukaj gracza</h1>
<form method="POST">
<input type="text" name="username" placeholder="Wpisz nick gracza" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>" required>
<button type="submit" name="szukaj">Szukaj</button>
</form>
</body>
</html>
PDO
<?php
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
// Sprawdzamy czy formularz został wysłany
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Pobieramy dane z formularza
$username = trim($_POST['username'] ?? '');
if (!empty($username)) {
// BEZPIECZNE - Prepared statement z named parameter
$stmt = $pdo->prepare("SELECT * FROM players WHERE username = :username");
$stmt->execute(['username' => $username]);
$gracz = $stmt->fetch();
if ($gracz) {
$safe_username = htmlspecialchars($gracz['username']);
$safe_email = htmlspecialchars($gracz['email']);
$coins = number_format($gracz['coins']);
echo "<h2>Profil Gracza</h2>";
echo "<strong>{$safe_username}</strong><br>";
echo "Email: {$safe_email}<br>";
echo "Level: {$gracz['level']}<br>";
echo "Rank: {$gracz['rank_name']}<br>";
echo "Coins: {$coins}<br>";
echo "Games: {$gracz['games_played']} (W: {$gracz['wins']}, L: {$gracz['losses']})<br>";
} else {
$safe_search = htmlspecialchars($username);
echo "Nie znaleziono gracza: {$safe_search}";
}
} else {
echo "Wpisz nick gracza!";
}
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
$pdo = null;
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Wyszukaj gracza</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
form {
background: #f4f4f4;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
input[type="text"] {
padding: 10px;
width: 70%;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<h1>Wyszukaj gracza</h1>
<form method="POST">
<input type="text" name="username" placeholder="Wpisz nick gracza" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>" required>
<button type="submit" name="szukaj">Szukaj</button>
</form>
</body>
</html>
SELECT z warunkiem WHERE – dane z URL ($_GET)
Przykład 3: Profil gracza przez URL (GET)
Adres URL: profil.php?id=5 lub profil.php?username=ProGamer_Mike
MySQLi obiektowo:
<?php
// profil.php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die('Błąd połączenia');
}
$db->set_charset('utf8mb4');
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['id'])) {
// Pobieramy ID z URL
$player_id = (int)$_GET['id'];
if ($player_id > 0) {
// BEZPIECZNE - Prepared statement
$stmt = $db->prepare("SELECT * FROM players WHERE id = ?");
$stmt->bind_param("i", $player_id); // i = integer
$stmt->execute();
$wynik = $stmt->get_result();
if ($gracz = $wynik->fetch_assoc()) {
$username = htmlspecialchars($gracz['username']);
$email = htmlspecialchars($gracz['email']);
$coins = number_format($gracz['coins']);
echo "<h1>Profil gracza</h1>";
echo "<div style='background:#f4f4f4; padding:20px; border-radius:8px;'>";
echo "<h2>{$username}</h2>";
echo "<p>Email: {$email}</p>";
echo "<p>Level: <strong>{$gracz['level']}</strong></p>";
echo "<p>Rank: <strong>{$gracz['rank_name']}</strong></p>";
echo "<p>Coins: <strong>{$coins}</strong></p>";
echo "<p>Rozegrane gry: {$gracz['games_played']}</p>";
echo "<p>Wygrane: {$gracz['wins']}</p>";
echo "<p>Przegrane: {$gracz['losses']}</p>";
// Win Rate
$winRate = $gracz['games_played'] > 0
? round(($gracz['wins'] / $gracz['games_played']) * 100, 1)
: 0;
echo "<p>Win Rate: <strong>{$winRate}%</strong></p>";
echo "</div>";
} else {
echo "Nie znaleziono gracza o ID: {$player_id}";
}
$stmt->close();
} else {
echo "Nieprawidłowe ID gracza";
}
} else {
echo "Podaj ID gracza w URL (np. profil.php?id=5)";
}
$db->close();
// Lista linków do innych graczy
echo "<br><hr><h3>Inni gracze:</h3>";
echo "<a href='profil.php?id=1'>Gracz #1</a> | ";
echo "<a href='profil.php?id=2'>Gracz #2</a> | ";
echo "<a href='profil.php?id=3'>Gracz #3</a> | ";
echo "<a href='profil.php?id=7'>Gracz #7</a>";
?>
PDO:
<?php
// profil.php
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
// sprawdzamy metodę HTTP i parametr
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['id'])) {
// Pobieramy ID z URL
$player_id = (int)$_GET['id'];
if ($player_id > 0) {
// BEZPIECZNE - Prepared statement
$stmt = $pdo->prepare("SELECT * FROM players WHERE id = :id");
$stmt->execute(['id' => $player_id]);
$gracz = $stmt->fetch();
if ($gracz) {
$username = htmlspecialchars($gracz['username']);
$email = htmlspecialchars($gracz['email']);
$coins = number_format($gracz['coins']);
echo "<h1>Profil gracza</h1>";
echo "<div style='background:#f4f4f4; padding:20px; border-radius:8px;'>";
echo "<h2>{$username}</h2>";
echo "<p>Email: {$email}</p>";
echo "<p>Level: <strong>{$gracz['level']}</strong></p>";
echo "<p>Rank: <strong>{$gracz['rank_name']}</strong></p>";
echo "<p>Coins: <strong>{$coins}</strong></p>";
echo "<p>Rozegrane gry: {$gracz['games_played']}</p>";
echo "<p>Wygrane: {$gracz['wins']}</p>";
echo "<p>Przegrane: {$gracz['losses']}</p>";
// Win Rate
$winRate = $gracz['games_played'] > 0
? round(($gracz['wins'] / $gracz['games_played']) * 100, 1)
: 0;
echo "<p>Win Rate: <strong>{$winRate}%</strong></p>";
echo "</div>";
} else {
echo "Nie znaleziono gracza o ID: {$player_id}";
}
} else {
echo "Nieprawidłowe ID gracza";
}
} else {
echo "Podaj ID gracza w URL (np. profil.php?id=5)";
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
$pdo = null;
// Lista linków do innych graczy
echo "<br><hr><h3>Inni gracze:</h3>";
echo "<a href='profil.php?id=1'>Gracz #1</a> | ";
echo "<a href='profil.php?id=2'>Gracz #2</a> | ";
echo "<a href='profil.php?id=3'>Gracz #3</a> | ";
echo "<a href='profil.php?id=7'>Gracz #7</a>";
?>
Przykład 4: Wyszukiwarka z wieloma filtrami (formularz POST)
MySQLi obiektowo:
<?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die('Błąd połączenia');
}
$db->set_charset('utf8mb4');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Pobieramy dane z formularza
$username = trim($_POST['username'] ?? '');
$rank = $_POST['rank'] ?? '';
$min_level = (int)($_POST['min_level'] ?? 0);
// Budujemy zapytanie dynamicznie (BEZPIECZNIE!)
$sql = "SELECT * FROM players WHERE 1=1";
$types = "";
$params = [];
if (!empty($username)) {
$sql .= " AND username LIKE ?";
$types .= "s";
$params[] = "%{$username}%";
}
if (!empty($rank)) {
$sql .= " AND rank_name = ?";
$types .= "s";
$params[] = $rank;
}
if ($min_level > 0) {
$sql .= " AND level >= ?";
$types .= "i";
$params[] = $min_level;
}
$sql .= " ORDER BY level DESC";
// Przygotowanie i wykonanie zapytania
$stmt = $db->prepare($sql);
if (!empty($params)) {
$stmt->bind_param($types, ...$params);
}
$stmt->execute();
$wynik = $stmt->get_result();
// Wyświetlamy wyniki
if ($wynik->num_rows > 0) {
echo "<h3>Znaleziono: {$wynik->num_rows} graczy</h3>";
echo '<table border="1" cellpadding="10" style="width:100%; border-collapse:collapse;">';
echo '<tr style="background:#007bff; color:white;">
<th>Username</th>
<th>Level</th>
<th>Rank</th>
<th>Coins</th>
<th>Games</th>
</tr>';
while ($gracz = $wynik->fetch_assoc()) {
$safe_username = htmlspecialchars($gracz['username']);
$coins = number_format($gracz['coins']);
echo "<tr>";
echo "<td>{$safe_username}</td>";
echo "<td>{$gracz['level']}</td>";
echo "<td>{$gracz['rank_name']}</td>";
echo "<td>{$coins}</td>";
echo "<td>{$gracz['games_played']}</td>";
echo "</tr>";
}
echo '</table>';
} else {
echo "<p>Nie znaleziono graczy spełniających kryteria</p>";
}
$stmt->close();
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Wyszukiwarka graczy</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 50px auto;
padding: 20px;
}
form {
background: #f4f4f4;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
input, select {
padding: 10px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
table {
width: 100%;
border-collapse: collapse;
}
tr:hover {
background: #f5f5f5;
}
</style>
</head>
<body>
<h1>Wyszukiwarka Graczy</h1>
<form method="POST">
<input type="text" name="username" placeholder="Nick gracza (fragment)" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>">
<select name="rank">
<option value="">Wszystkie rangi</option>
<option value="Bronze" <?= ($_POST['rank'] ?? '') == 'Bronze' ? 'selected' : '' ?>>Bronze</option>
<option value="Silver" <?= ($_POST['rank'] ?? '') == 'Silver' ? 'selected' : '' ?>>Silver</option>
<option value="Gold" <?= ($_POST['rank'] ?? '') == 'Gold' ? 'selected' : '' ?>>Gold</option>
<option value="Platinum" <?= ($_POST['rank'] ?? '') == 'Platinum' ? 'selected' : '' ?>>Platinum</option>
<option value="Diamond" <?= ($_POST['rank'] ?? '') == 'Diamond' ? 'selected' : '' ?>>Diamond</option>
<option value="Master" <?= ($_POST['rank'] ?? '') == 'Master' ? 'selected' : '' ?>>Master</option>
<option value="Grandmaster" <?= ($_POST['rank'] ?? '') == 'Grandmaster' ? 'selected' : '' ?>>Grandmaster</option>
</select>
<input type="number" name="min_level" placeholder="Min. poziom" min="1" value="<?= htmlspecialchars($_POST['min_level'] ?? '') ?>">
<button type="submit" name="szukaj">Szukaj</button>
</form>
</body>
</html>
PDO:
<?php
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Pobieramy dane z formularza
$username = trim($_POST['username'] ?? '');
$rank = $_POST['rank'] ?? '';
$min_level = (int)($_POST['min_level'] ?? 0);
// Budujemy zapytanie dynamicznie (BEZPIECZNIE!)
$sql = "SELECT * FROM players WHERE 1=1";
$params = [];
if (!empty($username)) {
$sql .= " AND username LIKE :username";
$params['username'] = "%{$username}%";
}
if (!empty($rank)) {
$sql .= " AND rank_name = :rank";
$params['rank'] = $rank;
}
if ($min_level > 0) {
$sql .= " AND level >= :min_level";
$params['min_level'] = $min_level;
}
$sql .= " ORDER BY level DESC";
// Wykonujemy zapytanie
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$wyniki = $stmt->fetchAll();
// Wyświetlamy wyniki
$liczba = count($wyniki);
if ($liczba > 0) {
echo "<h3>Znaleziono: {$liczba} graczy</h3>";
echo '<table border="1" cellpadding="10" style="width:100%; border-collapse:collapse;">';
echo '<tr style="background:#007bff; color:white;">
<th>Username</th>
<th>Level</th>
<th>Rank</th>
<th>Coins</th>
<th>Games</th>
</tr>';
foreach ($wyniki as $gracz) {
$safe_username = htmlspecialchars($gracz['username']);
$coins = number_format($gracz['coins']);
echo "<tr>";
echo "<td>{$safe_username}</td>";
echo "<td>{$gracz['level']}</td>";
echo "<td>{$gracz['rank_name']}</td>";
echo "<td>{$coins}</td>";
echo "<td>{$gracz['games_played']}</td>";
echo "</tr>";
}
echo '</table>';
} else {
echo "<p>Nie znaleziono graczy spełniających kryteria</p>";
}
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
$pdo = null;
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Wyszukiwarka graczy</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 50px auto;
padding: 20px;
}
form {
background: #f4f4f4;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
input, select {
padding: 10px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
table {
width: 100%;
border-collapse: collapse;
}
tr:hover {
background: #f5f5f5;
}
</style>
</head>
<body>
<h1>Wyszukiwarka Graczy</h1>
<form method="POST">
<input type="text" name="username" placeholder="Nick gracza (fragment)" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>">
<select name="rank">
<option value="">Wszystkie rangi</option>
<option value="Bronze" <?= ($_POST['rank'] ?? '') == 'Bronze' ? 'selected' : '' ?>>Bronze</option>
<option value="Silver" <?= ($_POST['rank'] ?? '') == 'Silver' ? 'selected' : '' ?>>Silver</option>
<option value="Gold" <?= ($_POST['rank'] ?? '') == 'Gold' ? 'selected' : '' ?>>Gold</option>
<option value="Platinum" <?= ($_POST['rank'] ?? '') == 'Platinum' ? 'selected' : '' ?>>Platinum</option>
<option value="Diamond" <?= ($_POST['rank'] ?? '') == 'Diamond' ? 'selected' : '' ?>>Diamond</option>
<option value="Master" <?= ($_POST['rank'] ?? '') == 'Master' ? 'selected' : '' ?>>Master</option>
<option value="Grandmaster" <?= ($_POST['rank'] ?? '') == 'Grandmaster' ? 'selected' : '' ?>>Grandmaster</option>
</select>
<input type="number" name="min_level" placeholder="Min. poziom" min="1" value="<?= htmlspecialchars($_POST['min_level'] ?? '') ?>">
<button type="submit" name="szukaj">Szukaj</button>
</form>
</body>
</html>
Przykład 5: Ranking graczy – sortowanie i limit (GET)
Adres URL: ranking.php?sort=level&order=desc&limit=10
MySQLi obiektowo:
<?php
// ranking.php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die('Błąd połączenia');
}
$db->set_charset('utf8mb4');
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Pobieramy parametry z URL
$sort = $_GET['sort'] ?? 'level';
$order = $_GET['order'] ?? 'desc';
$limit = (int)($_GET['limit'] ?? 10);
// Walidacja parametrów (whitelist - tylko dozwolone wartości)
$allowed_sort = ['level', 'coins', 'wins', 'games_played'];
$allowed_order = ['asc', 'desc'];
if (!in_array($sort, $allowed_sort)) {
$sort = 'level';
}
if (!in_array($order, $allowed_order)) {
$order = 'desc';
}
if ($limit < 1 || $limit > 100) {
$limit = 10;
}
// BEZPIECZNE - używamy walidacji + prepared statement dla limitu
// Kolumny sortowania są z whitelisty, więc bezpieczne
$sql = "SELECT * FROM players ORDER BY {$sort} {$order} LIMIT ?";
$stmt = $db->prepare($sql);
$stmt->bind_param("i", $limit);
$stmt->execute();
$wynik = $stmt->get_result();
$sort_label = [
'level' => 'poziomu',
'coins' => 'coins',
'wins' => 'wygranych',
'games_played' => 'rozegranych gier'
];
echo "<h1>Ranking graczy (wg {$sort_label[$sort]})</h1>";
echo "<p>Top {$limit} graczy</p>";
echo '<table border="1" cellpadding="10" style="width:100%; border-collapse:collapse;">';
echo '<tr style="background:#007bff; color:white;">
<th>#</th>
<th>Username</th>
<th>Level</th>
<th>Rank</th>
<th>Coins</th>
<th>Wins</th>
<th>Games</th>
</tr>';
$pozycja = 1;
while ($gracz = $wynik->fetch_assoc()) {
$username = htmlspecialchars($gracz['username']);
$coins = number_format($gracz['coins']);
echo "<tr>";
echo "<td><strong>#{$pozycja}</strong></td>";
echo "<td>{$username}</td>";
echo "<td>{$gracz['level']}</td>";
echo "<td>{$gracz['rank_name']}</td>";
echo "<td>{$coins}</td>";
echo "<td>{$gracz['wins']}</td>";
echo "<td>{$gracz['games_played']}</td>";
echo "</tr>";
$pozycja++;
}
echo '</table>';
echo "<br><p>Zmień sortowanie:</p>";
echo "<a href='ranking.php?sort=level&order=desc&limit=10'>Top 10 Level</a> | ";
echo "<a href='ranking.php?sort=coins&order=desc&limit=5'>Top 5 Coins</a> | ";
echo "<a href='ranking.php?sort=wins&order=desc&limit=20'>Top 20 Wins</a>";
$stmt->close();
}
$db->close();
?>
PDO:
<?php
// ranking.php
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Pobieramy parametry z URL
$sort = $_GET['sort'] ?? 'level';
$order = $_GET['order'] ?? 'desc';
$limit = (int)($_GET['limit'] ?? 10);
// Walidacja parametrów (whitelist - tylko dozwolone wartości)
$allowed_sort = ['level', 'coins', 'wins', 'games_played'];
$allowed_order = ['asc', 'desc'];
if (!in_array($sort, $allowed_sort)) {
$sort = 'level';
}
if (!in_array($order, $allowed_order)) {
$order = 'desc';
}
if ($limit < 1 || $limit > 100) {
$limit = 10;
}
// BEZPIECZNE - używamy walidacji + prepared statement dla limitu
// Kolumny sortowania są z whitelisty, więc bezpieczne
$sql = "SELECT * FROM players ORDER BY {$sort} {$order} LIMIT :limit";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
$wyniki = $stmt->fetchAll();
$sort_label = [
'level' => 'poziomu',
'coins' => 'coins',
'wins' => 'wygranych',
'games_played' => 'rozegranych gier'
];
echo "<h1>Ranking graczy (wg {$sort_label[$sort]})</h1>";
echo "<p>Top {$limit} graczy</p>";
echo '<table border="1" cellpadding="10" style="width:100%; border-collapse:collapse;">';
echo '<tr style="background:#007bff; color:white;">
<th>#</th>
<th>Username</th>
<th>Level</th>
<th>Rank</th>
<th>Coins</th>
<th>Wins</th>
<th>Games</th>
</tr>';
$pozycja = 1;
foreach ($wyniki as $gracz) {
$username = htmlspecialchars($gracz['username']);
$coins = number_format($gracz['coins']);
echo "<tr>";
echo "<td><strong>#{$pozycja}</strong></td>";
echo "<td>{$username}</td>";
echo "<td>{$gracz['level']}</td>";
echo "<td>{$gracz['rank_name']}</td>";
echo "<td>{$coins}</td>";
echo "<td>{$gracz['wins']}</td>";
echo "<td>{$gracz['games_played']}</td>";
echo "</tr>";
$pozycja++;
}
echo '</table>';
echo "<br><p>Zmień sortowanie:</p>";
echo "<a href='ranking.php?sort=level&order=desc&limit=10'>Top 10 Level</a> | ";
echo "<a href='ranking.php?sort=coins&order=desc&limit=5'>Top 5 Coins</a> | ";
echo "<a href='ranking.php?sort=wins&order=desc&limit=20'>Top 20 Wins</a>";
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
$pdo = null;
?>
Przykład 6: Wyszukiwanie LIKE – dane z GET
Adres URL: szukaj.php?q=Dragon
MySQLi obiektowo:
<?php
// szukaj.php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die('Błąd połączenia');
}
$db->set_charset('utf8mb4');
// Pobieramy fraze z URL
$szukaj = trim($_GET['q'] ?? '');
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['q'])) {
// BEZPIECZNE - Prepared statement z LIKE
$stmt = $db->prepare("SELECT * FROM players WHERE username LIKE ? OR email LIKE ?");
// Dodajemy % do wyszukiwania
$szukaj_wildcard = "%{$szukaj}%";
$stmt->bind_param("ss", $szukaj_wildcard, $szukaj_wildcard);
$stmt->execute();
$wynik = $stmt->get_result();
$safe_search = htmlspecialchars($szukaj);
if ($wynik->num_rows > 0) {
echo "<h2>Wyniki wyszukiwania: \"{$safe_search}\"</h2>";
echo "<p>Znaleziono: {$wynik->num_rows} graczy</p><br>";
while ($gracz = $wynik->fetch_assoc()) {
$username = htmlspecialchars($gracz['username']);
$email = htmlspecialchars($gracz['email']);
$coins = number_format($gracz['coins']);
echo "<div style='background:#f4f4f4; padding:15px; margin:10px 0; border-radius:8px;'>";
echo "<strong>{$username}</strong> (Level: {$gracz['level']}, Rank: {$gracz['rank_name']})<br>";
echo "{$email} | {$coins} coins<br>";
echo "<a href='profil.php?id={$gracz['id']}'>Zobacz profil</a>";
echo "</div>";
}
} else {
echo "<p>Nie znaleziono graczy dla frazy: \"{$safe_search}\"</p>";
}
$stmt->close();
} else {
echo "<p>Wpisz frazę do wyszukania (np. szukaj.php?q=Dragon)</p>";
}
$db->close();
echo "<br><hr>";
echo "<p>Przykłady wyszukiwań:</p>";
echo "<a href='szukaj.php?q=Dragon'>szukaj.php?q=Dragon</a><br>";
echo "<a href='szukaj.php?q=Gamer'>szukaj.php?q=Gamer</a><br>";
echo "<a href='szukaj.php?q=@example.com'>szukaj.php?q=@example.com</a>";
?>
PDO:
<?php
// szukaj.php
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
// Pobieramy fraze z URL
$szukaj = trim($_GET['q'] ?? '');
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['q'])) {
// BEZPIECZNE - Prepared statement z LIKE
$stmt = $pdo->prepare("SELECT * FROM players WHERE username LIKE :szukaj OR email LIKE :szukaj");
// Dodajemy % do wyszukiwania
$szukaj_wildcard = "%{$szukaj}%";
$stmt->execute(['szukaj' => $szukaj_wildcard]);
$wyniki = $stmt->fetchAll();
$safe_search = htmlspecialchars($szukaj);
$liczba = count($wyniki);
if ($liczba > 0) {
echo "<h2>Wyniki wyszukiwania: \"{$safe_search}\"</h2>";
echo "<p>Znaleziono: {$liczba} graczy</p><br>";
foreach ($wyniki as $gracz) {
$username = htmlspecialchars($gracz['username']);
$email = htmlspecialchars($gracz['email']);
$coins = number_format($gracz['coins']);
echo "<div style='background:#f4f4f4; padding:15px; margin:10px 0; border-radius:8px;'>";
echo "<strong>{$username}</strong> (Level: {$gracz['level']}, Rank: {$gracz['rank_name']})<br>";
echo "{$email} | {$coins} coins<br>";
echo "<a href='profil.php?id={$gracz['id']}'>Zobacz profil</a>";
echo "</div>";
}
} else {
echo "<p>Nie znaleziono graczy dla frazy: \"{$safe_search}\"</p>";
}
} else {
echo "<p>Wpisz frazę do wyszukania (np. szukaj.php?q=Dragon)</p>";
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
$pdo = null;
echo "<br><hr>";
echo "<p>Przykłady wyszukiwań:</p>";
echo "<a href='szukaj.php?q=Dragon'>szukaj.php?q=Dragon</a><br>";
echo "<a href='szukaj.php?q=Gamer'>szukaj.php?q=Gamer</a><br>";
echo "<a href='szukaj.php?q=@example.com'>szukaj.php?q=@example.com</a>";
?>
Przykład 7: Statystyki – funkcje agregujące
MySQLi obiektowo:
<?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die('Błąd połączenia');
}
$db->set_charset('utf8mb4');
// BEZPIECZNE - Prepared statement (brak parametrów użytkownika)
$stmt = $db->prepare("SELECT
COUNT(*) as total_players,
AVG(level) as avg_level,
SUM(coins) as total_coins,
MAX(level) as max_level,
MIN(level) as min_level,
SUM(games_played) as total_games,
SUM(wins) as total_wins,
SUM(losses) as total_losses
FROM players");
$stmt->execute();
$wynik = $stmt->get_result();
$stats = $wynik->fetch_assoc();
$avg_level = round($stats['avg_level'], 1);
$total_coins = number_format($stats['total_coins']);
$total_games = number_format($stats['total_games']);
$total_wins = number_format($stats['total_wins']);
$total_losses = number_format($stats['total_losses']);
echo "<h2>Statystyki Gaming Hub</h2>";
echo "<div style='background:#f4f4f4; padding:20px; border-radius:8px;'>";
echo "<p>Liczba graczy: <strong>{$stats['total_players']}</strong></p>";
echo "<p>Średni poziom: <strong>{$avg_level}</strong></p>";
echo "<p>Łączne coins: <strong>{$total_coins}</strong></p>";
echo "<p>Najwyższy poziom: <strong>{$stats['max_level']}</strong></p>";
echo "<p>Najniższy poziom: <strong>{$stats['min_level']}</strong></p>";
echo "<p>Rozegrane gry: <strong>{$total_games}</strong></p>";
echo "<p>Wygrane: <strong>{$total_wins}</strong></p>";
echo "<p>Przegrane: <strong>{$total_losses}</strong></p>";
echo "</div>";
$stmt->close();
$db->close();
?>
PDO:
<?php
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
// BEZPIECZNE - Prepared statement (brak parametrów użytkownika)
$stmt = $pdo->prepare("SELECT
COUNT(*) as total_players,
AVG(level) as avg_level,
SUM(coins) as total_coins,
MAX(level) as max_level,
MIN(level) as min_level,
SUM(games_played) as total_games,
SUM(wins) as total_wins,
SUM(losses) as total_losses
FROM players");
$stmt->execute();
$stats = $stmt->fetch();
$avg_level = round($stats['avg_level'], 1);
$total_coins = number_format($stats['total_coins']);
$total_games = number_format($stats['total_games']);
$total_wins = number_format($stats['total_wins']);
$total_losses = number_format($stats['total_losses']);
echo "<h2>Statystyki Gaming Hub</h2>";
echo "<div style='background:#f4f4f4; padding:20px; border-radius:8px;'>";
echo "<p>Liczba graczy: <strong>{$stats['total_players']}</strong></p>";
echo "<p>Średni poziom: <strong>{$avg_level}</strong></p>";
echo "<p>ączne coins: <strong>{$total_coins}</strong></p>";
echo "<p>Najwyższy poziom: <strong>{$stats['max_level']}</strong></p>";
echo "<p>Najniższy poziom: <strong>{$stats['min_level']}</strong></p>";
echo "<p>Rozegrane gry: <strong>{$total_games}</strong></p>";
echo "<p>Wygrane: <strong>{$total_wins}</strong></p>";
echo "<p>Przegrane: <strong>{$total_losses}</strong></p>";
echo "</div>";
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
$pdo = null;
?>
Whitelist vs Blacklist
Whitelist (ZALECANE):
// Lista dozwolonych wartości
$allowed_sort = ['level', 'coins', 'wins'];
if (in_array($_GET['sort'], $allowed_sort)) {
$sort = $_GET['sort']; // Bezpieczne
} else {
$sort = 'level'; // Domyślna wartość
}
Blacklist (NIE ZALECANE):
// Próba blokowania "złych" wartości - NIGDY nie złapiesz wszystkich!
if ($_GET['sort'] != 'DROP' && $_GET['sort'] != 'DELETE') {
$sort = $_GET['sort']; // NADAL NIEBEZPIECZNE!
}
Najlepsze praktyki
ZAWSZE:
- Używaj prepared statements dla WSZYSTKICH zapytań z danymi użytkownika
- Używaj PDO dla nowych projektów (uniwersalność + bezpieczeństwo)
- Używaj
htmlspecialchars()przy wyświetlaniu danych (ochrona XSS) - Waliduj dane wejściowe (whitelist dla sortowania, LIMIT, itp.)
- Ustawiaj charset na utf8mb4 (obsługa emoji i polskich znaków)
- Loguj błędy do pliku, nie pokazuj szczegółów użytkownikowi
- Używaj
number_format()dla czytelności dużych liczb - Rzutuj na właściwy typ –
(int)dla liczb,trim()dla stringów
NIGDY:
- Nie łącz zapytań SQL przez konkatenację z danymi użytkownika
- Nie pokazuj szczegółów błędów SQL użytkownikowi końcowemu
- Nie trzymaj danych dostępowych w kodzie (użyj
config.php) - Nie commituj
config.phpdo repozytorium - Nie używaj
rootbez hasła w środowisku produkcyjnym - Nie ufaj danym z
$_GETi$_POST– zawsze waliduj!
TaskQuest 1.8 — każdy gracz widzi swoje dane z bazy
KARTA PRACY
Po Lekcji 17 TaskQuest ma bazę danych taskquest z dwiema tabelami i danymi testowymi Kamy i Oli. Aplikacja jednak w ogóle z niej nie korzysta: zadania czyta z data/zadania.csv, a XP i streak są wpisane w index.php. Kuba i Ola po zalogowaniu nadal widzą te same zadania i te same 99 XP.
Dziś połączymy PHP z bazą i przełączymy stronę główną na dane z bazy. Po tej lekcji każdy użytkownik zobaczy własne zadania, własne XP i własny streak.
Cel lekcji
Nauczyć się:
- łączyć się z bazą w PHP obiektowym
mysqli(new mysqli(...)) i obsługiwać błąd połączenia wyjątkiem, - pisać zapytania
SELECTzWHERE,AND,IS NULL,ORDER BYiLIMIT, - odczytywać wynik zapytania wiersz po wierszu (
fetch_assoc()) i zamieniać wiersze w obiektyZadanieiUzytkownik, - rozpoznać SQL injection i przed nim się chronić: dane od użytkownika zawsze przekazujemy przez prepared statements (
prepare(),bind_param(),execute(),get_result()), - zapisywać typ „wartość albo
null” w PHP (?string,?int).
Efekt: logowanie sprawdza login w bazie, index.php pobiera z bazy zalogowanego użytkownika i tylko jego zadania, a pod listą zadań pojawia się ranking najlepszych graczy.
Uwaga — co jest, a czego jeszcze nie ma:
- Dziś tylko czytamy z bazy (
SELECT). Zapis (INSERT,UPDATE,DELETE) to Lekcja 19. Dlatego dodawanie zadań jest w tej wersji wyłączone z nawigacji: formularzdodaj-zadanie.phpnadal zapisuje do CSV, a strona główna czyta już z bazy. W Lekcji 19 formularz przejdzie naINSERTi wróci na stronę.- Logowanie nadal jest demonstracyjne: bez hasła. Różnica: login musi istnieć w bazie. Hasła — Lekcja 19 i 22.
- Streak nadal jest liczony uproszczoną metodą z poprzednich lekcji. Prawdziwy streak (data wykonania zadania) — Lekcja 19.
mysqliużywamy wyłącznie obiektowo. W internecie (i w starszych materiałach) zobaczyszmysqli_connect(),@new mysqli(...)i sprawdzanieconnect_error. My obsługujemy błędy wyjątkami, tak jak od Lekcji 12.
Rozgrzewka — 4 pytania z Lekcji 17
- Dlaczego w tabeli
zadaniajest kolumnadata_wykonania, a nie ma kolumnywykonane? Jak w PHP obliczymywykonane? - Kama ma
id= 1, Ola maid= 2. Która kolumna tabelizadaniamówi, czyje jest zadanie? - Czym dla bazy różnią się
NULL,''i'NULL'? diagnostyka.phppokazuje[OK]przymysqli. Czego to jeszcze nie gwarantuje?
1. Wracamy do naszego TaskQuest
Co zostaje, co dochodzi, co zmieniamy
| Element | Po Lekcji 17 | Po Lekcji 18 |
|---|---|---|
baza taskquest, sql/taskquest.sql | istnieje, nieużywana | zostaje bez zmian — teraz aplikacja z niej czyta |
klasa Zadanie | istnieje | zostaje bez zmian |
klasa Uzytkownik | bez id, dataOstatniejAktywnosci typu string | zmiana: dochodzi id + pobierzId(), data jako ?string |
symulowane xp: 99, streak: 5, ... w index.php | źródło danych gracza | usuwamy — dane gracza z tabeli uzytkownicy |
wczytajZadania(PLIK_ZADAN) w index.php | źródło zadań | zmiana: index.php czyta zadania z bazy; funkcja zostaje (używa jej jeszcze dodaj-zadanie.php) |
includes/funkcje_bazy.php | nie ma | dochodzi: połączenie + 4 funkcje z SELECT |
config/config.php | 3 stałe | dochodzą 4 stałe z danymi dostępowymi do bazy |
| logowanie | login + imię, każdy login „wchodzi” | zmiana: tylko login, musi istnieć w bazie; w sesji tylko uzytkownik_id |
wszystkie funkcje listy (sortujPoTerminie() itd.), grywalizacji, terminów | istnieją | zostają bez zmian |
Najważniejsza zasada tej lekcji jest ta sama co w Lekcji 17 (sekcja 6): zmienia się źródło danych, obiekty zostają te same. Dlatego większość index.php — liczniki, filtr widoku, pasek XP, lista — nie zmieni się ani o znak.
Docelowa struktura projektu
taskquest/
├── index.php (zmiana: dane z bazy, bez linku „+ Dodaj zadanie”, ranking)
├── dodaj-zadanie.php bez zmian (nadal CSV; wyłączony z nawigacji do L19)
├── zaloguj.php (zmiana: login sprawdzany w bazie, bez pola „imię”)
├── wyloguj.php bez zmian
├── diagnostyka.php (zmiana: 19 wymaganych plików)
├── classes/
│ ├── Zadanie.php bez zmian
│ └── Uzytkownik.php (zmiana: id, ?string)
├── config/
│ └── config.php (zmiana: stałe BAZA_...)
├── includes/
│ ├── footer.php (zmiana: „TaskQuest 1.8”)
│ ├── funkcje_bazy.php NOWY
│ ├── funkcje_sesji.php (zmiana: czyZalogowany())
│ ├── funkcje_tekstowe.php (zmiana: walidujDaneLogowania())
│ └── ... pozostałe bez zmian
├── sql/
│ └── taskquest.sql bez zmian
└── data/
└── zadania.csv bez zmian (czyta go już tylko dodaj-zadanie.php)
Przygotowanie (zrób to teraz)
- Zrób kopię zapasową katalogu
taskquest/jakotaskquest_lekcja17/(obok, nie w środku). - Uruchom w XAMPP Apache i MySQL.
- W phpMyAdmin uruchom ponownie cały
sql/taskquest.sql(bazataskquest→ zakładka SQL). Zaczynamy od czystych danych testowych.
2. Problem w aplikacji
Ola loguje się do TaskQuest jako Ola_TQ. Co widzi?
| Co widzi Ola | Co jest w bazie dla Oli |
|---|---|
| „Witaj, Ola!” | imię Ola ✔ |
| 99 XP, poziom 1 | 250 XP, poziom 3 |
| streak 5 | streak 0 |
| zadania Kamy z CSV | jedno własne zadanie „Zaprojektować bazę TaskQuest” |
Imię się zgadza tylko dlatego, że Ola sama je wpisała w formularzu. Co więcej, ktoś może się zalogować jako Nieistniejacy_123 — takiego konta nie ma nigdzie, a TaskQuest i tak go wpuści.
Potrzebujemy trzech rzeczy:
- połączenia PHP z bazą,
- zapytania, które zwróci dane jednego konkretnego użytkownika i tylko jego zadania,
- bezpiecznego sposobu wstawienia do zapytania danych z zewnątrz (login wpisany w formularzu,
idz sesji) — bo, jak zaraz zobaczysz, najprostszy sposób jest groźny.
3. Nowy mechanizm: SELECT i mysqli
3.1 SELECT — zadajemy bazie pytania (phpMyAdmin)
W Lekcji 17 używaliśmy tylko SELECT *. Teraz poznamy pełniejszą postać. Otwórz phpMyAdmin → baza taskquest → zakładka SQL. Przed uruchomieniem każdego zapytania przewidź, ile wierszy zwróci.
SELECT tytul, termin FROM zadania;
SELECT tytul, termin FROM zadania WHERE uzytkownik_id = 1;
SELECT tytul, termin FROM zadania WHERE uzytkownik_id = 1 AND data_wykonania IS NULL;
SELECT tytul, termin FROM zadania WHERE uzytkownik_id = 1 ORDER BY termin;
SELECT login, xp FROM uzytkownicy ORDER BY xp DESC LIMIT 1;
| Fragment | Znaczenie | Odpowiednik w TaskQuest |
|---|---|---|
SELECT tytul, termin | które kolumny chcemy dostać | — |
FROM zadania | z której tabeli | który plik CSV |
WHERE uzytkownik_id = 1 | tylko wiersze spełniające warunek | array_filter() w pobierzAktywneZadania() |
AND | oba warunki muszą być spełnione | && w PHP |
IS NULL / IS NOT NULL | czy wartość jest pusta (NULL) | === null / !== null w PHP |
ORDER BY termin | sortowanie rosnące (domyślnie ASC) | sortujPoTerminie() |
ORDER BY xp DESC | sortowanie malejące | — |
LIMIT 1 | najwyżej tyle wierszy | — |
Kolejność części jest stała: SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT .... Zamiana miejscami daje błąd składni.
Dlaczego wymieniamy kolumny zamiast *? Pobieramy tylko to, czego potrzebujemy. W tabeli uzytkownicy jest kolumna haslo_hash — od Lekcji 19 będzie w niej skrót hasła. SELECT * przy wyświetlaniu rankingu przeciągnąłby go do PHP zupełnie niepotrzebnie.
Pułapka: sprawdź teraz
SELECT tytul FROM zadania WHERE data_wykonania = NULL;
Wynik: zero wierszy, bez błędu, choć niewykonane zadania istnieją. NULL oznacza „nie wiadomo”, a „czy nie wiadomo jest równe nie wiadomo?” daje w SQL odpowiedź „nie wiadomo”, czyli nie „prawda”. Do NULL używamy tylko IS NULL / IS NOT NULL. To jeden z najczęstszych błędów początkujących, bo baza nic nie zgłasza.
3.2 Połączenie z bazą w PHP
W katalogu taskquest/ utwórz brudnopis.php:
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$polaczenie = new mysqli('localhost', 'root', '', 'taskquest');
$polaczenie->set_charset('utf8mb4');
echo 'Połączono. Serwer: ' . $polaczenie->server_info;
Uruchom http://localhost/taskquest/brudnopis.php.
| Linia | Po co |
|---|---|
mysqli_report(...) | ustawia, jak mysqli zgłasza błędy: MYSQLI_REPORT_ERROR — zgłaszaj błędy, MYSQLI_REPORT_STRICT — jako wyjątki. Od PHP 8.1 to ustawienie domyślne, ale piszemy je jawnie: kod nie zależy od wersji PHP, a czytający od razu wie, czego się spodziewać |
new mysqli(host, użytkownik, hasło, baza) | tworzy obiekt połączenia (klasa mysqli, tak jak od Lekcji 15 tworzysz new Zadanie(...)). XAMPP ma domyślnie użytkownika root bez hasła |
set_charset('utf8mb4') | PHP i baza umawiają się na kodowanie znaków. Bez tego polskie litery i emoji mogą przychodzić zniekształcone — zależnie od ustawień serwera |
$polaczenie->server_info | właściwość obiektu: wersja serwera. Tu tylko dowód, że połączenie działa |
Teraz zmień nazwę bazy na taskquestt i uruchom ponownie. Zobaczysz Fatal error: Uncaught mysqli_sql_exception: Unknown database 'taskquestt'. To znany Ci mechanizm z Lekcji 12: wyjątek, którego nikt nie złapał. mysqli_sql_exception to klasa wyjątku z rozszerzenia mysqli; jest rodzajem Exception, więc złapie go zwykłe catch (Exception $wyjatek). Przywróć nazwę taskquest.
Połączenie zamyka się samo, gdy skrypt się kończy. Można je zamknąć wcześniej:
$polaczenie->close();. W TaskQuest jedno żądanie = jedno krótkie połączenie, więc zostawiamy to PHP.
3.3 Pierwsze zapytanie z PHP
Dopisz do brudnopisu:
$wynik = $polaczenie->query('SELECT id, login, imie, xp FROM uzytkownicy');
echo '<p>Liczba użytkowników: ' . $wynik->num_rows . '</p>';
while ($wiersz = $wynik->fetch_assoc()) {
echo "<p>{$wiersz['id']}. {$wiersz['login']} ({$wiersz['imie']}) — {$wiersz['xp']} XP</p>";
}
Po drodze powstają trzy różne rzeczy:
$polaczenie (obiekt mysqli) ──query()──► $wynik (obiekt mysqli_result)
$wynik ──fetch_assoc()──► $wiersz (tablica asocjacyjna, klucze = nazwy kolumn)
albo null, gdy wiersze się skończyły
query()wysyła zapytanie i zwraca wynik — coś w rodzaju otwartego pliku CSV, z którego czytamy wiersz po wierszu.fetch_assoc()zwraca kolejny wiersz, a po ostatnim zwracanull. Dlategowhile ($wiersz = $wynik->fetch_assoc())działa jak pętla pofgetcsv()z Lekcji 11: przypisz, sprawdź, powtarzaj.num_rows— liczba wierszy wyniku.
Dopisz na końcu brudnopisu i przewidź, co pokaże:
$wynik = $polaczenie->query('SELECT id, xp, data_ostatniej_aktywnosci FROM uzytkownicy');
var_dump($wynik->fetch_assoc());
var_dump($wynik->fetch_assoc());
var_dump($wynik->fetch_assoc());
Wynik: id i xp przyszły jako teksty (string(1) "1", string(2) "99"), data jako tekst "2026-09-14", NULL u Oli jako PHP-owe null, a trzecie wywołanie zwraca null (użytkowników jest dwóch). Zapamiętaj te teksty — w 3.5 zobaczysz różnicę.
3.4 Dane od użytkownika w zapytaniu — i dlaczego tak NIE robimy
TaskQuest musi znaleźć użytkownika po loginie wpisanym w formularzu. Najprostszy pomysł: wkleić login do tekstu zapytania. Zastąp zawartość brudnopisu:
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$polaczenie = new mysqli('localhost', 'root', '', 'taskquest');
$polaczenie->set_charset('utf8mb4');
// ŹLE — wyłącznie do tej demonstracji!
$login = $_GET['login'] ?? '';
$sql = "SELECT id, login, xp FROM uzytkownicy WHERE login = '$login'";
echo "<p>Zapytanie: $sql</p>";
$wynik = $polaczenie->query($sql);
echo "<p>Znaleziono: {$wynik->num_rows}</p>";
while ($wiersz = $wynik->fetch_assoc()) {
echo "<p>{$wiersz['login']} — {$wiersz['xp']} XP</p>";
}
Uruchamiaj po kolei i za każdym razem czytaj linię „Zapytanie: …”:
| Adres | Wynik | Co się stało |
|---|---|---|
brudnopis.php?login=Ola_TQ | 1 wiersz | działa |
brudnopis.php?login=Nowy_1 | 0 wierszy | działa |
brudnopis.php?login=O'Neil | Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax... | apostrof z loginu zamknął tekst w SQL za wcześnie |
brudnopis.php?login=' OR '1'='1 | 2 wiersze — wszyscy użytkownicy | patrz niżej |
Spójrz na ostatnie zapytanie:
SELECT id, login, xp FROM uzytkownicy WHERE login = '' OR '1'='1'
Tekst wpisany przez użytkownika stał się częścią polecenia SQL. Warunek '1'='1' jest zawsze prawdziwy, więc zapytanie zwraca wszystkich. To SQL injection (wstrzykiwanie SQL) — od lat jedna z najczęściej wykorzystywanych podatności aplikacji webowych. Tą samą metodą da się ominąć logowanie albo wyciągnąć dane z innych tabel.
Ktoś powie: „przecież walidujLogin() nie przepuści apostrofu”. To prawda — dla loginu. Ale wyszukiwarka zadań (zadanie końcowe A) musi przyjąć dowolny tekst, a opis zadania może zawierać apostrof. Walidacja to pierwsza linia obrony, a zapytanie ma być bezpieczne samo z siebie.
Testuj takie rzeczy wyłącznie na własnej, lokalnej bazie. Próba włamania do cudzego systemu jest przestępstwem.
3.5 Prepared statements — dane osobno, polecenie osobno
Problem wziął się z tego, że polecenie i dane zostały sklejone w jeden tekst. Prepared statement (zapytanie przygotowane) rozdziela je:
1. prepare() wysyłamy SZABLON polecenia: ... WHERE login = ?
baza analizuje go i zapamiętuje — to jest już gotowe polecenie
2. bind_param() podpinamy dane pod znak ?
3. execute() wysyłamy dane osobno; baza wstawia je jako WARTOŚĆ, nigdy jako kod
4. get_result() odbieramy wynik — dalej jak w 3.3 (fetch_assoc())
Cokolwiek wpisze użytkownik, baza porówna to z loginem jako zwykły tekst. ' OR '1'='1 to po prostu dziwny login, którego nikt nie ma.
Zamień w brudnopisie część od // ŹLE do końca na:
$login = $_GET['login'] ?? '';
$zapytanie = $polaczenie->prepare('SELECT id, login, xp FROM uzytkownicy WHERE login = ?');
$zapytanie->bind_param('s', $login);
$zapytanie->execute();
$wynik = $zapytanie->get_result();
echo "<p>Znaleziono: {$wynik->num_rows}</p>";
while ($wiersz = $wynik->fetch_assoc()) {
var_dump($wiersz);
}
bind_param() ma dwa rodzaje argumentów:
- tekst z typami — po jednej literze na każdy
?, w tej samej kolejności:s— tekst (string),i— liczba całkowita (integer),d— liczba z przecinkiem (double), - zmienne z wartościami, w tej samej kolejności co znaki
?.
Przykład z dwoma parametrami: WHERE uzytkownik_id = ? AND tytul = ? → bind_param('is', $uzytkownikId, $tytul).
Powtórz cztery adresy z tabeli w 3.4. Oczekiwane: Ola_TQ → 1, Nowy_1 → 0, O'Neil → 0, bez błędu, ' OR '1'='1 → 0.
Teraz spójrz na var_dump przy Ola_TQ: ["id"]=> int(2), ["xp"]=> int(250). W 3.3 były teksty, tu są liczby. Prepared statements przesyłają wynik w innym formacie (tzw. protokół binarny) i mysqli zamienia kolumny INT na PHP-owe int. Daty (DATE) nadal przychodzą jako tekst RRRR-MM-DD, a NULL jako null. Dla TaskQuest to wygodne: typy pasują do właściwości Zadanie i Uzytkownik.
Zasada TaskQuest od dziś: każde zapytanie, w którym jest jakakolwiek wartość z zewnątrz (formularz, adres, cookie, sesja), piszemy jako prepared statement. Sklejanie SQL ze zmiennymi (
"... '$login'") jest zakazane.query()zostaje tylko dla zapytań bez żadnych zmiennych — a dla porządku i tak w TaskQuest ich nie używamy.
Typowe błędy
| Błąd | Skutek |
|---|---|
WHERE login = '?' (znak zapytania w apostrofach) | to już nie parametr, tylko tekst „?”; bind_param() zgłasza błąd o niezgodnej liczbie parametrów |
liczba liter w typach ≠ liczba ? | błąd o niezgodnej liczbie parametrów |
bind_param('i', 5) — wartość zamiast zmiennej | Error: argumentu nie da się przekazać przez referencję. bind_param() wymaga zmiennych |
$zapytanie->fetch_assoc() bez get_result() | Error: taka metoda nie istnieje. fetch_assoc() ma wynik, nie zapytanie |
WHERE data_wykonania = NULL | zawsze 0 wierszy, bez błędu (3.1) |
SELECT * „bo wygodniej” | pobieramy kolumny, których nie potrzebujemy (np. haslo_hash) |
4. SAMODZIELNIE — zadania wybranego użytkownika jako obiekty
Nadal w brudnopisie. Napisz skrypt, który:
- dołącza
classes/Zadanie.phpiincludes/funkcje_tekstowe.php, - łączy się z bazą jak w 3.2,
- bierze
idużytkownika z adresu (brudnopis.php?id=1); jeśliidnie składa się z samych cyfr, wyświetlaNieprawidłowe id.i kończy działanie, - prepared statementem pobiera kolumny
id,tytul,opis,termin,data_wykonania,xpzadań tego użytkownika, - z każdego wiersza tworzy obiekt
Zadanie(argumenty nazwane, jak w Lekcji 17, sekcja 6) i dopisuje go do tablicy$zadania, - na końcu wyświetla liczbę zadań i dla każdego:
[X]albo[ ]oraz oczyszczony tytuł.
Oczekiwane: ?id=1 — zadania Kamy (tyle, ile w bazie), ?id=2 — jedno zadanie Oli z [ ], ?id=3 — Zadań: 0, ?id=abc — Nieprawidłowe id.
HINT
Sprawdzanie liczby z adresu robiłaś/robiłeś w Lekcji 13 (wyrażenie regularne przed (int)). Pętla po wynikach wygląda jak w 3.3, tylko zamiast echo jest $zadania[] = new Zadanie(...). Który typ ma parametr: s czy i?
POMOC
$zapytanie = $polaczenie->prepare('SELECT ... FROM zadania WHERE uzytkownik_id = ?');
$zapytanie->bind_param('i', $uzytkownikId);
wykonane: $wiersz['data_wykonania'] !== null,
ROZWIĄZANIE
<?php
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$polaczenie = new mysqli('localhost', 'root', '', 'taskquest');
$polaczenie->set_charset('utf8mb4');
$idZAdresu = $_GET['id'] ?? '';
if (!preg_match('/^\d+$/', $idZAdresu)) {
echo 'Nieprawidłowe id.';
exit;
}
$uzytkownikId = (int) $idZAdresu;
$zapytanie = $polaczenie->prepare(
'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE uzytkownik_id = ?'
);
$zapytanie->bind_param('i', $uzytkownikId);
$zapytanie->execute();
$wynik = $zapytanie->get_result();
$zadania = [];
while ($wiersz = $wynik->fetch_assoc()) {
$zadania[] = new Zadanie(
id: $wiersz['id'],
tytul: $wiersz['tytul'],
opis: $wiersz['opis'],
termin: $wiersz['termin'],
wykonane: $wiersz['data_wykonania'] !== null,
xp: $wiersz['xp'],
);
}
echo '<p>Zadań: ' . count($zadania) . '</p>';
foreach ($zadania as $zadanie) {
$znacznik = $zadanie->czyWykonane() ? '[X]' : '[ ]';
echo '<p>' . $znacznik . ' ' . wyczyscTytul($zadanie->pobierzTytul()) . '</p>';
}
idixpnie potrzebują(int)— z prepared statementu przychodzą już jakoint(3.5). Porównaj zwczytajZadania(), gdzie(int) $wiersz[0]był konieczny, bo CSV zawiera same teksty.- Nie ma tu odpowiednika
sprawdzWierszZadania(). Baza nie wpuści złego terminu, ujemnego XP ani zbyt długiego tytułu (Test 4 z Lekcji 17), więc wiersz z bazy ma zawsze poprawny kształt. ?id=3nie jest błędem: zapytanie jest poprawne, po prostu nie ma pasujących wierszy — pusta tablica, tak jak pusty plik CSV w Lekcji 12 nie był awarią.
5. Włączamy bazę do TaskQuest
Od tego miejsca pracujesz na projekcie.
brudnopis.phpzostaw — przyda się do testowania nowych funkcji, zanim podłączysz je do stron.
Plan: najpierw budujemy i testujemy w brudnopisie wszystkie funkcje (kroki 1–5), potem w jednym ruchu przełączamy logowanie i stronę główną (kroki 6–7). Między krokiem 6 a 7 aplikacja nie działa — wykonaj je bez przerwy.
Krok 1 — dane dostępowe w config/config.php
Dane dostępowe wpisaliśmy w brudnopisie trzy razy. W projekcie mają być w jednym miejscu — tam, gdzie od Lekcji 10 trzymamy ustawienia. Na końcu config/config.php dopisz:
// (L18) dane dostępowe do bazy danych — domyślne ustawienia XAMPP
define('BAZA_HOST', 'localhost');
define('BAZA_UZYTKOWNIK', 'root');
define('BAZA_HASLO', '');
define('BAZA_NAZWA', 'taskquest');
Na szkolnym albo prawdziwym serwerze te wartości będą inne (i nigdy root bez hasła). Dzięki stałym zmienisz je w jednym pliku.
Krok 2 — plik includes/funkcje_bazy.php i funkcja polaczZBaza()
Utwórz includes/funkcje_bazy.php:
<?php
// Funkcje TaskQuest korzystające z bazy danych (Lekcja 18).
// Wymagają wcześniejszego dołączenia config/config.php (stałe BAZA_...).
// Funkcje tworzące obiekty wymagają też classes/Zadanie.php i classes/Uzytkownik.php.
function polaczZBaza(): mysqli
{
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$polaczenie = new mysqli(BAZA_HOST, BAZA_UZYTKOWNIK, BAZA_HASLO, BAZA_NAZWA);
} catch (Exception $wyjatek) {
throw new Exception('Nie udało się połączyć z bazą danych.');
}
$polaczenie->set_charset('utf8mb4');
return $polaczenie;
}
- Typ zwracany
mysqli— funkcja zwraca obiekt klasy, tak jak metoda mogłaby zwracaćZadanie. - Dlaczego łapiemy wyjątek i rzucamy nowy? Komunikat z
mysqliprzy złym haśle brzmi mniej więcej:Access denied for user 'root'@'localhost' (using password: YES). Zdradza nazwę użytkownika bazy i adres serwera. Od Lekcji 12 komunikaty wyjątków trafiają do użytkownika, więc mają być dla niego — bez szczegółów technicznych. Eksperyment 4 pokaże różnicę. - Plik z samym PHP — bez zamykającego
?>(konwencja od Lekcji 10).
Krok 3 — SAMODZIELNIE: wczytajZadaniaUzytkownika()
Kod z sekcji 4 zamień w funkcję w funkcje_bazy.php (pod polaczZBaza()):
function wczytajZadaniaUzytkownika(mysqli $polaczenie, int $uzytkownikId): array
Wymagania:
- funkcja nie tworzy połączenia — dostaje je w parametrze (jedno połączenie wystarczy na całe żądanie, a
index.phpwykona kilka zapytań), - zwraca tablicę obiektów
Zadanie— dokładnie to samo, co zwracawczytajZadania()z CSV; dla użytkownika bez zadań[], - sortuje po
id(ORDER BY id), żeby kolejność była stała, jak kolejność linii w CSV. Sortowaniem po terminie nadal zajmuje sięsortujPoTerminie()— nie zmieniamy działającej logiki, - niczego nie wyświetla i nie sprawdza
idregexem (int $uzytkownikIdgwarantuje liczbę; sprawdzanie tekstu z adresu to zadanie strony, nie tej funkcji).
Przetestuj w brudnopisie:
<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
$polaczenie = polaczZBaza();
var_dump(count(wczytajZadaniaUzytkownika($polaczenie, 1)));
var_dump(count(wczytajZadaniaUzytkownika($polaczenie, 2)));
var_dump(wczytajZadaniaUzytkownika($polaczenie, 3));
Oczekiwane: liczba zadań Kamy, int(1), array(0) {}.
HINT
Cała środkowa część rozwiązania z sekcji 4 (od prepare do końca pętli) przechodzi do funkcji prawie bez zmian. Czego w funkcji nie ma? Połączenia, $_GET, echo.
ROZWIĄZANIE
function wczytajZadaniaUzytkownika(mysqli $polaczenie, int $uzytkownikId): array
{
$zapytanie = $polaczenie->prepare(
'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE uzytkownik_id = ? ORDER BY id'
);
$zapytanie->bind_param('i', $uzytkownikId);
$zapytanie->execute();
$wynik = $zapytanie->get_result();
$zadania = [];
while ($wiersz = $wynik->fetch_assoc()) {
$zadania[] = new Zadanie(
id: $wiersz['id'],
tytul: $wiersz['tytul'],
opis: $wiersz['opis'],
termin: $wiersz['termin'],
wykonane: $wiersz['data_wykonania'] !== null,
xp: $wiersz['xp'],
);
}
return $zadania;
}
Błędów zapytania (np. literówki w nazwie kolumny) ta funkcja nie łapie. To błędy programisty, a nie awaria, której użytkownik może się spodziewać. Wyjątek poleci wyżej, do strony (Eksperyment 3).
Krok 4 — Uzytkownik dostaje id, a data może być null
Zanim napiszemy funkcję wczytującą użytkownika, sprawdźmy, czy wiersz Oli w ogóle zmieści się w obiekcie. Brudnopis:
<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
$uzytkownik = new Uzytkownik(
login: 'Ola_TQ',
imieUzytkownika: 'Ola',
xp: 250,
streak: 0,
dataOstatniejAktywnosci: null, // tak jest w bazie
);
Wynik: TypeError — argument $dataOstatniejAktywnosci musi być typu string, a podano null. W Lekcji 17 świadomie zdecydowaliśmy, że data_ostatniej_aktywnosci może być NULL (Ola jeszcze nic nie zrobiła). Klasa musi to umieć przyjąć.
Nowa składnia: typ z dopuszczalnym null. Znak zapytania przed typem oznacza „ten typ albo null”:
?string // tekst albo null
?int // liczba całkowita albo null
To bezpośredni odpowiednik kolumny NULL w bazie: DATE NULL → ?string, DATE NOT NULL → string.
Zmiany w classes/Uzytkownik.php (reszta klasy bez zmian):
Jako pierwszą właściwość dopisz
private int $id;— w Lekcji 17 zapowiadaliśmy, żeidwróci, gdy będzie skąd je wziąć. Jutro (L19) będzie potrzebne do zapisu XP konkretnego użytkownika.private string $dataOstatniejAktywnosci;→private ?string $dataOstatniejAktywnosci;Konstruktor: nowy pierwszy parametr
int $id, typ ostatniego?string, w ciele$this->id = $id;:public function __construct(
int $id,
string $login,
string $imieUzytkownika,
int $xp,
int $streak,
?string $dataOstatniejAktywnosci
) {
$this->id = $id;
$this->login = $login;
// ... pozostałe przypisania bez zmian
}Pod konstruktorem nowy getter:
public function pobierzId(): int
{
return $this->id;
}Metoda
aktualizujStreak()przekazuje datę do funkcjiaktualizujStreak(int $streak, string $dataOstatniejAktywnosci, bool $zadanieWykonane), która wymagastring. Dlanullteż byłbyTypeError. Na początku ciała metody dopisz:public function aktualizujStreak(bool $zadanieWykonane): void
{
if ($this->dataOstatniejAktywnosci === null) {
return; // brak aktywności — nie ma od czego liczyć serii, streak zostaje taki jak w bazie
}
$this->streak = aktualizujStreak($this->streak, $this->dataOstatniejAktywnosci, $zadanieWykonane);
}Funkcja
aktualizujStreak()zfunkcje_terminow.phpnie zmienia się. Problemnullzatrzymujemy w klasie, zanim dotrze do funkcji.
Popraw brudnopis (id: 2, na początku argumentów) i uruchom ponownie: brak błędu. Dopisz var_dump($uzytkownik->pobierzId(), $uzytkownik->pobierzPoziom()); → int(2), int(3).
Twój
index.phpprzestał teraz działać (new Uzytkownik(...)bezid) — to w porządku, w kroku 7 i tak zastąpimy ten fragment.
Krok 5 — SAMODZIELNIE: wczytajUzytkownika()
W funkcje_bazy.php napisz:
function wczytajUzytkownika(mysqli $polaczenie, int $uzytkownikId): ?Uzytkownik
- pobiera z tabeli
uzytkownicyjeden wiersz o podanymid— tylko kolumny potrzebne do obiektu (bezhaslo_hash!), - zwraca obiekt
Uzytkownikalbonull, gdy takiego użytkownika nie ma (typ?Uzytkownik— ten sam znak zapytania, tym razem przy klasie), - pamiętaj o konwencji nazw: kolumna
imie→ parametrimieUzytkownika,data_ostatniej_aktywnosci→dataOstatniejAktywnosci.
Test w brudnopisie (dołącz też classes/Uzytkownik.php):
var_dump(wczytajUzytkownika($polaczenie, 1)->pobierzImie());
var_dump(wczytajUzytkownika($polaczenie, 2)->pobierzXp());
var_dump(wczytajUzytkownika($polaczenie, 99));
Oczekiwane: string(4) "Kama", int(250), NULL.
HINT
Wiersz jest najwyżej jeden, więc pętla while nie jest potrzebna. Wystarczy jedno wywołanie fetch_assoc() — co zwróci, gdy wierszy nie ma wcale?
ROZWIĄZANIE
function wczytajUzytkownika(mysqli $polaczenie, int $uzytkownikId): ?Uzytkownik
{
$zapytanie = $polaczenie->prepare(
'SELECT id, login, imie, xp, streak, data_ostatniej_aktywnosci FROM uzytkownicy WHERE id = ?'
);
$zapytanie->bind_param('i', $uzytkownikId);
$zapytanie->execute();
$wiersz = $zapytanie->get_result()->fetch_assoc();
if ($wiersz === null) {
return null;
}
return new Uzytkownik(
id: $wiersz['id'],
login: $wiersz['login'],
imieUzytkownika: $wiersz['imie'],
xp: $wiersz['xp'],
streak: $wiersz['streak'],
dataOstatniejAktywnosci: $wiersz['data_ostatniej_aktywnosci'],
);
}
$zapytanie->get_result()->fetch_assoc() — dwie metody „w łańcuchu”: get_result() zwraca obiekt wyniku, a na nim od razu wywołujemy fetch_assoc(). To skrót od dwóch linii z $wynik.
Krok 6 — logowanie sprawdza bazę
6a. Funkcja znajdzIdUzytkownika() (funkcje_bazy.php)
Logowanie potrzebuje odpowiedzi na pytanie: „jakie id ma użytkownik o tym loginie — i czy w ogóle istnieje?”.
function znajdzIdUzytkownika(mysqli $polaczenie, string $login): ?int
{
$zapytanie = $polaczenie->prepare('SELECT id FROM uzytkownicy WHERE login = ?');
$zapytanie->bind_param('s', $login);
$zapytanie->execute();
$wiersz = $zapytanie->get_result()->fetch_assoc();
if ($wiersz === null) {
return null;
}
return $wiersz['id'];
}
To dokładnie zapytanie z 3.5 — login pochodzi z formularza, więc prepared statement jest obowiązkowy. Sprawdź w brudnopisie: Kama_2026 → int(1), kama_2026 → int(1) (porównanie utf8mb4_unicode_ci nie rozróżnia wielkości liter — Test 2 z Lekcji 17), Nowy_1 → NULL.
6b. W sesji zostaje tylko uzytkownik_id
Od Lekcji 14 zapisywaliśmy w sesji login i imie. Teraz oba są w bazie. Gdybyśmy trzymali je także w sesji, mielibyśmy dwie kopie tych samych danych: zmiana imienia w bazie nie byłaby widoczna do ponownego logowania (to ta sama zasada „jedno źródło prawdy” co w Lekcjach 15 i 17). W sesji zapisujemy więc tylko identyfikator zalogowanego użytkownika, a resztę przy każdym żądaniu czytamy z bazy.
includes/funkcje_sesji.php — w czyZalogowany() zmień sprawdzany klucz:
function czyZalogowany(): bool
{
return isset($_SESSION['uzytkownik_id']);
}
Przy okazji: jeśli ktoś ma otwartą sesję sprzed tej zmiany (z login, ale bez uzytkownik_id), zostanie po prostu przekierowany do logowania. Bez tej zmiany index.php próbowałby odczytać nieistniejący klucz.
6c. walidujDaneLogowania() sprawdza już tylko login
Imię jest w bazie, więc znika z formularza. W includes/funkcje_tekstowe.php zastąp funkcję wersją z jednym parametrem:
function walidujDaneLogowania(string $login): array
{
$bledy = [];
if (!walidujLogin($login)) {
$bledy[] = 'Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.';
}
return $bledy;
}
Funkcja nadal zwraca tablicę komunikatów (reguła z Lekcji 13). W Lekcji 19 dostanie drugi parametr — hasło.
6d. zaloguj.php
Zmiany w części PHP (na górze pliku):
- Do
require_oncedopiszconfig/config.php(na początku listy) iincludes/funkcje_bazy.php(na końcu). - Usuń zmienną
$imieUzytkownika(wartość początkowa i odczyt z$_POST). - Dodaj wartość początkową
$bladBazy = null;obok$bledy = [];. - Blok obsługi POST zastąp:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$login = trim($_POST['login'] ?? '');
$bledy = walidujDaneLogowania($login);
if ($bledy === []) {
try {
$polaczenie = polaczZBaza();
$uzytkownikId = znajdzIdUzytkownika($polaczenie, $login);
if ($uzytkownikId === null) {
$bledy[] = 'Nie ma użytkownika o takim loginie.';
} else {
session_regenerate_id(true);
$_SESSION['uzytkownik_id'] = $uzytkownikId;
ustawKomunikat('Zalogowano. Powodzenia w zadaniach!');
header('Location: index.php');
exit;
}
} catch (Exception $wyjatek) {
$bladBazy = $wyjatek->getMessage();
}
}
}
Zmiany w części HTML:
- Zdanie pod
<h2>Logowanie</h2>→Wersja demonstracyjna: podaj login konta zapisanego w bazie — bez hasła. - Usuń z formularza
<label>i<input>polaimie. - Pod ramką „Popraw formularz:” dodaj ramkę awarii (ten sam wzór co
$bladZapisuwdodaj-zadanie.php):
<?php if ($bladBazy !== null) { ?>
<p class="blad">Nie udało się zalogować: <?= $bladBazy; ?></p>
<?php } ?>
Zwróć uwagę na podział:
- „nie ma takiego loginu” to błąd użytkownika → trafia do
$bledy(tablica, pokazana w ramce „Popraw formularz”), - „baza nie odpowiada” to awaria → wyjątek →
$bladBazy.
To ta sama reguła co od Lekcji 13: błąd użytkownika to nie wyjątek.
zaloguj.php nie dołącza klas — znajdzIdUzytkownika() zwraca liczbę, nie obiekt. Funkcje z funkcje_bazy.php, które tworzą obiekty, nie są tu wywoływane, więc brak klas nie przeszkadza.
Krok 7 — index.php czyta z bazy
Od tej chwili Twoja sesja z logowania „po staremu” nie wystarcza. Po zmianach wyloguj się i zaloguj ponownie.
7a. Dołączane pliki
Zamień require_once __DIR__ . '/includes/funkcje_plikow.php'; na require_once __DIR__ . '/includes/funkcje_bazy.php';. Strona główna nie czyta już CSV.
7b. Dane z bazy zamiast symulacji
Pod wymagajLogowania(); masz dziś dwa bloki: $uzytkownik = new Uzytkownik(...) z symulowanymi wartościami oraz try/catch z wczytajZadania(PLIK_ZADAN). Usuń oba i wstaw w ich miejsce:
// --- dane z bazy: zalogowany użytkownik i jego zadania ---
$uzytkownik = null;
$zadania = [];
$bladBazy = null;
try {
$polaczenie = polaczZBaza();
$uzytkownik = wczytajUzytkownika($polaczenie, $_SESSION['uzytkownik_id']);
if ($uzytkownik !== null) {
$zadania = wczytajZadaniaUzytkownika($polaczenie, $uzytkownik->pobierzId());
}
} catch (Exception $wyjatek) {
$bladBazy = $wyjatek->getMessage();
}
// --- awaria bazy: bez danych gracza nie ma czego pokazać ---
if ($bladBazy !== null) {
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
echo "<p class=\"blad\">Nie udało się wczytać danych: $bladBazy</p>";
require_once __DIR__ . '/includes/footer.php';
exit;
}
// --- konto usunięte z bazy, a sesja jeszcze istnieje ---
if ($uzytkownik === null) {
unset($_SESSION['uzytkownik_id']);
header('Location: zaloguj.php');
exit;
}
Dlaczego tak:
- Jeden
tryna całą bazę. Użytkownik i zadania pochodzą z tego samego połączenia. Jeśli baza nie działa, nie mamy ani jednego, ani drugiego. - Przy awarii strona kończy się wcześnie. Do Lekcji 17 awaria dotyczyła tylko pliku z zadaniami, więc reszta panelu (XP, poziom, streak) mogła się wyświetlić. Teraz dane gracza też są w bazie — bez nich nie ma czego pokazać. Zamiast owijać cały szablon w
if/else, wyświetlamy nagłówek, ramkę błędu, stopkę i kończymy skrypt (exit). - Sesja wskazuje na nieistniejące konto (np. ktoś usunął użytkownika w phpMyAdmin): usuwamy
uzytkownik_idz sesji i odsyłamy do logowania (Test 7). $_SESSION['uzytkownik_id']czytamy bez??— wyżej jestwymagajLogowania(), a wartość zapisał nasz kod (konwencja z Lekcji 14).
Wszystko poniżej (listy zadań, liczniki, $zadanieWykonane, metody grywalizacji, $uzytkownik->aktualizujStreak($zadanieWykonane), filtr widoku, komunikat) zostaje bez zmian — dostaje obiekty tego samego typu co wcześniej.
7c. Prezentacja
- Sekcja „Twoje zadania” była od Lekcji 12 owinięta w
<?php if ($bladZadan !== null) { ?> ... <?php } else { ?> ... <?php } ?>. Zmiennej$bladZadanjuż nie ma — awarię obsługuje blok z 7b. Usuń gałąźifz ramką błędu i same znacznikielse, zostaw zawartość gałęzielse. - Usuń link
<p><a href="dodaj-zadanie.php">+ Dodaj zadanie</a></p>. Formularz zapisuje jeszcze do CSV, więc dodane zadanie i tak nie pojawiłoby się na liście. Link wróci w Lekcji 19.
Reszta prezentacji bez zmian — „Witaj, …!”, „Zalogowano jako …”, XP i streak od Lekcji 15 pochodzą z metod $uzytkownik, więc same pokażą dane z bazy.
7d. Sprawdzenie
Wyloguj się, zaloguj jako Kama_2026. Strona powinna wyglądać tak jak przed lekcją (bez linku „+ Dodaj zadanie”). Potem zaloguj się jako Ola_TQ: 250 XP, poziom 3, jedno zadanie.
Krok 8 — diagnostyka.php i stopka
- W
diagnostyka.phpdo$wymaganePlikidopisz'includes/funkcje_bazy.php'(razem 19 ścieżek). Nic więcej — diagnostyka nadal nie łączy się z bazą (zasada z Lekcji 10 i 17: ma działać nawet przy zepsutym projekcie i bezconfig.php). includes/footer.php:TaskQuest 1.8.
6. Kolejne zadanie — ranking najlepszych graczy
Od pierwszych lekcji TaskQuest ma XP i poziomy, ale gracz nie wie, jak wypada na tle innych. Teraz, gdy w bazie jest wielu użytkowników, możemy to pokazać.
Pod listą zadań w index.php ma się pojawić:
Najlepsi gracze
1. Ola_TQ — 250 XP (poziom 3)
2. Kama_2026 — 99 XP (poziom 1)
Wymagania:
- W
funkcje_bazy.phpfunkcjapobierzRanking(mysqli $polaczenie, int $limit): array, która zwraca tablicę wierszy z kluczamiloginixp— najwyżej$limitgraczy, od największego XP. Przy równym XP — alfabetycznie po loginie (żeby kolejność była zawsze ta sama). $limitprzekazujemy parametrem zapytania (LIMIT ?), nie sklejamy.- W
index.phpranking dla 3 graczy wczytujemy w tym samymtryco użytkownika i zadania (zmienna$ranking, wartość początkowa[]). - Poziom w rankingu liczy istniejąca funkcja — nie dodajemy kolumny
poziomdo zapytania (Lekcja 17, 3.5: poziomu nie ma w bazie). - Sekcja
<h2>Najlepsi gracze</h2>z listą numerowaną<ol>, na samym końcu panelu, po liście zadań.
Tu nie zwracamy obiektów Uzytkownik: ranking potrzebuje dwóch kolumn, a obiekt wymagałby wszystkich sześciu. Wynik zapytania, który służy tylko do wyświetlenia, może zostać tablicą.
HINT
ORDER BY może mieć kilka kolumn rozdzielonych przecinkiem — druga decyduje, gdy pierwsza ma równe wartości. Liczbę w LIMIT ? podpinasz tak samo jak id.
POMOC
Zamiast pętli while możesz pobrać wszystkie wiersze naraz:
return $wynik->fetch_all(MYSQLI_ASSOC);
fetch_all(MYSQLI_ASSOC) zwraca tablicę wszystkich wierszy (każdy jako tablica asocjacyjna); dla pustego wyniku []. Tam, gdzie z wiersza budujemy obiekt, zostajemy przy while — tu wiersze wystarczą.
ROZWIĄZANIE
includes/funkcje_bazy.php:
function pobierzRanking(mysqli $polaczenie, int $limit): array
{
$zapytanie = $polaczenie->prepare('SELECT login, xp FROM uzytkownicy ORDER BY xp DESC, login LIMIT ?');
$zapytanie->bind_param('i', $limit);
$zapytanie->execute();
return $zapytanie->get_result()->fetch_all(MYSQLI_ASSOC);
}
index.php — w bloku „dane z bazy” dopisz $ranking = []; pod $zadania = [];, a w try, w if ($uzytkownik !== null), pod wczytaniem zadań:
$ranking = pobierzRanking($polaczenie, 3);
Na końcu treści panelu (po liście zadań, przed dołączeniem stopki):
<h2>Najlepsi gracze</h2>
<ol>
<?php foreach ($ranking as $gracz) { ?>
<li><?= $gracz['login']; ?> — <?= $gracz['xp']; ?> XP (poziom <?= obliczPoziom($gracz['xp']); ?>)</li>
<?php } ?>
</ol>
obliczPoziom() przyjmuje int — i dostaje int, bo wynik pochodzi z prepared statementu (3.5).
7. Testy
Gdzie: przeglądarka (http://localhost/taskquest/...) i phpMyAdmin (baza taskquest → zakładka SQL albo edycja wiersza). Przed każdym testem zapisz przewidywany wynik. Po testach, które zmieniają dane, uruchom ponownie cały sql/taskquest.sql.
Test 1 — regresja: Kama widzi to samo co przed lekcją
Zaloguj się jako Kama_2026. Porównaj z taskquest_lekcja17/index.php uruchomionym tego samego dnia (tam zaloguj się jako Kama_2026 z imieniem Kama).
Oczekiwane: te same zadania, liczniki, XP 99, poziom 1, ten sam streak i te same komunikaty terminów. Różnice: stopka 1.8, brak linku „+ Dodaj zadanie”, nowa sekcja „Najlepsi gracze”. Jeśli w Lekcji 17 (Test 8) dodałaś/dodałeś zadanie przez formularz, jest ono w CSV, ale nie w bazie — w wersji 1.8 go nie zobaczysz. To zgodne z oczekiwaniami.
Test 2 — Ola widzi swoje dane
Wyloguj się, zaloguj jako Ola_TQ.
| Co sprawdzasz | Oczekiwane |
|---|---|
| powitanie | „Witaj, Ola!” (imię z bazy — w formularzu już go nie podajesz) |
| XP / poziom | 250 XP, poziom 3, do poziomu 4 brakuje 50 XP |
| streak | 0 (data_ostatniej_aktywnosci = NULL → metoda nie zmienia streaka) |
| zadania | dokładnie jedno: „Zaprojektować bazę TaskQuest”, niewykonane |
| ranking | Ola na 1. miejscu, Kama na 2. |
Test 3 — logowanie: przypadki
| Login w formularzu | Oczekiwane |
|---|---|
Kama_2026 | zalogowano, strona główna, komunikat „Zalogowano. Powodzenia w zadaniach!” |
kama_2026 | zalogowano jako Kama — „Zalogowano jako Kama_2026” (pisownia z bazy) |
Nowy_1 | formularz z błędem „Nie ma użytkownika o takim loginie.”, pole wypełnione Nowy_1 |
Ka | „Login musi mieć 3–20 znaków…” — do bazy w ogóle nie trafia zapytanie |
' OR '1'='1 | ten sam komunikat o formacie loginu (walidujLogin() odrzuca apostrof i spację) |
Przewidź: czy ' OR '1'='1 zalogowałby kogoś, gdyby usunąć walidację, a znajdzIdUzytkownika() zostawić z prepared statementem? (Odpowiedź: nie — patrz 3.5. Sprawdzisz to w Eksperymencie 1.)
Test 4 — dane żyją w bazie, nie w kodzie
Zalogowana/zalogowany jako Kama, w phpMyAdmin kliknij tabelę uzytkownicy → przy Kamie Edytuj → zmień xp na 150 i imie na Kamila → Wykonaj. Odśwież TaskQuest bez ponownego logowania.
Oczekiwane: „Witaj, Kamila!”, 150 XP, poziom 2, a w rankingu Kama nadal za Olą. Zmiana imienia jest widoczna od razu, bo w sesji jest tylko uzytkownik_id (6b). Zmień w phpMyAdmin tytuł jednego zadania Kamy i odśwież — nowy tytuł na liście. Uruchom ponownie taskquest.sql.
Test 5 — każdy widzi tylko swoje zadania
Zalogowana/zalogowany jako Ola, spróbuj wszystkich trzech widoków (?filtr=wszystkie, aktywne, wykonane). W żadnym nie ma zadań Kamy. Widok „wykonane” pokazuje „Brak zadań w tym widoku.”
Test 6 — baza nie działa
- Zalogowana/zalogowany jako Kama, zatrzymaj w XAMPP moduł MySQL i odśwież
index.php.
Oczekiwane: nagłówek, ramka „Nie udało się wczytać danych: Nie udało się połączyć z bazą danych.”, stopka. Żadnych danych gracza. (Jeśli nad ramką widzisz dodatkowoWarning, to ustawienia wyświetlania komunikatów PHP — wrócimy do tego w Lekcji 23.) - Wyloguj się i spróbuj zalogować. Oczekiwane: formularz z ramką „Nie udało się zalogować: Nie udało się połączyć z bazą danych.”
diagnostyka.php— działa normalnie (19 ×[OK]), bo nie łączy się z bazą. Czy to dobrze, czy źle? Zapisz swoją odpowiedź — wrócisz do niej w pytaniach kontrolnych.- Uruchom MySQL ponownie.
Test 7 — konto usunięte w trakcie sesji
Zaloguj się jako Ola. W phpMyAdmin: DELETE FROM uzytkownicy WHERE login = 'Ola_TQ';. Odśwież TaskQuest.
Oczekiwane: przekierowanie na stronę logowania (bez błędu PHP). Próba zalogowania jako Ola_TQ → „Nie ma użytkownika o takim loginie.” Uruchom ponownie taskquest.sql.
Test 8 — ranking i LIMIT
W phpMyAdmin:
INSERT INTO uzytkownicy (login, imie, xp) VALUES ('Kuba_TQ', 'Kuba', 180), ('Zosia_TQ', 'Zosia', 180);
Przewidź kolejność w rankingu, potem odśwież TaskQuest. Oczekiwane: Ola_TQ (250), Kuba_TQ (180), Zosia_TQ (180) — tylko trzy, Kama (99) nie mieści się w LIMIT. Kuba przed Zosią, bo przy równym XP decyduje login. Uruchom ponownie taskquest.sql.
Test 9 — diagnostyka
diagnostyka.php → 19 × [OK] (w tym includes/funkcje_bazy.php), sekcja „Baza danych” bez zmian.
8. Obowiązkowe eksperymenty
Gdzie: eksperymenty 1–2 w brudnopisie, 3–4 w projekcie. Po każdym przywróć zmieniony kod.
Eksperyment 1: prepared statement bez walidacji
W brudnopisie:
<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
$polaczenie = polaczZBaza();
var_dump(znajdzIdUzytkownika($polaczenie, "' OR '1'='1"));
var_dump(znajdzIdUzytkownika($polaczenie, "Kama_2026' -- "));
Przewidź oba wyniki. Oczekiwane: NULL, NULL. Drugi ciąg to klasyczna próba „ucięcia” reszty zapytania komentarzem SQL (-- ). Przy sklejaniu dałaby zalogowanie jako Kama; w prepared statemencie to zwykły, nieistniejący login. Wniosek: walidacja w zaloguj.php jest wygodą i porządkiem, ale bezpieczeństwo zapytania zapewnia prepared statement.
Eksperyment 2: te same dane, dwa typy
W brudnopisie porównaj:
$wynik = $polaczenie->query('SELECT id FROM uzytkownicy WHERE login = \'Kama_2026\'');
$wierszZQuery = $wynik->fetch_assoc();
$login = 'Kama_2026';
$zapytanie = $polaczenie->prepare('SELECT id FROM uzytkownicy WHERE login = ?');
$zapytanie->bind_param('s', $login);
$zapytanie->execute();
$wierszZPrepare = $zapytanie->get_result()->fetch_assoc();
var_dump($wierszZQuery['id'], $wierszZPrepare['id']);
var_dump($wierszZQuery['id'] === 1, $wierszZPrepare['id'] === 1);
Oczekiwane: string(1) "1", int(1), a potem false, true. Gdyby TaskQuest używał query(), porównania === z liczbami przestałyby działać, a obiekty dostawałyby teksty zamiast liczb (bez błędu — tryb domyślny typów z Lekcji 15 po cichu zamienia '1' na 1). To jeszcze jeden powód, żeby w TaskQuest zawsze używać prepared statements.
Eksperyment 3: literówka w zapytaniu
W wczytajZadaniaUzytkownika() zmień w zapytaniu tytul na tytl i odśwież index.php.
Oczekiwane: ramka „Nie udało się wczytać danych: Unknown column 'tytl’ in 'field list’”. Programistę ten komunikat ratuje — od razu wiadomo, co poprawić. Ale użytkownik widzi nazwę kolumny, czyli fragment budowy bazy. Porównaj z komunikatem połączenia, który celowo ukryliśmy (Eksperyment 4). Na etapie nauki zostawiamy go widocznym; ukrywanie szczegółów przed użytkownikiem i zapis błędów do logu — Lekcja 22 i 23. Przywróć tytul.
Eksperyment 4: po co ukrywamy komunikat połączenia
- W
config/config.phpustawBAZA_HASLOna'zlehaslo'. Odświeżindex.php. Oczekiwane: „Nie udało się połączyć z bazą danych.” - W
polaczZBaza()tymczasowo zamień linięthrow new Exception('Nie udało się połączyć z bazą danych.');nathrow $wyjatek;(ponowne rzucenie oryginalnego wyjątku). Odśwież.
Oczekiwane: komunikat w rodzajuAccess denied for user 'root'@'localhost' (using password: YES)— w przeglądarce użytkownika: nazwa konta bazy i adres serwera. - Przywróć obie zmiany i sprawdź, że TaskQuest działa.
Eksperyment 5: filtr właściciela
W wczytajZadaniaUzytkownika() usuń z zapytania WHERE uzytkownik_id = ? oraz linię bind_param(...) (bez parametru nie ma czego podpinać). Zaloguj się jako Ola.
Oczekiwane: Ola widzi zadania Kamy. To dokładnie problem z Lekcji 17 („Kuba i Ola widzą te same zadania”) — tylko że tym razem to nie ograniczenie CSV, lecz błąd w zapytaniu. Baza oddaje to, o co prosimy; pilnowanie, czyje dane pokazujemy, należy do nas. Przywróć WHERE i bind_param().
9. ROZWIĄZANIE
Poniżej wszystkie pliki, które zmieniły się w Lekcji 18. Pliki niewymienione są identyczne jak po Lekcji 17 (w PHP: jak po Lekcji 15, wersja w 15_TaskQuest.md, sekcja 9; sql/taskquest.sql jak w 17_TaskQuest.md). Przy plikach, w których zmienia się tylko fragment, podany jest dokładny zakres zmiany.
ROZWIĄZANIE
config/config.php — dopisane na końcu
// (L18) dane dostępowe do bazy danych — domyślne ustawienia XAMPP
define('BAZA_HOST', 'localhost');
define('BAZA_UZYTKOWNIK', 'root');
define('BAZA_HASLO', '');
define('BAZA_NAZWA', 'taskquest');
includes/funkcje_bazy.php (nowy plik)
<?php
// Funkcje TaskQuest korzystające z bazy danych (Lekcja 18).
// Wymagają wcześniejszego dołączenia config/config.php (stałe BAZA_...).
// Funkcje tworzące obiekty wymagają też classes/Zadanie.php i classes/Uzytkownik.php.
function polaczZBaza(): mysqli
{
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$polaczenie = new mysqli(BAZA_HOST, BAZA_UZYTKOWNIK, BAZA_HASLO, BAZA_NAZWA);
} catch (Exception $wyjatek) {
throw new Exception('Nie udało się połączyć z bazą danych.');
}
$polaczenie->set_charset('utf8mb4');
return $polaczenie;
}
function wczytajZadaniaUzytkownika(mysqli $polaczenie, int $uzytkownikId): array
{
$zapytanie = $polaczenie->prepare(
'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE uzytkownik_id = ? ORDER BY id'
);
$zapytanie->bind_param('i', $uzytkownikId);
$zapytanie->execute();
$wynik = $zapytanie->get_result();
$zadania = [];
while ($wiersz = $wynik->fetch_assoc()) {
$zadania[] = new Zadanie(
id: $wiersz['id'],
tytul: $wiersz['tytul'],
opis: $wiersz['opis'],
termin: $wiersz['termin'],
wykonane: $wiersz['data_wykonania'] !== null,
xp: $wiersz['xp'],
);
}
return $zadania;
}
function wczytajUzytkownika(mysqli $polaczenie, int $uzytkownikId): ?Uzytkownik
{
$zapytanie = $polaczenie->prepare(
'SELECT id, login, imie, xp, streak, data_ostatniej_aktywnosci FROM uzytkownicy WHERE id = ?'
);
$zapytanie->bind_param('i', $uzytkownikId);
$zapytanie->execute();
$wiersz = $zapytanie->get_result()->fetch_assoc();
if ($wiersz === null) {
return null;
}
return new Uzytkownik(
id: $wiersz['id'],
login: $wiersz['login'],
imieUzytkownika: $wiersz['imie'],
xp: $wiersz['xp'],
streak: $wiersz['streak'],
dataOstatniejAktywnosci: $wiersz['data_ostatniej_aktywnosci'],
);
}
function znajdzIdUzytkownika(mysqli $polaczenie, string $login): ?int
{
$zapytanie = $polaczenie->prepare('SELECT id FROM uzytkownicy WHERE login = ?');
$zapytanie->bind_param('s', $login);
$zapytanie->execute();
$wiersz = $zapytanie->get_result()->fetch_assoc();
if ($wiersz === null) {
return null;
}
return $wiersz['id'];
}
function pobierzRanking(mysqli $polaczenie, int $limit): array
{
$zapytanie = $polaczenie->prepare('SELECT login, xp FROM uzytkownicy ORDER BY xp DESC, login LIMIT ?');
$zapytanie->bind_param('i', $limit);
$zapytanie->execute();
return $zapytanie->get_result()->fetch_all(MYSQLI_ASSOC);
}
classes/Uzytkownik.php — zmiany
- nowa pierwsza właściwość
private int $id;, private ?string $dataOstatniejAktywnosci;(byłostring),- konstruktor:
__construct(int $id, string $login, string $imieUzytkownika, int $xp, int $streak, ?string $dataOstatniejAktywnosci), pierwsza linia ciała$this->id = $id;, - nowa metoda
pobierzId(): int(pod konstruktorem), - metoda
aktualizujStreak(bool $zadanieWykonane): void— na początku ciałaif ($this->dataOstatniejAktywnosci === null) { return; }(kod w kroku 4).
Pozostałe metody bez zmian.
includes/funkcje_sesji.php — czyZalogowany()
function czyZalogowany(): bool
{
return isset($_SESSION['uzytkownik_id']);
}
includes/funkcje_tekstowe.php — walidujDaneLogowania()
function walidujDaneLogowania(string $login): array
{
$bledy = [];
if (!walidujLogin($login)) {
$bledy[] = 'Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.';
}
return $bledy;
}
zaloguj.php — część PHP (cała, do dołączenia header.php)
<?php
session_start();
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
if (czyZalogowany()) {
header('Location: index.php');
exit;
}
$login = '';
$bledy = [];
$bladBazy = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$login = trim($_POST['login'] ?? '');
$bledy = walidujDaneLogowania($login);
if ($bledy === []) {
try {
$polaczenie = polaczZBaza();
$uzytkownikId = znajdzIdUzytkownika($polaczenie, $login);
if ($uzytkownikId === null) {
$bledy[] = 'Nie ma użytkownika o takim loginie.';
} else {
session_regenerate_id(true);
$_SESSION['uzytkownik_id'] = $uzytkownikId;
ustawKomunikat('Zalogowano. Powodzenia w zadaniach!');
header('Location: index.php');
exit;
}
} catch (Exception $wyjatek) {
$bladBazy = $wyjatek->getMessage();
}
}
}
$nazwaStrony = 'Logowanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
część HTML (cała, od końca części PHP do końca pliku)
Wklej bezpośrednio pod linią require_once __DIR__ . '/includes/header.php';. Obie części razem to cały plik `zaloguj.php
?>
<h2>Logowanie</h2>
<p>Wersja demonstracyjna: podaj login konta zapisanego w bazie — bez hasła.</p>
<?php if ($bledy !== []) { ?>
<div class="blad">
<p>Popraw formularz:</p>
<ul>
<?php foreach ($bledy as $blad) { ?>
<li><?= $blad; ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($bladBazy !== null) { ?>
<p class="blad">Nie udało się zalogować: <?= $bladBazy; ?></p>
<?php } ?>
<form method="post" action="zaloguj.php">
<label for="login">Login</label>
<input type="text" id="login" name="login" maxlength="20" required value="<?= $login; ?>">
<button type="submit">Wejdź do TaskQuest</button>
</form>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
index.php — zmiany
require_onceplikuincludes/funkcje_plikow.phpzastąpionerequire_once __DIR__ . '/includes/funkcje_bazy.php';.- Bloki „dane użytkownika” (
new Uzytkownik(...)z wartościami symulowanymi) i „dane zadań” (try/catchzwczytajZadania(PLIK_ZADAN)) zastąpione blokiem:
// --- dane z bazy: zalogowany użytkownik, jego zadania i ranking ---
$uzytkownik = null;
$zadania = [];
$ranking = [];
$bladBazy = null;
try {
$polaczenie = polaczZBaza();
$uzytkownik = wczytajUzytkownika($polaczenie, $_SESSION['uzytkownik_id']);
if ($uzytkownik !== null) {
$zadania = wczytajZadaniaUzytkownika($polaczenie, $uzytkownik->pobierzId());
$ranking = pobierzRanking($polaczenie, 3);
}
} catch (Exception $wyjatek) {
$bladBazy = $wyjatek->getMessage();
}
// --- awaria bazy: bez danych gracza nie ma czego pokazać ---
if ($bladBazy !== null) {
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
echo "<p class=\"blad\">Nie udało się wczytać danych: $bladBazy</p>";
require_once __DIR__ . '/includes/footer.php';
exit;
}
// --- konto usunięte z bazy, a sesja jeszcze istnieje ---
if ($uzytkownik === null) {
unset($_SESSION['uzytkownik_id']);
header('Location: zaloguj.php');
exit;
}
- Wszystko od bloku wywołań (listy, liczniki,
$zadanieWykonane, metody grywalizacji, streak) do komunikatu jednorazowego — bez zmian. - Prezentacja: usunięty link „+ Dodaj zadanie”, usunięte
if ($bladZadan !== null) { ... } else {wraz z zamknięciem (zawartość gałęzielsezostaje), na końcu panelu dodana sekcja:
<h2>Najlepsi gracze</h2>
<ol>
<?php foreach ($ranking as $gracz) { ?>
<li><?= $gracz['login']; ?> — <?= $gracz['xp']; ?> XP (poziom <?= obliczPoziom($gracz['xp']); ?>)</li>
<?php } ?>
</ol>
diagnostyka.php
W $wymaganePliki dopisane 'includes/funkcje_bazy.php' (razem 19 ścieżek). Reszta bez zmian.
includes/footer.php
<p>TaskQuest 1.8</p>
</div>
</body>
</html>
10. Co zmieniliśmy w TaskQuest?
Zostaje
klasa Zadanie (bez zmian), wszystkie funkcje listy, grywalizacji, terminów, walidujLogin(), wyczyscTytul(), skrocOpis()
index.php: liczniki, filtr widoku + cookie, pasek XP, komunikat jednorazowy, lista z .po-terminie
sesja, session_regenerate_id(), wyloguj.php, wymagajLogowania(), ustawKomunikat(), pobierzKomunikat()
baza taskquest i sql/taskquest.sql (bez zmian)
dodaj-zadanie.php, data/zadania.csv, wczytajZadania(), zapiszZadania(), sprawdzWierszZadania(), PLIK_ZADAN
— zostają do Lekcji 19 (formularz nadal zapisuje do CSV; wyłączony z nawigacji)
$zadanieWykonane — nadal uproszczenie (prawdziwy streak w L19)
Dochodzi
config.php: BAZA_HOST, BAZA_UZYTKOWNIK, BAZA_HASLO, BAZA_NAZWA
includes/funkcje_bazy.php:
polaczZBaza(): mysqli
wczytajZadaniaUzytkownika(mysqli, int): array — tablica obiektów Zadanie
wczytajUzytkownika(mysqli, int): ?Uzytkownik
znajdzIdUzytkownika(mysqli, string): ?int
pobierzRanking(mysqli, int): array — wiersze ['login', 'xp']
Uzytkownik: właściwość id, pobierzId()
$_SESSION['uzytkownik_id']
index.php: $bladBazy, $ranking, sekcja „Najlepsi gracze”
zaloguj.php: $bladBazy, $uzytkownikId
Nowe narzędzia: SQL SELECT kolumny FROM ... WHERE ... AND ... IS NULL ... ORDER BY ... DESC ... LIMIT; mysqli_report(), new mysqli(...), set_charset(), query(), prepare(), bind_param(), execute(), get_result(), fetch_assoc(), fetch_all(MYSQLI_ASSOC), num_rows; typy ?string, ?int, ?Klasa; pojęcie SQL injection.
Zmieniamy
Uzytkownik:dataOstatniejAktywnoscitypu?string; konstruktor zidna początku;aktualizujStreak()nic nie robi, gdy data tonull,czyZalogowany(): sprawdzauzytkownik_id,walidujDaneLogowania(): jeden parametr (login),zaloguj.php: login musi istnieć w bazie; formularz bez imienia,index.php: dane z bazy, obsługa awarii bazy i usuniętego konta, bez linku do formularza, ranking,diagnostyka.php: 19 plików; stopka1.8.
Usuwamy
| Co | Dlaczego |
|---|---|
wartości symulowane xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14' w index.php | dane gracza są w bazie; te same wartości ma Kama w taskquest.sql |
$bladZadan i owinięcie sekcji zadań w if/else | awarię bazy obsługuje $bladBazy dla całej strony |
$_SESSION['login'], $_SESSION['imie'] | kopie danych z bazy mogłyby się rozjechać; w sesji tylko identyfikator |
pole „imię” w formularzu logowania i $imieUzytkownika w zaloguj.php | imię jest w bazie; użytkownik nie „deklaruje” go przy logowaniu |
reguły imienia w walidujDaneLogowania() | j.w. |
dołączanie funkcje_plikow.php w index.php | strona główna nie czyta CSV |
| link „+ Dodaj zadanie” (tymczasowo) | formularz zapisuje do CSV — zadanie nie pojawiłoby się na liście; wraca w L19 |
Co nowego potrafi TaskQuest po tej lekcji?
Po Lekcji 18 TaskQuest łączy się z bazą danych i czyta z niej wszystko, co pokazuje na stronie głównej. Logowanie sprawdza, czy konto istnieje. Każdy użytkownik widzi własne imię, XP, poziom, streak i wyłącznie własne zadania, a ranking pokazuje trzech najlepszych graczy. Wszystkie zapytania z danymi z zewnątrz są odporne na SQL injection dzięki prepared statements. Gdy baza nie działa, użytkownik dostaje czytelny komunikat zamiast błędu PHP.
11. Zadania końcowe — rozbuduj TaskQuest samodzielnie
Nie są częścią wersji referencyjnej.
A. Wyszukiwarka zadań. Nad listą zadań formularz method="get" z polem szukaj. Po wysłaniu lista pokazuje tylko zadania zalogowanego użytkownika, których tytuł zawiera wpisany tekst. Nowa funkcja szukajZadanUzytkownika(mysqli $polaczenie, int $uzytkownikId, string $fraza): array z warunkiem WHERE uzytkownik_id = ? AND tytul LIKE ?. Znaki % wokół frazy dodaj w PHP przed bind_param() ('%' . $fraza . '%'), nie w tekście zapytania. Przetestuj frazy: PHP, php (czy wielkość liter ma znaczenie? dlaczego?), ' OR '1'='1, pusty tekst. Pytanie dodatkowe: co znajdzie fraza %? Czy to błąd bezpieczeństwa, czy tylko niespodzianka?
B. Statystyki liczone przez bazę. Jednym zapytaniem pobierz dla zalogowanego użytkownika liczbę aktywnych zadań i sumę ich XP: SELECT COUNT(*) AS liczba, SUM(xp) AS suma FROM zadania WHERE uzytkownik_id = ? AND data_wykonania IS NULL. Wyświetl obok liczników z PHP i sprawdź, czy wyniki są równe $liczbaAktywnych i $xpDoZdobycia. Co zwraca SUM(xp), gdy aktywnych zadań nie ma (zaloguj się jako użytkownik bez zadań z Testu 8)? Kiedy liczenie w bazie jest lepsze niż w PHP?
C. Moje miejsce w rankingu. Pod rankingiem: „Twoje miejsce: N”. Podpowiedź: miejsce to liczba graczy z większym XP plus 1 (SELECT COUNT(*) ... WHERE xp > ?). Dodatkowo wyróżnij (<strong>) wiersz zalogowanego użytkownika w rankingu. Jak potraktujesz dwóch graczy z równym XP?
Sprzątanie
- Usuń
brudnopis.php. - Sprawdź, że przywrócone są:
tytulw zapytaniu,BAZA_HASLO='',throw new Exception(...)wpolaczZBaza(),WHERE uzytkownik_id = ?ibind_param()(Eksperymenty 3–5). - Uruchom ponownie
sql/taskquest.sql. - Zaloguj się jako Kama i jako Ola — Testy 1 i 2 mają przechodzić.
Do samodzielnego sprawdzenia
[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Potrafię napisać SELECT z wybranymi kolumnami, WHERE, AND, IS NULL, ORDER BY i LIMIT.
[ ] Wiem, dlaczego WHERE data_wykonania = NULL zwraca zero wierszy.
[ ] Potrafię połączyć się z bazą obiektem mysqli i wiem, co robi mysqli_report(...STRICT).
[ ] Rozróżniam $polaczenie, $zapytanie, $wynik i $wiersz.
[ ] Potrafię pokazać na przykładzie, na czym polega SQL injection.
[ ] Potrafię napisać prepared statement z jednym i z dwoma parametrami (typy s, i).
[ ] Wiem, dlaczego z prepared statementu id przychodzi jako int, a z query() jako tekst.
[ ] Potrafię zamienić wiersz z bazy w obiekt Zadanie / Uzytkownik.
[ ] Wiem, co znaczy ?string i dlaczego Uzytkownik go potrzebuje.
[ ] Wiem, dlaczego w sesji jest tylko uzytkownik_id.
[ ] Kama i Ola widzą po zalogowaniu swoje dane; ranking działa.
[ ] Przy wyłączonej bazie TaskQuest pokazuje komunikat, a nie błąd PHP.
[ ] Usunęłam/usunąłem brudnopis.
Pytania kontrolne
Co zwróci każde zapytanie (liczba wierszy), gdy w bazie są tylko dane testowe z taskquest.sql?
SELECT tytul FROM zadania WHERE uzytkownik_id = 2 AND data_wykonania IS NOT NULL;
SELECT login FROM uzytkownicy ORDER BY xp LIMIT 1;
SELECT tytul FROM zadania WHERE data_wykonania = NULL;
Kolega napisał:
$polaczenie->query("SELECT id FROM uzytkownicy WHERE login = '$login'") i twierdzi, że to bezpieczne, bo $login przeszedł walidujLogin(). Czy ma rację w tym jednym miejscu? Dlaczego mimo to w TaskQuest tak nie piszemy?Znajdź dwa błędy:
$zapytanie = $polaczenie->prepare('SELECT id, tytul FROM zadania WHERE uzytkownik_id = ? AND termin < ?');
$zapytanie->bind_param('i', $uzytkownikId, $dzisiaj);
$zapytanie->execute();
while ($wiersz = $zapytanie->fetch_assoc()) { ... }
wczytajUzytkownika($polaczenie, 7) zwraca null. Czy to awaria? Czym ta sytuacja różni się od wyłączonego serwera MySQL i jak każdą z nich obsługuje index.php?Dlaczego
polaczZBaza() łapie wyjątek i rzuca nowy z innym komunikatem, a wczytajZadaniaUzytkownika() nie łapie niczego?W sesji jest tylko
uzytkownik_id. Nauczycielka zmienia w phpMyAdmin imię ucznia. Kiedy uczeń zobaczy nowe imię — i jak byłoby, gdyby imię nadal było w sesji?Dlaczego właściwość
dataOstatniejAktywnosci ma typ ?string, a login zwykły string? Wskaż kolumny w taskquest.sql, które o tym decydują.Ranking zwraca tablice
['login' => ..., 'xp' => ...], a wczytajUzytkownika() obiekt Uzytkownik. Dlaczego ta różnica jest uzasadniona? Skąd ranking bierze poziom gracza, skoro w zapytaniu go nie ma?Bez
WHERE uzytkownik_id = ? Ola widzi zadania Kamy (Eksperyment 5). Czy prepared statement chroni przed tym błędem? Czym ten błąd różni się od SQL injection?Przy wyłączonym MySQL
diagnostyka.php pokazuje 19 × [OK]. Czy diagnostyka powinna sprawdzać połączenie z bazą? Podaj argument za i przeciw (wskazówka: zasada z Lekcji 10 i miejsce, w którym są dane dostępowe).
PODSUMOWANIE LEKCJI
W skrócie
Cztery różne rzeczy, których nie wolno mylić:
$polaczenie obiekt mysqli — połączenie z serwerem
$zapytanie obiekt mysqli_stmt — przygotowane polecenie (prepare)
$wynik obiekt mysqli_result — zbiór wierszy do odczytania
$wiersz tablica asocjacyjna — jeden wiersz; null, gdy wierszy już nie ma
Metody nie są wymienne: num_rows i fetch_assoc() należą do wyniku, bind_param() i execute() do zapytania, prepare() i set_charset() do połączenia.
SQL:
- Kolejność klauzul jest stała:
SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT. Zamiana miejscami = błąd składni. - Do
NULLtylkoIS NULL/IS NOT NULL.= NULLzwraca zero wierszy bez żadnego błędu — to najtrudniejszy do znalezienia z tych błędów. - Wymieniaj kolumny zamiast
SELECT *— nie przeciągasz do PHP danych, których nie potrzebujesz (np.haslo_hash). - Zero wierszy to poprawny wynik, nie awaria. Awaria to brak połączenia albo błąd składni.
Bezpieczeństwo:
- Każda wartość pochodząca z zewnątrz (
$_GET,$_POST, cookie, sesja) trafia do zapytania tylko przez prepared statement. Sklejanie"... = '$login'"jest zakazane. - Walidacja to pierwsza linia obrony, nie zabezpieczenie zapytania. Zapytanie ma być bezpieczne samo z siebie.
- Przez
?/:nazwaprzekazujesz wartości, nigdy nazw tabel, nazw kolumn ani kierunku sortowania. Tam działa wyłącznie whitelist (in_array()+ wartość domyślna). - Prepared statement chroni przed SQL injection, ale nie przed brakiem
WHERE uzytkownik_id = ?. Cudze dane pokazane właściwemu zapytaniu to błąd logiki, nie podatności. htmlspecialchars()przy wyświetlaniu — to osobna ochrona (XSS), niezależna od prepared statements.
Szczegóły, które zaskakują:
query()zwraca kolumnyINTjako teksty, prepared statement (get_result()) — jakoint. Daty zawsze jako tekstRRRR-MM-DD,NULLjakonull.bind_param()przyjmuje zmienne, nie wartości:bind_param('i', 5)kończy się błędem.- Liczba liter w typach musi być równa liczbie znaków
?. - Znaki
%przyLIKEdodajesz w PHP do zmiennej, nie do tekstu zapytania. - W PDO liczbę wierszy zwróconych przez
SELECTliczymy przezcount($wyniki)pofetchAll(), afetch()po ostatnim wierszu zwracafalse(wmysqlifetch_assoc()zwracanull).
Co trzeba umieć na INF.03?
- połączyć skrypt z bazą na warunkach podanych w arkuszu:
localhost, użytkownikrootbez hasła, wskazana nazwa bazy; - zamknąć połączenie na końcu skryptu — arkusz wymienia to jako osobne wymaganie wobec skryptów, więc brak zamknięcia to strata punktu, mimo że PHP zamyka połączenie sam;
- napisać
SELECTz wybranymi kolumnami i warunkiemWHERE(arkusz: „wybierające jedynie polaidObiekt,panstwo,nazwa,wartoscCechy… dlaidRodzaj = 10iidKontynent = 6”) i zapisać zapytania w pliku tekstowym; - wypełnić tabelę HTML wierszami z bazy — pętla po wyniku, w każdym obrocie jeden
<tr>z komórkami z kolejnych pól; - wypełnić listę rozwijaną danymi z bazy —
<option value="idTurysta">nick</option>dla każdego wiersza; - wygenerować odnośniki z parametrem GET na podstawie wyniku zapytania (
index.php?idKontynent=6) i odebrać ten parametr z wartością domyślną, gdy go nie ma; - rozpoznać w arkuszu tabelę funkcji
mysqli_connect,mysqli_query,mysqli_fetch_row,mysqli_fetch_array,mysqli_num_rows,mysqli_num_fields,mysqli_error,mysqli_close,isseti umieć z niej skorzystać.
Minimum, które powinno wejść w pamięć
Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:
SELECT kolumny FROM tabela WHERE warunek ORDER BY kolumna LIMIT n— kolejność stała.IS NULL/IS NOT NULL.= NULLzawsze daje zero wierszy i nie zgłasza błędu.- Schemat obiektowy:
new mysqli(...)→prepare('... WHERE x = ?')→bind_param('i', $x)→execute()→get_result()→while ($wiersz = $wynik->fetch_assoc()). - Typy w
bind_param():stekst,iliczba całkowita,dliczba z przecinkiem. Tyle liter, ile znaków?, w tej samej kolejności. ?zastępuje wartość. Nazwa kolumny wORDER BYidzie przez whitelistę.- Dane z formularza lub adresu nigdy nie są doklejane do tekstu zapytania.
fetch_assoc()zwraca kolejny wiersz albonull— dlatego działawhile ($wiersz = ...).- Przy wyświetlaniu danych z bazy:
htmlspecialchars(). - Na egzaminie:
localhost,root, bez hasła, na końcu zamknięcie połączenia.
Ściągawka – najważniejsze polecenia i składnia
Połączenie — obiektowo, błędy jako wyjątki (styl TaskQuest)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$polaczenie = new mysqli('localhost', 'root', '', 'nazwa_bazy');
$polaczenie->set_charset('utf8mb4');
// ...
$polaczenie->close();
Połączenie — obiektowo, błąd sprawdzany warunkiem (styl z części teoretycznej)
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'nazwa_bazy');
if ($db->connect_error) { die("Błąd połączenia: {$db->connect_error}"); }
Połączenie — PDO
$pdo = new PDO("mysql:host=localhost;dbname=nazwa_bazy;charset=utf8mb4", 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$pdo = null; // zamknięcie
SELECT
SELECT tytul, termin FROM zadania
WHERE uzytkownik_id = 1 AND data_wykonania IS NULL
ORDER BY termin DESC
LIMIT 10;
Odczyt wyniku — mysqli
$zapytanie = $polaczenie->prepare('SELECT id, tytul FROM zadania WHERE uzytkownik_id = ?');
$zapytanie->bind_param('i', $uzytkownikId);
$zapytanie->execute();
$wynik = $zapytanie->get_result();
echo $wynik->num_rows;
while ($wiersz = $wynik->fetch_assoc()) {
echo htmlspecialchars($wiersz['tytul']);
}
$zapytanie->close();
Odczyt wyniku — PDO
$stmt = $pdo->prepare('SELECT id, tytul FROM zadania WHERE uzytkownik_id = :id');
$stmt->execute(['id' => $uzytkownikId]);
$wiersze = $stmt->fetchAll(); // wszystkie naraz -> count($wiersze)
$wiersz = $stmt->fetch(); // po jednym; false, gdy koniec
Dwa parametry, LIKE, LIMIT, agregacje
$zapytanie = $polaczenie->prepare('SELECT id FROM zadania WHERE uzytkownik_id = ? AND tytul LIKE ?');
$fraza = '%' . $szukane . '%'; // % w PHP, nie w SQL
$zapytanie->bind_param('is', $uzytkownikId, $fraza);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT); // LIMIT w PDO wymaga typu
SELECT COUNT(*) AS liczba, SUM(xp) AS suma, AVG(xp) AS srednia, MAX(xp) AS maks
FROM zadania WHERE uzytkownik_id = 1;
Whitelist dla sortowania
$sort = $_GET['sort'] ?? 'xp';
$dozwolone = ['xp', 'login', 'streak'];
if (!in_array($sort, $dozwolone)) { $sort = 'xp'; }
$sql = "SELECT login, xp FROM uzytkownicy ORDER BY {$sort} DESC";
Mapowanie: zapis obiektowy ↔ funkcje z arkusza egzaminacyjnego
| Obiektowo | Proceduralnie (tabela z arkusza) |
|---|---|
new mysqli($h, $u, $p, $b) | mysqli_connect($h, $u, $p, $b) |
$db->query($sql) | mysqli_query($db, $sql) |
$wynik->fetch_assoc() | mysqli_fetch_array($wynik) |
— (odpowiednik: fetch_row()) | mysqli_fetch_row($wynik) — tablica numeryczna |
$wynik->num_rows | mysqli_num_rows($wynik) |
$db->error | mysqli_error($db) |
$db->close() | mysqli_close($db) |
mysqli_fetch_array() domyślnie zwraca klucze i numeryczne, i tekstowe (MYSQLI_BOTH) — stąd w arkuszu działa zarówno $w['nazwa'], jak i $w[1].
Quiz – sprawdź się
Ile wierszy zwróci każde zapytanie, jeżeli w tabeli
zadaniasą 4 wiersze Kamy (w tym 1 wykonane) i 1 wiersz Oli (niewykonany)?SELECT tytul FROM zadania WHERE uzytkownik_id = 1 AND data_wykonania IS NULL;
SELECT tytul FROM zadania WHERE data_wykonania = NULL;
SELECT tytul FROM zadania ORDER BY termin LIMIT 2;Dlaczego to zapytanie nie zadziała i jak je poprawić?
SELECT login FROM uzytkownicy ORDER BY xp DESC WHERE xp > 100;Co jest źle?
$zapytanie = $polaczenie->prepare("SELECT id FROM uzytkownicy WHERE login = '?'");
$zapytanie->bind_param('s', $login);Znajdź dwa błędy:
$zapytanie = $polaczenie->prepare('SELECT id, tytul FROM zadania WHERE uzytkownik_id = ? AND xp > ?');
$zapytanie->bind_param('i', $uzytkownikId, $minimumXp);
$zapytanie->execute();
while ($wiersz = $zapytanie->fetch_assoc()) { echo $wiersz['tytul']; }Co wypisze ten kod, jeżeli zapytanie zwróci 3 wiersze?
$wynik = $polaczenie->query('SELECT login FROM uzytkownicy');
$ile = 0;
while ($wiersz = $wynik->fetch_assoc()) { $ile++; }
echo $ile + $wynik->num_rows;Użytkownik wpisuje w wyszukiwarkę
' OR '1'='1. Wersja A używaquery()ze sklejonym tekstem, wersja B prepared statement. Ile wierszy zwróci każda i dlaczego?Uzupełnij tak, aby znaleźć zadania użytkownika o
id= 3, których tytuł zawiera słowo wpisane w$szukane:$zapytanie = $polaczenie->prepare('SELECT tytul FROM zadania WHERE uzytkownik_id = ? AND tytul LIKE ?');
$fraza = ______________;
$zapytanie->bind_param(______, $id, $fraza);Kolega chce pozwolić użytkownikowi wybrać kolumnę sortowania i pisze:
$zapytanie = $polaczenie->prepare('SELECT login, xp FROM uzytkownicy ORDER BY ?');Czy to jest bezpieczne? Czy to zadziała? Jak zrobić to poprawnie?
$zapytanie->bind_param('s', $_GET['sort']);Ten sam wiersz odczytany raz przez
query(), raz przez prepared statement. Co pokażevar_dump($wiersz['xp'])w każdym przypadku i skąd ta różnica?wczytajUzytkownika($polaczenie, 7)zwracanull, bo użytkownika o takimidnie ma. Czy skrypt powinien rzucić wyjątek? Uzasadnij.Który fragment jest niebezpieczny i jaki to typ podatności?
// A
$stmt = $polaczenie->prepare('SELECT imie FROM uzytkownicy WHERE id = ?');
$stmt->bind_param('i', $id);
$stmt->execute();
$wiersz = $stmt->get_result()->fetch_assoc();
echo "Cześć, {$wiersz['imie']}!";
// B
$imie = $_POST['imie'];
echo "Cześć, " . htmlspecialchars($imie) . "!";Zadanie praktyczne (schemat egzaminacyjny). Napisz skrypt, który: łączy się z bazą
wodospady(localhost,root, bez hasła), pobieraidObiekt,panstwo,nazwaz tabeliobiektydlaidKontynentprzesłanego metodą GET (domyślnie6), wypisuje każdy rekord jako wiersz tabeli HTML i na końcu zamyka połączenie. Wypisz sam szkielet PHP, bez stylów.
Odpowiedzi
3 wiersze; 0 wierszy (mimo że niewykonane zadania istnieją —
= NULLnigdy nie jest prawdą); 2 wiersze.Zła kolejność klauzul. Poprawnie:
SELECT login FROM uzytkownicy WHERE xp > 100 ORDER BY xp DESC;'?'w apostrofach to zwykły tekst, a nie parametr. Zapytanie ma zero parametrów, więcbind_param()zgłosi błąd o niezgodnej liczbie parametrów. Poprawnie:WHERE login = ?.(a) Typy: dwa znaki
?, a tylko jedna litera — powinno byćbind_param('ii', $uzytkownikId, $minimumXp). (b)fetch_assoc()wywołane na zapytaniu zamiast na wyniku — brakuje$wynik = $zapytanie->get_result();i pętli po$wynik.6. Pętla odczytała 3 wiersze, więc$ilewynosi 3.num_rowsto liczba wierszy w wyniku i nie zmniejsza się w miarę odczytywania — nadal 3. Razem 6.Wersja A zwróci wszystkie wiersze — tekst użytkownika stał się częścią polecenia i warunek
'1'='1'jest zawsze prawdziwy (SQL injection). Wersja B zwróci 0 — baza porówna to jako zwykły tekst, czyli poszuka nazwy dosłownie brzmiącej' OR '1'='1.$fraza = '%' . $szukane . '%';orazbind_param('is', $id, $fraza).Zadziała bez błędu, ale nie posortuje — parametr zostanie wstawiony jako wartość tekstowa, czyli sortowanie odbędzie się po stałym tekście. Przez
?przekazujemy tylko wartości. Poprawnie: whitelist (in_array()+ wartość domyślna) i wstawienie nazwy kolumny do tekstu zapytania.Po
query():string(2) "99". Po prepared statement zget_result():int(99). Prepared statements przesyłają wynik protokołem binarnym imysqlizamienia kolumnyINTna PHP-oweint.Nie. Brak wiersza to normalny wynik zapytania, nie awaria — funkcja zwraca
null, a kod wywołujący decyduje, co z tym zrobić (np. wylogowanie i komunikat). Wyjątek zostaw na sytuacje, w których nie da się wykonać zapytania: brak połączenia, błąd SQL.Fragment A. Zapytanie jest bezpieczne (prepared statement), ale imię pobrane z bazy trafia do HTML bez
htmlspecialchars()— jeżeli ktoś wcześniej zapisał do bazy<script>, dostajemy XSS. Baza nie jest źródłem „zaufanych” danych. Fragment B jest poprawny.Szkielet (obiektowo; wersja proceduralna z arkusza analogicznie):
<?phpPunkty, o których najłatwiej zapomnieć: wartość domyślna dla parametru GET,
$polaczenie = new mysqli('localhost', 'root', '', 'wodospady');
$idKontynent = $_GET['idKontynent'] ?? 6;
$zapytanie = $polaczenie->prepare(
'SELECT idObiekt, panstwo, nazwa FROM obiekty WHERE idKontynent = ?'
);
$zapytanie->bind_param('i', $idKontynent);
$zapytanie->execute();
$wynik = $zapytanie->get_result();
while ($wiersz = $wynik->fetch_assoc()) {
echo '<tr>';
echo '<td>' . $wiersz['idObiekt'] . '</td>';
echo '<td>' . htmlspecialchars($wiersz['panstwo']) . '</td>';
echo '<td>' . htmlspecialchars($wiersz['nazwa']) . '</td>';
echo '</tr>';
}
$polaczenie->close();
?><tr>wewnątrz pętli (a nie przed nią) i zamknięcie połączenia na końcu.