Szybki quiz – przypomnienie poprzedniej lekcji
Lekcja 18: połączenie z bazą, SELECT, prepared statements, SQL injection.
To zapytanie nie zwraca żadnego wiersza, choć w tabeli są zadania bez daty wykonania. Dlaczego i jak je poprawić?
SELECT tytul FROM zadania WHERE data_wykonania = NULL;Znajdź błąd:
$zapytanie = $polaczenie->prepare("SELECT id, login FROM uzytkownicy WHERE login = '?'");
$zapytanie->bind_param('s', $login);Zapytanie ma warunek
WHERE uzytkownik_id = ? AND termin < ? AND xp >= ?(kolumny kolejno: liczba, data, liczba). Jaki będzie pierwszy argumentbind_param()?Co zwraca
fetch_assoc(), gdy wiersze się skończyły? Czym różni się odnum_rows?Użytkownik wpisuje w pole wyszukiwania
' OR '1'='1. Co się stanie, gdy tekst zostanie wklejony do zapytania SQL, a co gdy trafi tam przezbind_param()?wczytajUzytkownika($polaczenie, 99)zwracanull. Czy to awaria bazy? Uzasadnij.
Odpowiedzi
- Porównanie z
NULLprzez=nigdy nie daje prawdy —NULLznaczy „nie wiadomo”, więc warunek nie jest spełniony dla żadnego wiersza, a baza nie zgłasza błędu. Poprawnie:WHERE data_wykonania IS NULL. '?'w apostrofach to zwykły tekst, a nie parametr. Zapytanie nie ma żadnego?, więcbind_param()zgłosi błąd o niezgodnej liczbie parametrów. Poprawnie:WHERE login = ?.'isi'— kolejno: liczba całkowita, data (przekazywana jako tekst), liczba całkowita.fetch_assoc()zwraca kolejny wiersz jako tablicę asocjacyjną, a po ostatnim —null.num_rowsto liczba wierszy w całym wyniku; niczego z niego nie pobiera.- Przy sklejaniu SQL tekst staje się częścią polecenia — warunek
'1'='1'jest zawsze prawdziwy i zapytanie zwraca wszystkich użytkowników (SQL injection). Przybind_param()to tylko dziwny login, którego nikt nie ma: 0 wierszy, bez błędu. - Nie. Zapytanie wykonało się poprawnie, po prostu żaden wiersz nie pasował. Awaria (np. brak bazy) to wyjątek
mysqli_sql_exception, a nienull. - Przez
?przekazujemy wartości, nie nazwy kolumn — nazwa trafiłaby tam jako tekst i nic by nie posortowała. Nazwę kolumny sprawdzamy białą listą dozwolonych nazw (whitelist).
W poprzednim materiale nauczyłeś się pobierać dane z bazy używając zapytań SELECT. Teraz czas na operacje, które zmieniają dane:
- INSERT – dodawanie nowych rekordów (np. rejestracja gracza, dodanie posta)
- UPDATE – aktualizacja istniejących danych (np. zmiana poziomu, edycja profilu)
- DELETE – usuwanie rekordów (np. usunięcie konta, skasowanie komentarza)
Te operacje są fundamentem każdej aplikacji:
- Rejestracja użytkownika → INSERT
- Aktualizacja avatara → UPDATE
- Usunięcie spamu → DELETE
- System komentarzy → INSERT + UPDATE + DELETE
UWAGA: Operacje zmieniające dane są nieodwracalne! Zawsze używaj prepared statements i walidacji danych.
Będziemy pracować na bazie gaming_hub z poprzedniego materiału. Upewnij się, że masz utworzoną tabelę players
UWAGA: Przedstawione poniżej przykłady nie są zabezpieczone przed odświeżaniem strony (F5) lub ponownym kliknięciem przycisku zmieniającego dane. Każde odświeżenie strony będzie powodowało ponowne wykonanie operacji na bazie. Jak to zrobić opiszę na końcu materiału.
INSERT – Dodawanie nowych rekordów
Przykład 1: Rejestracja gracza (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');
$errors = [];
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Pobieramy i czyszczenie dane z formularza
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$password_confirm = $_POST['password_confirm'] ?? '';
// Walidacja danych
if (empty($username)) {
$errors[] = "Nick jest wymagany";
} elseif (strlen($username) < 3) {
$errors[] = "Nick musi mieć minimum 3 znaki";
} elseif (strlen($username) > 50) {
$errors[] = "Nick może mieć maksymalnie 50 znaków";
} elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
$errors[] = "Nick może zawierać tylko litery, cyfry i podkreślnik";
}
if (empty($email)) {
$errors[] = "Email jest wymagany";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Nieprawidłowy format email";
}
if (empty($password)) {
$errors[] = "Hasło jest wymagane";
} elseif (strlen($password) < 8) {
$errors[] = "Hasło musi mieć minimum 8 znaków";
}
if ($password !== $password_confirm) {
$errors[] = "Hasła nie są identyczne";
}
// Jeśli brak błędów walidacji - sprawdzamy czy nick/email już istnieją
if (empty($errors)) {
// BEZPIECZNE - sprawdzamy czy username już istnieje
$stmt = $db->prepare("SELECT id FROM players WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
$errors[] = "Nick '{$username}' jest już zajęty";
}
$stmt->close();
// BEZPIECZNE - sprawdzamy czy email już istnieje
$stmt = $db->prepare("SELECT id FROM players WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
$errors[] = "Email '{$email}' jest już zarejestrowany";
}
$stmt->close();
}
// Jeśli wszystko OK - dodajemy gracza do bazy
if (empty($errors)) {
// Hashowanie hasła (NIGDY nie zapisuj hasła w plain text!)
$password_hash = password_hash($password, PASSWORD_DEFAULT);
// BEZPIECZNE - INSERT z prepared statement
$stmt = $db->prepare("INSERT INTO players (username, email, password, created_at) VALUES (?, ?, ?, NOW())");
$stmt->bind_param("sss", $username, $email, $password_hash);
if ($stmt->execute()) {
// Pobieramy ID dodanego gracza
$new_player_id = $stmt->insert_id;
// Sprawdzamy ile wierszy zostało dodanych
$affected_rows = $stmt->affected_rows;
$success = true;
$success_message = "Gracz '{$username}' został zarejestrowany! ID: {$new_player_id}";
// Możemy od razu zalogować użytkownika
// session_start();
// $_SESSION['user_id'] = $new_player_id;
// $_SESSION['username'] = $username;
} else {
$errors[] = "Wystąpił błąd podczas rejestracji: {$stmt->error}";
}
$stmt->close();
}
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Rejestracja gracza</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
width: 100%;
max-width: 500px;
}
h1 {
margin-bottom: 30px;
color: #333;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #f5c6cb;
}
.error ul {
margin-left: 20px;
}
.success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #c3e6cb;
text-align: center;
}
.info {
background: #d1ecf1;
color: #0c5460;
padding: 10px;
border-radius: 5px;
margin-top: 15px;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>Rejestracja Gracza</h1>
<?php if ($success): ?>
<div class="success">
<?= htmlspecialchars($success_message) ?>
<br><br>
<a href="profil.php?id=<?= $new_player_id ?>">Zobacz profil</a> |
<a href="lista_graczy.php">Lista graczy</a>
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="error">
<strong>Błędy walidacji:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label>Nick gracza:</label>
<input type="text" name="username"
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
placeholder="np. ProGamer123"
required>
</div>
<div class="form-group">
<label>Email:</label>
<input type="email" name="email"
value="<?= htmlspecialchars($_POST['email'] ?? '') ?>"
placeholder="twoj@email.com"
required>
</div>
<div class="form-group">
<label>Hasło:</label>
<input type="password" name="password"
placeholder="Min. 8 znaków"
required>
</div>
<div class="form-group">
<label>Powtórz hasło:</label>
<input type="password" name="password_confirm"
placeholder="Powtórz hasło"
required>
</div>
<button type="submit">Zarejestruj się</button>
<div class="info">
<strong>Wymagania:</strong><br>
• Nick: 3-50 znaków (litery, cyfry, podkreślnik)<br>
• Hasło: minimum 8 znaków<br>
• Poprawny format email
</div>
</form>
</div>
</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
]
);
$errors = [];
$success = false;
// PROFESJONALNE - sprawdzamy metodę HTTP
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Pobieramy i czyszczenie dane z formularza
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$password_confirm = $_POST['password_confirm'] ?? '';
// Walidacja danych
if (empty($username)) {
$errors[] = "Nick jest wymagany";
} elseif (strlen($username) < 3) {
$errors[] = "Nick musi mieć minimum 3 znaki";
} elseif (strlen($username) > 50) {
$errors[] = "Nick może mieć maksymalnie 50 znaków";
} elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
$errors[] = "Nick może zawierać tylko litery, cyfry i podkreślnik";
}
if (empty($email)) {
$errors[] = "Email jest wymagany";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Nieprawidłowy format email";
}
if (empty($password)) {
$errors[] = "Hasło jest wymagane";
} elseif (strlen($password) < 8) {
$errors[] = "Hasło musi mieć minimum 8 znaków";
}
if ($password !== $password_confirm) {
$errors[] = "Hasła nie są identyczne";
}
// Jeśli brak błędów walidacji - sprawdzamy czy nick/email już istnieją
if (empty($errors)) {
// BEZPIECZNE - sprawdzamy czy username już istnieje
$stmt = $pdo->prepare("SELECT id FROM players WHERE username = :username");
$stmt->execute(['username' => $username]);
if ($stmt->fetch()) {
$errors[] = "Nick '{$username}' jest już zajęty";
}
// BEZPIECZNE - sprawdzamy czy email już istnieje
$stmt = $pdo->prepare("SELECT id FROM players WHERE email = :email");
$stmt->execute(['email' => $email]);
if ($stmt->fetch()) {
$errors[] = "Email '{$email}' jest już zarejestrowany";
}
}
// Jeśli wszystko OK - dodajemy gracza do bazy
if (empty($errors)) {
// Hashowanie hasła (NIGDY nie zapisuj hasła w plain text!)
$password_hash = password_hash($password, PASSWORD_DEFAULT);
// BEZPIECZNE - INSERT z prepared statement
$stmt = $pdo->prepare("INSERT INTO players (username, email, password, created_at) VALUES (:username, :email, :password, NOW())");
if ($stmt->execute([
'username' => $username,
'email' => $email,
'password' => $password_hash
])) {
// Pobieramy ID dodanego gracza
$new_player_id = $pdo->lastInsertId();
// Sprawdzamy ile wierszy zostało dodanych
$affected_rows = $stmt->rowCount();
$success = true;
$success_message = "Gracz '{$username}' został zarejestrowany! ID: {$new_player_id}";
// Możemy od razu zalogować użytkownika
// session_start();
// $_SESSION['user_id'] = $new_player_id;
// $_SESSION['username'] = $username;
}
}
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
$errors[] = "Wystąpił błąd podczas rejestracji. Spróbuj ponownie.";
}
$pdo = null;
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Rejestracja gracza</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
width: 100%;
max-width: 500px;
}
h1 {
margin-bottom: 30px;
color: #333;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #f5c6cb;
}
.error ul {
margin-left: 20px;
}
.success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #c3e6cb;
text-align: center;
}
.info {
background: #d1ecf1;
color: #0c5460;
padding: 10px;
border-radius: 5px;
margin-top: 15px;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>Rejestracja Gracza</h1>
<?php if ($success): ?>
<div class="success">
<?= htmlspecialchars($success_message) ?>
<br><br>
<a href="profil.php?id=<?= $new_player_id ?>">Zobacz profil</a> |
<a href="lista_graczy.php">Lista graczy</a>
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="error">
<strong>Błędy walidacji:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label>Nick gracza:</label>
<input type="text" name="username"
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
placeholder="np. ProGamer123"
required>
</div>
<div class="form-group">
<label>Email:</label>
<input type="email" name="email"
value="<?= htmlspecialchars($_POST['email'] ?? '') ?>"
placeholder="twoj@email.com"
required>
</div>
<div class="form-group">
<label>Hasło:</label>
<input type="password" name="password"
placeholder="Min. 8 znaków"
required>
</div>
<div class="form-group">
<label>Powtórz hasło:</label>
<input type="password" name="password_confirm"
placeholder="Powtórz hasło"
required>
</div>
<button type="submit">Zarejestruj się</button>
<div class="info">
<strong>Wymagania:</strong><br>
• Nick: 3-50 znaków (litery, cyfry, podkreślnik)<br>
• Hasło: minimum 8 znaków<br>
• Poprawny format email
</div>
</form>
</div>
</body>
</html>
Sprawdzanie wyniku operacji INSERT
Metody sprawdzania dla MySQLi:
// 1. Pobierz ID ostatnio dodanego rekordu
$new_id = $stmt->insert_id; // Obiektowo
$new_id = mysqli_insert_id($db); // Proceduralnie
// 2. Sprawdź ile wierszy zostało dodanych
$affected_rows = $stmt->affected_rows; // Obiektowo
$affected_rows = mysqli_affected_rows($db); // Proceduralnie
// 3. Sprawdź czy zapytanie się wykonało
if ($stmt->execute()) {
echo "Sukces!";
} else {
echo "Błąd: {$stmt->error}";
}
Metody sprawdzania dla PDO:
// 1. Pobierz ID ostatnio dodanego rekordu
$new_id = $pdo->lastInsertId();
// 2. Sprawdź ile wierszy zostało dodanych
$affected_rows = $stmt->rowCount();
// 3. Sprawdź czy zapytanie się wykonało
if ($stmt->execute()) {
echo "Sukces!";
} else {
echo "Błąd!";
}
// 4. Obsługa wyjątków (try-catch)
try {
$stmt->execute();
echo "Sukces!";
} catch (PDOException $e) {
echo "Błąd: {$e->getMessage()}";
}
Przykład 2: Dodawanie wielu graczy naraz (batch INSERT)
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');
// Tablica graczy do dodania
$gracze = [
['username' => 'TestPlayer1', 'email' => 'test1@example.com', 'level' => 5],
['username' => 'TestPlayer2', 'email' => 'test2@example.com', 'level' => 10],
['username' => 'TestPlayer3', 'email' => 'test3@example.com', 'level' => 15]
];
// BEZPIECZNE - przygotowanie zapytania raz
$stmt = $db->prepare("INSERT INTO players (username, email, level, created_at) VALUES (?, ?, ?, NOW())");
$dodano = 0;
$bledy = [];
foreach ($gracze as $gracz) {
// Bindowanie parametrów
$stmt->bind_param("ssi", $gracz['username'], $gracz['email'], $gracz['level']);
if ($stmt->execute()) {
$dodano++;
// Możesz pobrać ID każdego dodanego gracza
$last_id = $stmt->insert_id;
echo "Dodano: {$gracz['username']} (ID: {$last_id})<br>";
} else {
$bledy[] = "Błąd dla {$gracz['username']}: {$stmt->error}";
}
}
echo "<br><strong>Podsumowanie:</strong><br>";
echo "Dodano: {$dodano} graczy<br>";
echo "Błędów: " . count($bledy) . "<br>";
if (!empty($bledy)) {
echo "<br><strong>Szczegóły błędów:</strong><br>";
foreach ($bledy as $blad) {
echo "{$blad}<br>";
}
}
$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
]
);
// Tablica graczy do dodania
$gracze = [
['username' => 'TestPlayer1', 'email' => 'test1@example.com', 'level' => 5],
['username' => 'TestPlayer2', 'email' => 'test2@example.com', 'level' => 10],
['username' => 'TestPlayer3', 'email' => 'test3@example.com', 'level' => 15]
];
// BEZPIECZNE - przygotowanie zapytania raz
$stmt = $pdo->prepare("INSERT INTO players (username, email, level, created_at) VALUES (:username, :email, :level, NOW())");
$dodano = 0;
$bledy = [];
foreach ($gracze as $gracz) {
try {
$stmt->execute($gracz);
$dodano++;
// Możesz pobrać ID każdego dodanego gracza
$last_id = $pdo->lastInsertId();
echo "Dodano: {$gracz['username']} (ID: {$last_id})<br>";
} catch (PDOException $e) {
$bledy[] = "Błąd dla {$gracz['username']}: {$e->getMessage()}";
}
}
echo "<br><strong>Podsumowanie:</strong><br>";
echo "Dodano: {$dodano} graczy<br>";
echo "Błędów: " . count($bledy) . "<br>";
if (!empty($bledy)) {
echo "<br><strong>Szczegóły błędów:</strong><br>";
foreach ($bledy as $blad) {
echo "{$blad}<br>";
}
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
$pdo = null;
?>
UPDATE – Aktualizacja istniejących rekordów
Przykład 3: Edycja profilu gracza (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');
$errors = [];
$success = false;
$gracz = null;
// Pobieramy ID gracza z URL
$player_id = (int)($_GET['id'] ?? 0);
if ($player_id <= 0) {
die('Nieprawidłowe ID gracza');
}
// Pobieramy dane gracza do wyświetlenia w formularzu
$stmt = $db->prepare("SELECT * FROM players WHERE id = ?");
$stmt->bind_param("i", $player_id);
$stmt->execute();
$wynik = $stmt->get_result();
$gracz = $wynik->fetch_assoc();
$stmt->close();
if (!$gracz) {
die('Nie znaleziono gracza o ID: ' . $player_id);
}
// PROFESJONALNE - sprawdzamy metodę HTTP
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Pobieramy dane z formularza
$email = trim($_POST['email'] ?? '');
$level = (int)($_POST['level'] ?? 1);
$coins = (int)($_POST['coins'] ?? 0);
$rank_name = $_POST['rank_name'] ?? 'Bronze';
// Walidacja danych
if (empty($email)) {
$errors[] = "Email jest wymagany";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Nieprawidłowy format email";
}
if ($level < 1 || $level > 100) {
$errors[] = "Poziom musi być między 1 a 100";
}
if ($coins < 0) {
$errors[] = "Coins nie może być ujemny";
}
$allowed_ranks = ['Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Master', 'Grandmaster'];
if (!in_array($rank_name, $allowed_ranks)) {
$errors[] = "Nieprawidłowa ranga";
}
// Sprawdzamy czy email nie jest już używany przez innego gracza
if (empty($errors)) {
$stmt = $db->prepare("SELECT id FROM players WHERE email = ? AND id != ?");
$stmt->bind_param("si", $email, $player_id);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
$errors[] = "Email '{$email}' jest już używany przez innego gracza";
}
$stmt->close();
}
// Jeśli brak błędów - aktualizujemy dane
if (empty($errors)) {
// BEZPIECZNE - UPDATE z prepared statement
$stmt = $db->prepare("UPDATE players SET email = ?, level = ?, coins = ?, rank_name = ?, updated_at = NOW() WHERE id = ?");
$stmt->bind_param("siisi", $email, $level, $coins, $rank_name, $player_id);
if ($stmt->execute()) {
// Sprawdzamy ile wierszy zostało zaktualizowanych
$affected_rows = $stmt->affected_rows;
if ($affected_rows > 0) {
$success = true;
$success_message = "Profil gracza '{$gracz['username']}' został zaktualizowany! (Zmieniono {$affected_rows} rekord)";
// Odświeżamy dane gracza
$stmt_refresh = $db->prepare("SELECT * FROM players WHERE id = ?");
$stmt_refresh->bind_param("i", $player_id);
$stmt_refresh->execute();
$wynik_refresh = $stmt_refresh->get_result();
$gracz = $wynik_refresh->fetch_assoc();
$stmt_refresh->close();
} else {
$success_message = "Nie wprowadzono żadnych zmian (dane są identyczne)";
}
} else {
$errors[] = "Wystąpił błąd podczas aktualizacji: {$stmt->error}";
}
$stmt->close();
}
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Edycja profilu - <?= htmlspecialchars($gracz['username']) ?></title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
}
h1 {
margin-bottom: 10px;
color: #333;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
input, select {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #f5c6cb;
}
.error ul {
margin-left: 20px;
}
.success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #c3e6cb;
}
.info-box {
background: #e7f3ff;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #2196F3;
}
.back-link {
display: inline-block;
margin-top: 20px;
color: #667eea;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>Edycja profilu gracza</h1>
<div class="subtitle">ID: <?= $gracz['id'] ?> | Nick: <strong><?= htmlspecialchars($gracz['username']) ?></strong></div>
<?php if ($success): ?>
<div class="success">
<?= htmlspecialchars($success_message) ?>
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="error">
<strong>Błędy walidacji:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="info-box">
<strong>Informacje:</strong><br>
• Nick gracza nie może być edytowany (to unikalna nazwa)<br>
• Hasło można zmienić w osobnej sekcji zabezpieczeń<br>
• Data rejestracji: <?= date('d.m.Y H:i', strtotime($gracz['created_at'])) ?>
</div>
<form method="POST">
<div class="form-group">
<label>Email:</label>
<input type="email" name="email"
value="<?= htmlspecialchars($gracz['email']) ?>"
required>
</div>
<div class="form-row">
<div class="form-group">
<label>Poziom (1-100):</label>
<input type="number" name="level"
min="1" max="100"
value="<?= $gracz['level'] ?>"
required>
</div>
<div class="form-group">
<label>Coins:</label>
<input type="number" name="coins"
min="0"
value="<?= $gracz['coins'] ?>"
required>
</div>
</div>
<div class="form-group">
<label>Ranga:</label>
<select name="rank_name" required>
<option value="Bronze" <?= $gracz['rank_name'] == 'Bronze' ? 'selected' : '' ?>>Bronze</option>
<option value="Silver" <?= $gracz['rank_name'] == 'Silver' ? 'selected' : '' ?>>Silver</option>
<option value="Gold" <?= $gracz['rank_name'] == 'Gold' ? 'selected' : '' ?>>Gold</option>
<option value="Platinum" <?= $gracz['rank_name'] == 'Platinum' ? 'selected' : '' ?>>Platinum</option>
<option value="Diamond" <?= $gracz['rank_name'] == 'Diamond' ? 'selected' : '' ?>>Diamond</option>
<option value="Master" <?= $gracz['rank_name'] == 'Master' ? 'selected' : '' ?>>Master</option>
<option value="Grandmaster" <?= $gracz['rank_name'] == 'Grandmaster' ? 'selected' : '' ?>>Grandmaster</option>
</select>
</div>
<button type="submit">Zapisz zmiany</button>
</form>
<a href="profil.php?id=<?= $player_id ?>" class="back-link">← Powrót do profilu</a>
</div>
</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
]
);
$errors = [];
$success = false;
$gracz = null;
// Pobieramy ID gracza z URL
$player_id = (int)($_GET['id'] ?? 0);
if ($player_id <= 0) {
die('Nieprawidłowe ID gracza');
}
// Pobieramy dane gracza do wyświetlenia w formularzu
$stmt = $pdo->prepare("SELECT * FROM players WHERE id = :id");
$stmt->execute(['id' => $player_id]);
$gracz = $stmt->fetch();
if (!$gracz) {
die('Nie znaleziono gracza o ID: ' . $player_id);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Pobieramy dane z formularza
$email = trim($_POST['email'] ?? '');
$level = (int)($_POST['level'] ?? 1);
$coins = (int)($_POST['coins'] ?? 0);
$rank_name = $_POST['rank_name'] ?? 'Bronze';
// Walidacja danych
if (empty($email)) {
$errors[] = "Email jest wymagany";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Nieprawidłowy format email";
}
if ($level < 1 || $level > 100) {
$errors[] = "Poziom musi być między 1 a 100";
}
if ($coins < 0) {
$errors[] = "Coins nie może być ujemny";
}
$allowed_ranks = ['Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Master', 'Grandmaster'];
if (!in_array($rank_name, $allowed_ranks)) {
$errors[] = "Nieprawidłowa ranga";
}
// Sprawdzamy czy email nie jest już używany przez innego gracza
if (empty($errors)) {
$stmt = $pdo->prepare("SELECT id FROM players WHERE email = :email AND id != :id");
$stmt->execute(['email' => $email, 'id' => $player_id]);
if ($stmt->fetch()) {
$errors[] = "Email '{$email}' jest już używany przez innego gracza";
}
}
// Jeśli brak błędów - aktualizujemy dane
if (empty($errors)) {
// BEZPIECZNE - UPDATE z prepared statement
$stmt = $pdo->prepare("UPDATE players SET email = :email, level = :level, coins = :coins, rank_name = :rank_name, updated_at = NOW() WHERE id = :id");
if ($stmt->execute([
'email' => $email,
'level' => $level,
'coins' => $coins,
'rank_name' => $rank_name,
'id' => $player_id
])) {
// Sprawdzamy ile wierszy zostało zaktualizowanych
$affected_rows = $stmt->rowCount();
if ($affected_rows > 0) {
$success = true;
$success_message = "Profil gracza '{$gracz['username']}' został zaktualizowany! (Zmieniono {$affected_rows} rekord)";
// Odświeżamy dane gracza
$stmt_refresh = $pdo->prepare("SELECT * FROM players WHERE id = :id");
$stmt_refresh->execute(['id' => $player_id]);
$gracz = $stmt_refresh->fetch();
} else {
$success_message = "Nie wprowadzono żadnych zmian (dane są identyczne)";
}
}
}
}
} 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>Edycja profilu - <?= htmlspecialchars($gracz['username']) ?></title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
}
h1 {
margin-bottom: 10px;
color: #333;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
input, select {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #f5c6cb;
}
.error ul {
margin-left: 20px;
}
.success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #c3e6cb;
}
.info-box {
background: #e7f3ff;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #2196F3;
}
.back-link {
display: inline-block;
margin-top: 20px;
color: #667eea;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>Edycja profilu gracza</h1>
<div class="subtitle">ID: <?= $gracz['id'] ?> | Nick: <strong><?= htmlspecialchars($gracz['username']) ?></strong></div>
<?php if ($success): ?>
<div class="success">
<?= htmlspecialchars($success_message) ?>
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="error">
<strong>Błędy walidacji:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="info-box">
<strong>Informacje:</strong><br>
• Nick gracza nie może być edytowany (to unikalna nazwa)<br>
• Hasło można zmienić w osobnej sekcji zabezpieczeń<br>
• Data rejestracji: <?= date('d.m.Y H:i', strtotime($gracz['created_at'])) ?>
</div>
<form method="POST">
<div class="form-group">
<label>Email:</label>
<input type="email" name="email"
value="<?= htmlspecialchars($gracz['email']) ?>"
required>
</div>
<div class="form-row">
<div class="form-group">
<label>Poziom (1-100):</label>
<input type="number" name="level"
min="1" max="100"
value="<?= $gracz['level'] ?>"
required>
</div>
<div class="form-group">
<label>Coins:</label>
<input type="number" name="coins"
min="0"
value="<?= $gracz['coins'] ?>"
required>
</div>
</div>
<div class="form-group">
<label>Ranga:</label>
<select name="rank_name" required>
<option value="Bronze" <?= $gracz['rank_name'] == 'Bronze' ? 'selected' : '' ?>>Bronze</option>
<option value="Silver" <?= $gracz['rank_name'] == 'Silver' ? 'selected' : '' ?>>Silver</option>
<option value="Gold" <?= $gracz['rank_name'] == 'Gold' ? 'selected' : '' ?>>Gold</option>
<option value="Platinum" <?= $gracz['rank_name'] == 'Platinum' ? 'selected' : '' ?>>Platinum</option>
<option value="Diamond" <?= $gracz['rank_name'] == 'Diamond' ? 'selected' : '' ?>>Diamond</option>
<option value="Master" <?= $gracz['rank_name'] == 'Master' ? 'selected' : '' ?>>Master</option>
<option value="Grandmaster" <?= $gracz['rank_name'] == 'Grandmaster' ? 'selected' : '' ?>>Grandmaster</option>
</select>
</div>
<button type="submit">Zapisz zmiany</button>
</form>
<a href="profil.php?id=<?= $player_id ?>" class="back-link">← Powrót do profilu</a>
</div>
</body>
</html>
Sprawdzanie wyniku operacji UPDATE
Metody sprawdzania dla MySQLi:
// 1. Sprawdź ile wierszy zostało zaktualizowanych
$affected_rows = $stmt->affected_rows; // Obiektowo
// 2. Sprawdź czy zapytanie się wykonało
if ($stmt->execute()) {
if ($affected_rows > 0) {
echo "Zaktualizowano {$affected_rows} rekord(ów)";
} else {
echo "Nie wprowadzono zmian (dane są identyczne)";
}
} else {
echo "Błąd: {$stmt->error}";
}
// 3. Sprawdź czy rekord istnieje przed UPDATE
$stmt_check = $db->prepare("SELECT id FROM players WHERE id = ?");
$stmt_check->bind_param("i", $player_id);
$stmt_check->execute();
$stmt_check->store_result();
if ($stmt_check->num_rows == 0) {
echo "Rekord o tym ID nie istnieje";
}
Metody sprawdzania dla PDO:
// 1. Sprawdź ile wierszy zostało zaktualizowanych
$affected_rows = $stmt->rowCount();
// 2. Sprawdź czy zapytanie się wykonało
if ($stmt->execute()) {
if ($affected_rows > 0) {
echo "Zaktualizowano {$affected_rows} rekord(ów)";
} else {
echo "Nie wprowadzono zmian (dane są identyczne)";
}
}
// 3. Sprawdź czy rekord istnieje przed UPDATE
$stmt_check = $pdo->prepare("SELECT id FROM players WHERE id = :id");
$stmt_check->execute(['id' => $player_id]);
if (!$stmt_check->fetch()) {
echo "Rekord o tym ID nie istnieje";
}
Przykład 4: Aktualizacja wielu graczy naraz (batch UPDATE)
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');
// Przykład: Dodaj 100 coins wszystkim graczom Bronze
$rank = 'Bronze';
$bonus_coins = 100;
$stmt = $db->prepare("UPDATE players SET coins = coins + ? WHERE rank_name = ?");
$stmt->bind_param("is", $bonus_coins, $rank);
if ($stmt->execute()) {
$affected_rows = $stmt->affected_rows;
if ($affected_rows > 0) {
echo "Sukces! Dodano {$bonus_coins} coins dla {$affected_rows} graczy rangi {$rank}";
} else {
echo "Nie znaleziono graczy rangi {$rank} lub już mieli te coins";
}
} else {
echo "Błąd: {$stmt->error}";
}
$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
]
);
// Przykład: Dodaj 100 coins wszystkim graczom Bronze
$rank = 'Bronze';
$bonus_coins = 100;
$stmt = $pdo->prepare("UPDATE players SET coins = coins + :bonus WHERE rank_name = :rank");
if ($stmt->execute(['bonus' => $bonus_coins, 'rank' => $rank])) {
$affected_rows = $stmt->rowCount();
if ($affected_rows > 0) {
echo "Sukces! Dodano {$bonus_coins} coins dla {$affected_rows} graczy rangi {$rank}";
} else {
echo "Nie znaleziono graczy rangi {$rank} lub już mieli te coins";
}
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
$pdo = null;
?>
DELETE – Usuwanie rekordów
Przykład 5: Usuwanie gracza z potwierdzeniem (GET + 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');
$gracz = null;
$deleted = false;
// Pobieramy ID gracza z URL
$player_id = (int)($_GET['id'] ?? 0);
if ($player_id <= 0) {
die('Nieprawidłowe ID gracza');
}
// Pobieramy dane gracza do wyświetlenia
$stmt = $db->prepare("SELECT * FROM players WHERE id = ?");
$stmt->bind_param("i", $player_id);
$stmt->execute();
$wynik = $stmt->get_result();
$gracz = $wynik->fetch_assoc();
$stmt->close();
if (!$gracz) {
die('Nie znaleziono gracza o ID: ' . $player_id);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_delete'])) {
// Dodatkowe zabezpieczenie - sprawdzamy token CSRF (opcjonalnie)
// if ($_POST['token'] !== $_SESSION['token']) die('Invalid token');
$stmt = $db->prepare("DELETE FROM players WHERE id = ?");
$stmt->bind_param("i", $player_id);
if ($stmt->execute()) {
// Sprawdzamy ile wierszy zostało usuniętych
$affected_rows = $stmt->affected_rows;
if ($affected_rows > 0) {
$deleted = true;
$username = htmlspecialchars($gracz['username']);
$success_message = "Gracz '{$username}' (ID: {$player_id}) został permanentnie usunięty z bazy danych.";
} else {
$error_message = "Rekord nie został usunięty (możliwe że już nie istnieje)";
}
} else {
$error_message = "Błąd podczas usuwania: {$stmt->error}";
}
$stmt->close();
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Usuń gracza - <?= htmlspecialchars($gracz['username'] ?? 'Usunięty') ?></title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
max-width: 600px;
width: 100%;
}
h1 {
color: #d32f2f;
margin-bottom: 20px;
text-align: center;
}
.warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin-bottom: 20px;
border-radius: 5px;
}
.player-info {
background: #f8f9fa;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
.player-info h3 {
margin-bottom: 15px;
color: #333;
}
.player-info p {
margin: 8px 0;
color: #666;
}
.buttons {
display: flex;
gap: 10px;
margin-top: 30px;
}
button, .button {
flex: 1;
padding: 15px;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
text-align: center;
text-decoration: none;
display: inline-block;
}
.btn-delete {
background: #d32f2f;
color: white;
}
.btn-delete:hover {
background: #b71c1c;
}
.btn-cancel {
background: #6c757d;
color: white;
}
.btn-cancel:hover {
background: #5a6268;
}
.success {
background: #d4edda;
color: #155724;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #c3e6cb;
text-align: center;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #f5c6cb;
}
.checkbox-confirm {
margin: 20px 0;
display: flex;
align-items: center;
gap: 10px;
}
.checkbox-confirm input {
width: 20px;
height: 20px;
cursor: pointer;
}
.checkbox-confirm label {
cursor: pointer;
user-select: none;
}
</style>
</head>
<body>
<div class="container">
<?php if ($deleted): ?>
<div class="success">
<h2>Gracz został usunięty</h2>
<p><?= $success_message ?></p>
<br>
<a href="lista_graczy.php" class="button btn-cancel">← Powrót do listy graczy</a>
</div>
<?php else: ?>
<h1>Usuń gracza</h1>
<?php if (isset($error_message)): ?>
<div class="error">
<?= htmlspecialchars($error_message) ?>
</div>
<?php endif; ?>
<div class="warning">
<strong>OSTRZEŻENIE:</strong><br>
Ta operacja jest <strong>nieodwracalna</strong>! Gracz i wszystkie jego dane zostaną <strong>permanentnie usunięte</strong> z bazy danych.
</div>
<div class="player-info">
<h3>Dane gracza do usunięcia:</h3>
<p><strong>ID:</strong> <?= $gracz['id'] ?></p>
<p><strong>Username:</strong> <?= htmlspecialchars($gracz['username']) ?></p>
<p><strong>Email:</strong> <?= htmlspecialchars($gracz['email']) ?></p>
<p><strong>Level:</strong> <?= $gracz['level'] ?></p>
<p><strong>Rank:</strong> <?= $gracz['rank_name'] ?></p>
<p><strong>Coins:</strong> <?= number_format($gracz['coins']) ?></p>
<p><strong>Rozegrane gry:</strong> <?= $gracz['games_played'] ?></p>
<p><strong>Data rejestracji:</strong> <?= date('d.m.Y H:i', strtotime($gracz['created_at'])) ?></p>
</div>
<form method="POST">
<div class="checkbox-confirm">
<input type="checkbox" id="confirm" name="confirm_check" required>
<label for="confirm">Potwierdzam, że chcę <strong>permanentnie usunąć</strong> tego gracza</label>
</div>
<div class="buttons">
<a href="profil.php?id=<?= $player_id ?>" class="button btn-cancel">Anuluj</a>
<button type="submit" name="confirm_delete" class="btn-delete" onclick="return confirm('Czy NA PEWNO chcesz usunąć tego gracza? Ta operacja jest NIEODWRACALNA!')">
Usuń gracza
</button>
</div>
</form>
<?php endif; ?>
</div>
</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
]
);
$gracz = null;
$deleted = false;
// Pobieramy ID gracza z URL
$player_id = (int)($_GET['id'] ?? 0);
if ($player_id <= 0) {
die('Nieprawidłowe ID gracza');
}
// Pobieramy dane gracza do wyświetlenia
$stmt = $pdo->prepare("SELECT * FROM players WHERE id = :id");
$stmt->execute(['id' => $player_id]);
$gracz = $stmt->fetch();
if (!$gracz) {
die('Nie znaleziono gracza o ID: ' . $player_id);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_delete'])) {
// Dodatkowe zabezpieczenie - sprawdzamy token CSRF (opcjonalnie)
// if ($_POST['token'] !== $_SESSION['token']) die('Invalid token');
// BEZPIECZNE - DELETE z prepared statement
$stmt = $pdo->prepare("DELETE FROM players WHERE id = :id");
if ($stmt->execute(['id' => $player_id])) {
// Sprawdzamy ile wierszy zostało usuniętych
$affected_rows = $stmt->rowCount();
if ($affected_rows > 0) {
$deleted = true;
$username = htmlspecialchars($gracz['username']);
$success_message = "Gracz '{$username}' (ID: {$player_id}) został permanentnie usunięty z bazy danych.";
} else {
$error_message = "Rekord nie został usunięty (możliwe że już nie istnieje)";
}
}
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
$error_message = "Wystąpił błąd podczas usuwania gracza";
}
$pdo = null;
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Usuń gracza - <?= htmlspecialchars($gracz['username'] ?? 'Usunięty') ?></title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
max-width: 600px;
width: 100%;
}
h1 {
color: #d32f2f;
margin-bottom: 20px;
text-align: center;
}
.warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin-bottom: 20px;
border-radius: 5px;
}
.player-info {
background: #f8f9fa;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
.player-info h3 {
margin-bottom: 15px;
color: #333;
}
.player-info p {
margin: 8px 0;
color: #666;
}
.buttons {
display: flex;
gap: 10px;
margin-top: 30px;
}
button, .button {
flex: 1;
padding: 15px;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
text-align: center;
text-decoration: none;
display: inline-block;
}
.btn-delete {
background: #d32f2f;
color: white;
}
.btn-delete:hover {
background: #b71c1c;
}
.btn-cancel {
background: #6c757d;
color: white;
}
.btn-cancel:hover {
background: #5a6268;
}
.success {
background: #d4edda;
color: #155724;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #c3e6cb;
text-align: center;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #f5c6cb;
}
.checkbox-confirm {
margin: 20px 0;
display: flex;
align-items: center;
gap: 10px;
}
.checkbox-confirm input {
width: 20px;
height: 20px;
cursor: pointer;
}
.checkbox-confirm label {
cursor: pointer;
user-select: none;
}
</style>
</head>
<body>
<div class="container">
<?php if ($deleted): ?>
<div class="success">
<h2>Gracz został usunięty</h2>
<p><?= $success_message ?></p>
<br>
<a href="lista_graczy.php" class="button btn-cancel">← Powrót do listy graczy</a>
</div>
<?php else: ?>
<h1>Usuń gracza</h1>
<?php if (isset($error_message)): ?>
<div class="error">
<?= htmlspecialchars($error_message) ?>
</div>
<?php endif; ?>
<div class="warning">
<strong>OSTRZEŻENIE:</strong><br>
Ta operacja jest <strong>nieodwracalna</strong>! Gracz i wszystkie jego dane zostaną <strong>permanentnie usunięte</strong> z bazy danych.
</div>
<div class="player-info">
<h3>Dane gracza do usunięcia:</h3>
<p><strong>ID:</strong> <?= $gracz['id'] ?></p>
<p><strong>Username:</strong> <?= htmlspecialchars($gracz['username']) ?></p>
<p><strong>Email:</strong> <?= htmlspecialchars($gracz['email']) ?></p>
<p><strong>Level:</strong> <?= $gracz['level'] ?></p>
<p><strong>Rank:</strong> <?= $gracz['rank_name'] ?></p>
<p><strong>Coins:</strong> <?= number_format($gracz['coins']) ?></p>
<p><strong>Rozegrane gry:</strong> <?= $gracz['games_played'] ?></p>
<p><strong>Data rejestracji:</strong> <?= date('d.m.Y H:i', strtotime($gracz['created_at'])) ?></p>
</div>
<form method="POST">
<div class="checkbox-confirm">
<input type="checkbox" id="confirm" name="confirm_check" required>
<label for="confirm">Potwierdzam, że chcę <strong>permanentnie usunąć</strong> tego gracza</label>
</div>
<div class="buttons">
<a href="profil.php?id=<?= $player_id ?>" class="button btn-cancel">Anuluj</a>
<button type="submit" name="confirm_delete" class="btn-delete" onclick="return confirm('Czy NA PEWNO chcesz usunąć tego gracza? Ta operacja jest NIEODWRACALNA!')">
Usuń gracza
</button>
</div>
</form>
<?php endif; ?>
</div>
</body>
</html>
Sprawdzanie wyniku operacji DELETE
Metody sprawdzania dla MySQLi:
// 1. Sprawdź ile wierszy zostało usuniętych
$affected_rows = $stmt->affected_rows;
// 2. Sprawdź czy zapytanie się wykonało
if ($stmt->execute()) {
if ($affected_rows > 0) {
echo "Usunięto {$affected_rows} rekord(ów)";
} else {
echo "Nie usunięto żadnego rekordu (możliwe że nie istniał)";
}
} else {
echo "Błąd: {$stmt->error}";
}
Metody sprawdzania dla PDO:
// 1. Sprawdź ile wierszy zostało usuniętych
$affected_rows = $stmt->rowCount();
// 2. Sprawdź czy zapytanie się wykonało
if ($stmt->execute()) {
if ($affected_rows > 0) {
echo "Usunięto {$affected_rows} rekord(ów)";
} else {
echo "Nie usunięto żadnego rekordu (możliwe że nie istniał)";
}
}
PHP – Zabezpieczenie przed duplikatami po odświeżeniu strony (F5)
Problem: Ponowne wysłanie formularza
Co się dzieje gdy użytkownik naciśnie F5?
1. Użytkownik wypełnia formularz rejestracji
2. Klika "Wyślij" (POST)
3. Dane zostają zapisane w bazie
4. Strona wyświetla "Sukces!"
5. Użytkownik naciska F5 (odświeżenie)
6. Przeglądarka pyta: "Czy ponownie wysłać formularz?"
7. Jeśli użytkownik kliknie "Tak" → próba dodania tego samego gracza ponownie!
Konsekwencje:
- Duplikaty w bazie danych
- Błędy (jeśli są ograniczenia UNIQUE)
- Złe doświadczenie użytkownika
- Problemy z logowaniem transakcji
Rozwiązanie: Post/Redirect/Get (PRG Pattern)
Po wykonaniu operacji należy dodać kod
// PRZEKIEROWANIE - zapobiega duplikatom
header("Location: nazwa_strony.php?<dalej możemy przekazać dane w GET>;
exit; // ZAWSZE exit po header!
Przykład 1: Podstawowe przekierowanie
<?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
// Walidacja...
$stmt = $db->prepare("INSERT INTO players (username, email, created_at) VALUES (?, ?, NOW())");
$stmt->bind_param("ss", $username, $email);
if ($stmt->execute()) {
$new_player_id = $stmt->insert_id;
$stmt->close();
$db->close();
// PRZEKIEROWANIE - zapobiega duplikatom
header("Location: profil.php?id={$new_player_id}");
exit; // ZAWSZE exit po header!
}
$stmt->close();
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Rejestracja</title>
</head>
<body>
<form method="POST">
<input type="text" name="username" required>
<input type="email" name="email" required>
<button type="submit">Zarejestruj</button>
</form>
</body>
</html>
Przykład 2: Przekazywanie informacji przez URL (GET)
<?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$level = (int)($_POST['level'] ?? 1);
// Walidacja...
$stmt = $db->prepare("INSERT INTO players (username, email, level, created_at) VALUES (?, ?, ?, NOW())");
$stmt->bind_param("ssi", $username, $email, $level);
if ($stmt->execute()) {
$new_player_id = $stmt->insert_id;
$affected_rows = $stmt->affected_rows;
$stmt->close();
$db->close();
// Przekazanie wielu informacji przez URL
header("Location: sukces.php?id={$new_player_id}&username=" . urlencode($username) . "&affected={$affected_rows}&action=register");
exit;
}
$stmt->close();
}
$db->close();
?>
Plik: sukces.php
<?php
// Odbieramy dane z URL
$new_player_id = (int)($_GET['id'] ?? 0);
$username = htmlspecialchars($_GET['username'] ?? 'Nieznany');
$affected_rows = (int)($_GET['affected'] ?? 0);
$action = htmlspecialchars($_GET['action'] ?? '');
if ($new_player_id <= 0) {
die('Nieprawidłowe ID');
}
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Rejestracja zakończona</title>
<style>
.success-box {
background: #d4edda;
color: #155724;
padding: 20px;
border-radius: 5px;
border-left: 4px solid #c3e6cb;
margin: 20px;
}
.info {
background: #d1ecf1;
color: #0c5460;
padding: 15px;
border-radius: 5px;
margin: 20px;
}
</style>
</head>
<body>
<div class="success-box">
<h2>Rejestracja zakończona sukcesem!</h2>
<p>Witaj, <strong><?= $username ?></strong>!</p>
</div>
<div class="info">
<strong>Szczegóły operacji:</strong><br>
ID nowego gracza: <strong><?= $new_player_id ?></strong><br>
Liczba dodanych rekordów: <strong><?= $affected_rows ?></strong><br>
Typ operacji: <strong><?= $action ?></strong><br>
<br>
<a href="profil.php?id=<?= $new_player_id ?>">Zobacz profil</a> |
<a href="lista_graczy.php">Lista graczy</a>
</div>
</body>
</html>
Zalety:
- Proste
- Nie wymaga sesji
- Działa zawsze
Wady:
- Dane widoczne w URL
- Ograniczona ilość danych (max ~2000 znaków)
- Nie nadaje się dla wrażliwych informacji
Przykład 3: Przekazywanie informacji przez SESJĘ
Kiedy używać:
- Komunikaty sukcesu/błędu
- Więcej danych
- Dane wrażliwe
- Profesjonalne projekty
<?php
session_start(); // ZAWSZE na początku pliku!
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
// Walidacja danych
if (empty($username)) {
$errors[] = "Nick jest wymagany";
} elseif (strlen($username) < 3) {
$errors[] = "Nick musi mieć minimum 3 znaki";
}
if (empty($email)) {
$errors[] = "Email jest wymagany";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Nieprawidłowy format email";
}
if (empty($password)) {
$errors[] = "Hasło jest wymagane";
} elseif (strlen($password) < 8) {
$errors[] = "Hasło musi mieć minimum 8 znaków";
}
// Sprawdzamy czy username już istnieje
if (empty($errors)) {
$stmt = $db->prepare("SELECT id FROM players WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
$errors[] = "Nick '{$username}' jest już zajęty";
}
$stmt->close();
}
// Jeśli brak błędów - dodajemy gracza
if (empty($errors)) {
$password_hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO players (username, email, password, created_at) VALUES (?, ?, ?, NOW())");
$stmt->bind_param("sss", $username, $email, $password_hash);
if ($stmt->execute()) {
// Pobieramy informacje o operacji
$new_player_id = $stmt->insert_id;
$affected_rows = $stmt->affected_rows;
$stmt->close();
$db->close();
// Zapisujemy dane w sesji
$_SESSION['registration_success'] = [
'player_id' => $new_player_id,
'username' => $username,
'email' => $email,
'affected_rows' => $affected_rows,
'timestamp' => date('Y-m-d H:i:s')
];
// Przekierowanie (URL czysty, bez danych)
header("Location: rejestracja_sukces.php");
exit;
} else {
$errors[] = "Wystąpił błąd podczas rejestracji: {$stmt->error}";
}
$stmt->close();
}
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Rejestracja gracza</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
max-width: 500px;
width: 100%;
}
h1 {
margin-bottom: 30px;
color: #333;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
}
button:hover {
background: #764ba2;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #f5c6cb;
}
.error ul {
margin-left: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Rejestracja Gracza</h1>
<?php if (!empty($errors)): ?>
<div class="error">
<strong>Błędy walidacji:</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label>Nick gracza:</label>
<input type="text" name="username"
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
placeholder="np. ProGamer123"
required>
</div>
<div class="form-group">
<label>Email:</label>
<input type="email" name="email"
value="<?= htmlspecialchars($_POST['email'] ?? '') ?>"
placeholder="twoj@email.com"
required>
</div>
<div class="form-group">
<label>Hasło:</label>
<input type="password" name="password"
placeholder="Min. 8 znaków"
required>
</div>
<button type="submit">Zarejestruj się</button>
</form>
</div>
</body>
</html>
Plik: rejestracja_sukces.php
<?php
session_start(); // ZAWSZE na początku!
// Sprawdź czy istnieją dane o rejestracji
if (!isset($_SESSION['registration_success'])) {
// Jeśli ktoś próbuje wejść bezpośrednio na tę stronę
header("Location: rejestracja.php");
exit;
}
// Pobierz dane z sesji
$data = $_SESSION['registration_success'];
$player_id = (int)$data['player_id'];
$username = htmlspecialchars($data['username']);
$email = htmlspecialchars($data['email']);
$affected_rows = (int)$data['affected_rows'];
$timestamp = htmlspecialchars($data['timestamp']);
// Usuń dane z sesji (wyświetl tylko raz)
unset($_SESSION['registration_success']);
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Rejestracja zakończona</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.success-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
max-width: 600px;
width: 100%;
}
.success-icon {
text-align: center;
font-size: 80px;
color: #28a745;
margin-bottom: 20px;
}
h1 {
text-align: center;
color: #155724;
margin-bottom: 20px;
}
.success-message {
background: #d4edda;
color: #155724;
padding: 20px;
border-radius: 5px;
border-left: 4px solid #c3e6cb;
margin-bottom: 30px;
}
.info-box {
background: #e7f3ff;
padding: 20px;
border-radius: 5px;
border-left: 4px solid #2196F3;
margin-bottom: 20px;
}
.info-box p {
margin: 8px 0;
color: #333;
}
.info-box strong {
color: #0c5460;
}
.buttons {
display: flex;
gap: 10px;
justify-content: center;
}
a {
padding: 12px 30px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
text-align: center;
}
a:hover {
background: #764ba2;
}
</style>
</head>
<body>
<div class="success-container">
<div class="success-icon">✓</div>
<h1>Rejestracja zakończona sukcesem!</h1>
<div class="success-message">
<p>Witaj, <strong><?= $username ?></strong>!</p>
<p>Twoje konto zostało pomyślnie utworzone.</p>
</div>
<div class="info-box">
<p><strong>Szczegóły operacji:</strong></p>
<p>ID gracza: <strong><?= $player_id ?></strong></p>
<p>Username: <strong><?= $username ?></strong></p>
<p>Email: <strong><?= $email ?></strong></p>
<p>Liczba dodanych rekordów: <strong><?= $affected_rows ?></strong></p>
<p>Data rejestracji: <strong><?= $timestamp ?></strong></p>
</div>
<div class="buttons">
<a href="profil.php?id=<?= $player_id ?>">Zobacz profil</a>
<a href="logowanie.php">Zaloguj się</a>
<a href="lista_graczy.php">Lista graczy</a>
</div>
</div>
</body>
</html>
Zalety:
- Bezpieczne (dane niewidoczne w URL)
- Można przekazać wiele danych
- Komunikat wyświetla się tylko raz
- Profesjonalne rozwiązanie
Wady:
- Wymaga sesji
- Trochę więcej kodu
Przykład 4: UPDATE z informacją o affected_rows
<?php
session_start();
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$player_id = (int)($_GET['id'] ?? 0);
if ($player_id <= 0) {
die('Nieprawidłowe ID gracza');
}
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
$level = (int)($_POST['level'] ?? 1);
$coins = (int)($_POST['coins'] ?? 0);
// Walidacja...
if (empty($email)) {
$errors[] = "Email jest wymagany";
}
if ($level < 1 || $level > 100) {
$errors[] = "Poziom musi być między 1 a 100";
}
if (empty($errors)) {
$stmt = $db->prepare("UPDATE players SET email = ?, level = ?, coins = ? WHERE id = ?");
$stmt->bind_param("siii", $email, $level, $coins, $player_id);
if ($stmt->execute()) {
$affected_rows = $stmt->affected_rows;
$stmt->close();
$db->close();
// Zapisz informacje w sesji
$_SESSION['update_result'] = [
'player_id' => $player_id,
'affected_rows' => $affected_rows,
'action' => 'update',
'message' => $affected_rows > 0
? "Profil został zaktualizowany ({$affected_rows} zmian)"
: "Nie wprowadzono żadnych zmian (dane są identyczne)"
];
// Przekierowanie
header("Location: profil.php?id={$player_id}");
exit;
}
$stmt->close();
}
}
$db->close();
?>
Plik: profil.php (wyświetlanie komunikatu)
<?php
session_start();
$player_id = (int)($_GET['id'] ?? 0);
// Sprawdź czy są dane o aktualizacji
$update_message = '';
$update_data = null;
if (isset($_SESSION['update_result'])) {
$update_data = $_SESSION['update_result'];
$update_message = $update_data['message'];
// Usuń z sesji po pobraniu
unset($_SESSION['update_result']);
}
// Połączenie i pobranie danych gracza...
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$stmt = $db->prepare("SELECT * FROM players WHERE id = ?");
$stmt->bind_param("i", $player_id);
$stmt->execute();
$wynik = $stmt->get_result();
$gracz = $wynik->fetch_assoc();
$stmt->close();
$db->close();
if (!$gracz) {
die('Nie znaleziono gracza');
}
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Profil - <?= htmlspecialchars($gracz['username']) ?></title>
<style>
.success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #c3e6cb;
}
.info {
background: #d1ecf1;
color: #0c5460;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #bee5eb;
}
</style>
</head>
<body>
<?php if (!empty($update_message)): ?>
<div class="<?= $update_data['affected_rows'] > 0 ? 'success' : 'info' ?>">
<?= htmlspecialchars($update_message) ?>
<?php if ($update_data['affected_rows'] > 0): ?>
<br><small>Liczba zaktualizowanych rekordów: <?= $update_data['affected_rows'] ?></small>
<?php endif; ?>
</div>
<?php endif; ?>
<h1>Profil gracza: <?= htmlspecialchars($gracz['username']) ?></h1>
<p>Email: <?= htmlspecialchars($gracz['email']) ?></p>
<p>Level: <?= $gracz['level'] ?></p>
<p>Coins: <?= number_format($gracz['coins']) ?></p>
<a href="edycja.php?id=<?= $player_id ?>">Edytuj profil</a>
</body>
</html>
Przykład 5: DELETE z potwierdzeniem i informacją o affected_rows
<?php
session_start();
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$player_id = (int)($_GET['id'] ?? 0);
if ($player_id <= 0) {
die('Nieprawidłowe ID gracza');
}
// Pobierz dane gracza przed usunięciem
$stmt = $db->prepare("SELECT * FROM players WHERE id = ?");
$stmt->bind_param("i", $player_id);
$stmt->execute();
$wynik = $stmt->get_result();
$gracz = $wynik->fetch_assoc();
$stmt->close();
if (!$gracz) {
die('Nie znaleziono gracza');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_delete'])) {
$stmt = $db->prepare("DELETE FROM players WHERE id = ?");
$stmt->bind_param("i", $player_id);
if ($stmt->execute()) {
$affected_rows = $stmt->affected_rows;
$stmt->close();
$db->close();
// Zapisz informacje w sesji
$_SESSION['delete_result'] = [
'username' => $gracz['username'],
'player_id' => $player_id,
'affected_rows' => $affected_rows,
'action' => 'delete',
'message' => $affected_rows > 0
? "Gracz '{$gracz['username']}' (ID: {$player_id}) został usunięty"
: "Nie usunięto gracza (możliwe że już nie istnieje)"
];
// Przekierowanie do listy graczy
header("Location: lista_graczy.php");
exit;
}
$stmt->close();
}
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Usuń gracza</title>
<style>
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
max-width: 600px;
width: 100%;
}
h1 {
color: #d32f2f;
margin-bottom: 20px;
}
.warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 20px;
margin-bottom: 20px;
border-radius: 5px;
}
.player-info {
background: #f8f9fa;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
.buttons {
display: flex;
gap: 10px;
}
button, a {
flex: 1;
padding: 15px;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
text-align: center;
text-decoration: none;
display: inline-block;
}
.btn-delete {
background: #d32f2f;
color: white;
}
.btn-delete:hover {
background: #b71c1c;
}
.btn-cancel {
background: #6c757d;
color: white;
}
.btn-cancel:hover {
background: #5a6268;
}
</style>
</head>
<body>
<div class="container">
<h1>Usuń gracza</h1>
<div class="warning">
<strong>OSTRZEŻENIE:</strong><br>
Ta operacja jest nieodwracalna! Gracz zostanie permanentnie usunięty z bazy danych.
</div>
<div class="player-info">
<h3>Dane gracza do usunięcia:</h3>
<p><strong>ID:</strong> <?= $gracz['id'] ?></p>
<p><strong>Username:</strong> <?= htmlspecialchars($gracz['username']) ?></p>
<p><strong>Email:</strong> <?= htmlspecialchars($gracz['email']) ?></p>
<p><strong>Level:</strong> <?= $gracz['level'] ?></p>
<p><strong>Coins:</strong> <?= number_format($gracz['coins']) ?></p>
</div>
<form method="POST">
<div class="buttons">
<a href="profil.php?id=<?= $player_id ?>" class="btn-cancel">Anuluj</a>
<button type="submit" name="confirm_delete" class="btn-delete"
onclick="return confirm('Czy NA PEWNO chcesz usunąć tego gracza?')">
Usuń gracza
</button>
</div>
</form>
</div>
</body>
</html>
Plik: lista_graczy.php (wyświetlanie komunikatu po usunięciu)
<?php
session_start();
// Sprawdź czy są dane o usunięciu
$delete_message = '';
$delete_data = null;
if (isset($_SESSION['delete_result'])) {
$delete_data = $_SESSION['delete_result'];
$delete_message = $delete_data['message'];
// Usuń z sesji po pobraniu
unset($_SESSION['delete_result']);
}
// Połączenie i pobranie listy graczy...
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$stmt = $db->prepare("SELECT * FROM players ORDER BY id DESC");
$stmt->execute();
$wynik = $stmt->get_result();
$gracze = $wynik->fetch_all(MYSQLI_ASSOC);
$stmt->close();
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Lista graczy</title>
<style>
.success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #c3e6cb;
}
.warning {
background: #fff3cd;
color: #856404;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #ffeeba;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
border: 1px solid #ddd;
text-align: left;
}
th {
background: #667eea;
color: white;
}
</style>
</head>
<body>
<h1>Lista graczy</h1>
<?php if (!empty($delete_message)): ?>
<div class="<?= $delete_data['affected_rows'] > 0 ? 'success' : 'warning' ?>">
<?= htmlspecialchars($delete_message) ?>
<?php if ($delete_data['affected_rows'] > 0): ?>
<br><small>Liczba usuniętych rekordów: <?= $delete_data['affected_rows'] ?></small>
<?php endif; ?>
</div>
<?php endif; ?>
<table>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Level</th>
<th>Akcje</th>
</tr>
<?php foreach ($gracze as $gracz): ?>
<tr>
<td><?= $gracz['id'] ?></td>
<td><?= htmlspecialchars($gracz['username']) ?></td>
<td><?= htmlspecialchars($gracz['email']) ?></td>
<td><?= $gracz['level'] ?></td>
<td>
<a href="profil.php?id=<?= $gracz['id'] ?>">Profil</a> |
<a href="edycja.php?id=<?= $gracz['id'] ?>">Edytuj</a> |
<a href="usun.php?id=<?= $gracz['id'] ?>">Usuń</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</body>
</html>
Porównanie metod przekazywania danych:
| Metoda | Bezpieczeństwo | Złożoność | Dane w URL | Wielokrotne odświeżenie | Kiedy używać |
|---|---|---|---|---|---|
| GET (URL) | Niskie | Proste | Widoczne | Bezpieczne | Proste ID, niepoufne dane |
| SESSION | Wysokie | Średnie | Ukryte | Bezpieczne | Komunikaty, wrażliwe dane |
Najlepsze praktyki:
- ZAWSZE używaj przekierowania po INSERT/UPDATE/DELETE
- Używaj SESSION dla komunikatów i wrażliwych danych
- Używaj GET tylko dla ID i prostych parametrów
- Zapisuj
insert_idiaffected_rowsprzed przekierowaniem - Usuwaj dane z sesji po jednokrotnym wyświetleniu
- ZAWSZE dodawaj
exit;poheader() - Waliduj dane po stronie odbiorczej (nawet po przekierowaniu)
TaskQuest 1.9 — zadania żyją w bazie: dodawanie, „Zrobione!” i usuwanie
KARTA PRACY
Po Lekcji 18 TaskQuest czyta z bazy wszystko, co pokazuje. Każdy gracz widzi własne zadania, XP i streak. Ale to, co widzi, może zmienić tylko ktoś w phpMyAdmin. Formularz „Dodaj zadanie” jest wyłączony, bo zapisuje do starego pliku CSV. Zadania nie da się oznaczyć jako wykonanego, więc XP stoi w miejscu. Streak jest nadal symulowany.
Dziś TaskQuest zacznie zmieniać dane w bazie. Po tej lekcji gracz dodaje zadania, klika „Zrobione!”, dostaje XP i buduje prawdziwą serię dni. Może też usunąć zadanie.
Cel lekcji
Nauczyć się:
- dodawać wiersze (
INSERT INTO ... VALUES), zmieniać je (UPDATE ... SET ... WHERE) i usuwać (DELETE FROM ... WHERE), - wykonywać te polecenia z PHP przez prepared statements i sprawdzać efekt (
affected_rows,insert_id), - używać
WHEREjako bezpiecznika. Warunek właściciela (uzytkownik_id = ?) pilnuje, żeby nikt nie zmienił cudzego zadania. Warunek stanu (data_wykonania IS NULL) pilnuje, żeby XP nie wpadło dwa razy, - zmieniać wartość względem obecnej (
xp = xp + ?), - połączyć zmianę danych z wzorcem POST → przekierowanie → GET z Lekcji 13 i komunikatem jednorazowym z Lekcji 14.
Efekt: formularz dodawania zapisuje do bazy i wraca na stronę. Przy każdym niewykonanym zadaniu jest przycisk „Zrobione!”, który przyznaje XP i aktualizuje streak. Przy każdym zadaniu jest przycisk „Usuń”. Plik CSV i funkcje plikowe znikają z projektu.
Uwaga — co jest, a czego jeszcze nie ma:
- Rejestracja i hasła przechodzą do Lekcji 22. W Lekcji 18 zapowiadaliśmy hasło już dziś. Zmieniamy plan świadomie: zapisywanie hasła (
password_hash()) i jego sprawdzanie przy logowaniu (password_verify()) pojawią się razem. Hasło, którego logowanie przez trzy lekcje nie sprawdza, dawałoby tylko pozór bezpieczeństwa. Logowanie zostaje więc takie jak po Lekcji 18.- Dziś nie ma edycji zadania (zmiana tytułu, terminu). To zadanie końcowe A.
- „Zrobione!” zmienia dwie tabele dwoma zapytaniami. Co się stanie, jeśli drugie się nie uda, zobaczysz w Eksperymencie 5. Rozwiązanie (transakcja) to zadanie końcowe C.
Rozgrzewka — 4 pytania z Lekcji 18
- Dlaczego login z formularza przekazujemy do zapytania przez
bind_param(), a nie wklejamy do tekstu SQL? wczytajUzytkownika($polaczenie, 99)zwracanull. Czy to awaria? Jak reaguje na toindex.php?- Po czym w bazie poznajemy, że zadanie jest wykonane? Jak w SQL wybrać tylko zadania niewykonane i dlaczego nie
= NULL? - Dlaczego w sesji zapisujemy tylko
uzytkownik_id, a nie login i imię?
1. Wracamy do naszego TaskQuest
Co zostaje, co dochodzi, co zmieniamy
| Element | Po Lekcji 18 | Po Lekcji 19 |
|---|---|---|
baza taskquest, sql/taskquest.sql | aplikacja tylko czyta | zostaje bez zmian; aplikacja także zapisuje |
includes/funkcje_bazy.php | połączenie + 4 funkcje z SELECT | dochodzi 5 funkcji: INSERT, UPDATE ×2, DELETE i SELECT jednego zadania |
dodaj-zadanie.php | zapisuje do CSV, niedostępny z nawigacji | zmiana: zapis do bazy, link „+ Dodaj zadanie” wraca |
oznacz-wykonane.php, usun-zadanie.php | nie ma | dochodzą (bez HTML, jak wyloguj.php) |
Uzytkownik::aktualizujStreak() | przelicza streak przy każdym wyświetleniu, na podstawie symulacji | zmiana: liczy streak raz, w chwili wykonania zadania |
$zadanieWykonane w index.php | symulacja „aktywności” | usuwamy — aktywność to kliknięcie „Zrobione!” |
data/zadania.csv, funkcje_plikow.php, PLIK_ZADAN, nastepneIdZadania() | używane tylko przez formularz | usuwamy — nic już z nich nie korzysta |
walidacja formularza (walidujNoweZadanie()), funkcja aktualizujStreak(), klasy, logowanie, sesja | istnieją | zostają bez zmian |
Zasada z Lekcji 18 obowiązuje dalej: obiekty i logika zostają, zmienia się miejsce, w którym żyją dane. Walidacja formularza, obliczanie poziomu i liczenie serii dni to kod, który już znasz.
Docelowa struktura projektu
taskquest/
├── index.php (zmiana: link „+ Dodaj zadanie”, przyciski „Zrobione!” i „Usuń”, bez symulacji streaka)
├── dodaj-zadanie.php (zmiana: zapis do bazy)
├── oznacz-wykonane.php NOWY
├── usun-zadanie.php NOWY (zadanie z sekcji 6)
├── zaloguj.php bez zmian
├── wyloguj.php bez zmian
├── diagnostyka.php (zmiana: lista plików, bez sekcji „Pliki danych”)
├── classes/
│ ├── Zadanie.php bez zmian
│ └── Uzytkownik.php (zmiana: aktualizujStreak())
├── config/
│ └── config.php (zmiana: bez PLIK_ZADAN)
├── includes/
│ ├── header.php (zmiana: styl form.akcja)
│ ├── footer.php (zmiana: „TaskQuest 1.9”)
│ ├── funkcje_bazy.php (zmiana: 5 nowych funkcji)
│ ├── funkcje_zadan.php (zmiana: bez nastepneIdZadania())
│ ├── funkcje_plikow.php USUWAMY
│ └── ... pozostałe bez zmian
├── sql/
│ └── taskquest.sql bez zmian
└── data/ USUWAMY (razem z zadania.csv)
Przygotowanie (zrób to teraz)
- Zrób kopię zapasową katalogu
taskquest/jakotaskquest_lekcja18/(obok, nie w środku). - Uruchom w XAMPP Apache i MySQL.
- W phpMyAdmin uruchom ponownie cały
sql/taskquest.sql. Dziś będziemy często psuć dane testowe. Ten skrypt przywraca je w kilka sekund.
2. Problem w aplikacji
Kama zrobiła wczoraj zadanie „Nauczyć się regexów PHP”. Loguje się do TaskQuest i chce je odhaczyć. Nie ma jak. Na liście jest [ ], XP stoi na 99. Zmienić to może tylko ktoś, kto wejdzie do phpMyAdmin i ręcznie przepisze liczby.
Ola chce dodać nowe zadanie. Formularz istnieje, ale zniknął z nawigacji, a pod bezpośrednim adresem zapisuje do pliku, którego strona już nie czyta.
TaskQuest jest na razie tablicą wyników, którą ktoś uzupełnia ręcznie. Żeby był grą, aplikacja musi umieć:
- dodać wiersz do tabeli
zadania(nowe zadanie), - zmienić wiersz: wpisać datę wykonania zadania, dopisać graczowi XP i zaktualizować serię dni,
- usunąć wiersz (zadanie niepotrzebne).
Są też dwa haczyki, o których pomyślimy od początku. Po pierwsze, nikt nie może zmienić cudzego zadania. Po drugie, zadanie wykonane raz nie może dawać XP drugi raz, nawet jeśli ktoś kliknie dwa razy.
3. Nowy mechanizm: INSERT, UPDATE, DELETE
3.1 Trzy polecenia w phpMyAdmin
Otwórz phpMyAdmin → baza taskquest → zakładka SQL. Przed każdym poleceniem zapisz, co Twoim zdaniem się stanie.
INSERT — dodaj wiersz
INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp)
VALUES (2, 'Przetestować INSERT', '', '2026-10-10', 10);
- Po nazwie tabeli wymieniamy kolumny, po
VALUESwartości w tej samej kolejności. To trochę jak argumenty nazwane z Lekcji 15: kolejność w bazie nie ma znaczenia, liczy się dopasowanie nazwy do wartości. - Brakuje dwóch kolumn:
ididata_wykonania.idnada baza (AUTO_INCREMENT).data_wykonaniadostanieNULL, bo kolumna maNULLbez innej wartości domyślnej. Nowe zadanie jest więc od razu niewykonane.
Sprawdź: SELECT id, tytul, data_wykonania FROM zadania WHERE uzytkownik_id = 2;. Ola ma teraz dwa zadania. Zapamiętaj id nowego.
UPDATE — zmień wiersz
UPDATE zadania
SET data_wykonania = CURDATE()
WHERE uzytkownik_id = 2 AND tytul = 'Przetestować INSERT';
SET kolumna = wartośćmówi, co zmienić.WHEREmówi, w których wierszach.CURDATE()to funkcja SQL: dzisiejsza data według serwera MySQL, w formacieRRRR-MM-DD.- phpMyAdmin pokaże „Zmieniono rekordów: 1” (w wersji angielskiej „1 row affected”).
Uruchom to samo polecenie jeszcze raz. Przewidź wynik, zanim klikniesz. Teraz zobaczysz „Zmieniono rekordów: 0”. Wiersz pasował do WHERE, ale data już była dzisiejsza, więc nic się nie zmieniło. MySQL liczy zmienione wiersze, nie pasujące. Ta różnica przyda się w TaskQuest.
Teraz ten sam UPDATE z dodatkowym warunkiem:
UPDATE zadania
SET data_wykonania = CURDATE()
WHERE uzytkownik_id = 2 AND tytul = 'Przetestować INSERT' AND data_wykonania IS NULL;
Wynik: 0. Warunek mówi „zmień tylko, jeśli zadanie jeszcze nie jest wykonane”. Zapamiętaj ten wzór — od niego zależy, czy gracz dostanie XP raz, czy wiele razy.
UPDATE względem obecnej wartości
UPDATE uzytkownicy SET xp = xp + 10 WHERE id = 2;
Po prawej stronie = stoi stara wartość kolumny. Ola miała 250, ma 260. Uruchom jeszcze raz — 270. Nie musimy znać obecnej liczby XP, żeby ją zwiększyć. Baza dolicza do tego, co ma w chwili zmiany.
DELETE — usuń wiersz
DELETE FROM zadania WHERE uzytkownik_id = 2 AND tytul = 'Przetestować INSERT';
phpMyAdmin zapyta o potwierdzenie. Tak ma być: DELETE jest nieodwracalny, nie ma kosza.
Najważniejsze zdanie tej lekcji:
UPDATEiDELETEbezWHEREdziałają na wszystkich wierszach tabeli. Nie ma pytania „czy na pewno?” w PHP. Zobaczysz to w Eksperymencie 1.
Uruchom ponownie sql/taskquest.sql — Ola wraca do 250 XP.
3.2 Te same polecenia z PHP
Z SELECT znasz cztery kroki: prepare() → bind_param() → execute() → get_result(). Przy INSERT, UPDATE i DELETE baza nie zwraca wierszy, więc ostatniego kroku nie ma. Zamiast wyniku sprawdzamy, ile wierszy się zmieniło.
Utwórz brudnopis.php w katalogu taskquest/:
<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
$polaczenie = polaczZBaza();
$premia = 5;
$uzytkownikId = 2;
$zapytanie = $polaczenie->prepare('UPDATE uzytkownicy SET xp = xp + ? WHERE id = ?');
$zapytanie->bind_param('ii', $premia, $uzytkownikId);
$zapytanie->execute();
var_dump($zapytanie->affected_rows);
Oczekiwane: int(1). Sprawdź w phpMyAdmin: Ola ma 255 XP.
Teraz dwie próby:
- Odśwież brudnopis trzy razy. Ile XP ma Ola? (270.) Każde odświeżenie to nowe żądanie, a każde żądanie wykonuje
UPDATEod nowa. Dlatego od Lekcji 13 dane zmieniamy wyłącznie przez POST + przekierowanie, nigdy przez zwykłe wejście na adres. - Zmień
$uzytkownikIdna99. Wynik:int(0)i żaden wyjątek. Zapytanie jest poprawne, tylko żaden wiersz nie pasował. To ta sama sytuacja conullzwczytajUzytkownika()w Lekcji 18: brak pasujących danych to nie awaria.
Co mówi affected_rows:
| Polecenie | affected_rows to… |
|---|---|
INSERT | liczba dodanych wierszy (u nas zawsze 1) |
UPDATE | liczba wierszy, które się naprawdę zmieniły (nie tych, które pasowały do WHERE) |
DELETE | liczba usuniętych wierszy |
Błąd zapytania (literówka, zła wartość) nie daje specjalnej liczby. Od Lekcji 18 polaczZBaza() ustawia MYSQLI_REPORT_STRICT, więc błąd to wyjątek.
Po INSERT przydaje się jeszcze jedna właściwość: $zapytanie->insert_id. To id, które baza nadała nowemu wierszowi przez AUTO_INCREMENT. Nie musimy go zgadywać, jak robiła to nastepneIdZadania() przy CSV.
Uruchom ponownie sql/taskquest.sql.
3.3 WHERE jako bezpiecznik
W TaskQuest każde UPDATE i DELETE na zadaniu będzie miało warunek na dwie rzeczy naraz.
1. Czyje to zadanie? Przycisk „Zrobione!” wyśle do serwera id zadania w ukrytym polu formularza. „Ukryte” znaczy tylko „niewidoczne na stronie”. Każdy może je zmienić w narzędziach przeglądarki (F12) i wysłać id zadania Kamy, będąc zalogowanym jako Ola. Zasada z Lekcji 13 obowiązuje: danym z formularza nie ufamy. Ufamy za to $_SESSION['uzytkownik_id'], bo zapisał go nasz kod. Dlatego:
WHERE id = ? AND uzytkownik_id = ?
Jeśli ktoś podrobi id, warunek nie pasuje, zmienia się 0 wierszy. Prepared statement tu nie pomoże — to nie SQL injection, tylko brak sprawdzenia uprawnień (Eksperyment 3).
2. Czy wolno to jeszcze zmienić? XP ma wpaść tylko raz. Najprostszy pomysł to sprawdzić w PHP: wczytać zadanie, if (!$zadanie->czyWykonane()), potem UPDATE. Ale co, jeśli gracz ma TaskQuest otwarty w dwóch kartach i kliknie „Zrobione!” w obu? Oba żądania wczytają zadanie jako niewykonane, oba przejdą przez if, oba dodadzą XP.
Dlatego o tym, czy zmiana się należy, decyduje ten sam UPDATE, który zmienia dane:
UPDATE zadania SET data_wykonania = CURDATE()
WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL
InnoDB blokuje wiersz na czas takiej zmiany. Drugie żądanie poczeka, zobaczy już wpisaną datę i dostanie affected_rows = 0. W PHP przyznajemy XP tylko wtedy, gdy affected_rows wynosi 1. Sprawdzisz to w Teście 4 i Eksperymencie 2.
Typowe błędy
- Brak
WHEREwUPDATE/DELETE— zmiana wszystkich wierszy (Eksperyment 1). - Stała albo wyrażenie w
bind_param():bind_param('ii', 5, $uzytkownikId)albobind_param('i', (int) $xpZadania)kończy sięError(„could not be passed by reference”).bind_param()przyjmuje tylko zmienne. Wartość zapisz najpierw w zmiennej. (W funkcji to się nie zdarza, bo parametry funkcji to zmienne.) - Liczba liter w
bind_param()różna od liczby?—ArgumentCountErroralbo błąd zapytania. Liczymy?od lewej i dla każdego piszemy literęi(liczba całkowita) albos(tekst, także data). get_result()poUPDATE/DELETE— nie ma wyniku do odczytania. Efekt sprawdzamy przezaffected_rows.affected_rows > 0jako „wiersz istnieje” — przyUPDATE0 może też znaczyć „wiersz istnieje, ale wartość była taka sama” (3.1).
4. SAMODZIELNIE — pierwsze zadanie dodane z PHP
W brudnopis.php dodaj do bazy zadanie dla Oli (uzytkownik_id = 2):
- tytuł
Zadanie z brudnopisu, opis pusty, termin2026-10-15, 15 XP, - dane zapisz najpierw w zmiennych (
$uzytkownikId,$tytul,$opis,$termin,$xp) — tak, jakby przyszły z formularza, - zapytanie przez prepared statement,
- na końcu wypisz
affected_rowsiinsert_id.
Potem zaloguj się do TaskQuest jako Ola_TQ. Zadanie z brudnopisu powinno być na liście, bez żadnych zmian w index.php. Strona czyta z bazy od Lekcji 18.
HINT
Kolumn podajesz pięć, więc w VALUES będzie pięć znaków ?. Termin to tekst w formacie RRRR-MM-DD — jaką literą go oznaczysz?
POMOC
$zapytanie = $polaczenie->prepare(
'INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp) VALUES (?, ?, ?, ?, ?)'
);
$zapytanie->bind_param('.....', ...);
Litery w bind_param() idą w tej samej kolejności co ?: liczba, tekst, tekst, tekst, liczba.
ROZWIĄZANIE
<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
$polaczenie = polaczZBaza();
$uzytkownikId = 2;
$tytul = 'Zadanie z brudnopisu';
$opis = '';
$termin = '2026-10-15';
$xp = 15;
$zapytanie = $polaczenie->prepare(
'INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp) VALUES (?, ?, ?, ?, ?)'
);
$zapytanie->bind_param('isssi', $uzytkownikId, $tytul, $opis, $termin, $xp);
$zapytanie->execute();
var_dump($zapytanie->affected_rows);
var_dump($zapytanie->insert_id);
Oczekiwane: int(1) i liczba o jeden większa od największego id w tabeli zadania.
Odśwież brudnopis jeszcze raz. Co się stało? Ola ma dwa identyczne zadania, z różnymi id. Baza nie wie, że „to samo zadanie” już jest. Każde wykonanie INSERT to nowy wiersz. Dlatego formularz w TaskQuest kończy się przekierowaniem (POST → przekierowanie → GET, Lekcja 13). Odświeżenie strony po dodaniu nie wyśle formularza drugi raz.
Sprawdź też: zmień $uzytkownikId na 99 i uruchom. Wynik: wyjątek z komunikatem o kluczu obcym (Cannot add or update a child row: a foreign key constraint fails...). Baza nie pozwala dodać zadania użytkownikowi, którego nie ma. To relacja z Lekcji 17 w działaniu. Przywróć 2.
Uruchom ponownie sql/taskquest.sql.
5. Włączamy zapis do TaskQuest
Od tego miejsca pracujesz na projekcie.
brudnopis.phpzostaw — przyda się do testowania nowych funkcji.
Plan: najpierw dodawanie zadań i sprzątanie po CSV (kroki 1–3), potem „Zrobione!” z XP i streakiem (kroki 4–7), na końcu diagnostyka (krok 8). Po każdym kroku aplikacja działa.
Krok 1 — SAMODZIELNIE: funkcja dodajZadanie()
Kod z brudnopisu (sekcja 4) zamienisz w funkcję. W kroku 2 formularz wywoła ją jedną linią, zamiast budować zapytanie od nowa.
Co zrobić:
1. Otwórz includes/funkcje_bazy.php. W komentarzu na górze pliku zamień (Lekcja 18) na (Lekcje 18–19).
2. Pod funkcją pobierzRanking() utwórz funkcję z takim nagłówkiem:
function dodajZadanie(mysqli $polaczenie, int $uzytkownikId, string $tytul, string $opis, string $termin, int $xp): int
3. Przenieś do niej z brudnopisu tylko linie, które wykonują zapytanie. Przy każdej linii brudnopisu zadaj sobie pytanie: czy funkcja jej potrzebuje, czy tę wartość dostaje już w parametrze?
4. Na końcu funkcja ma zwrócić id nowego zadania (3.2).
5. Zasady są te same co dla funkcji z Lekcji 18. Funkcja nie łączy się sama z bazą, niczego nie wyświetla, nie łapie wyjątków i nie sprawdza danych, bo dostaje je już sprawdzone przez walidujNoweZadanie().
Sprawdzenie. W brudnopisie zostaw dwa require_once i $polaczenie = polaczZBaza();, a resztę zastąp:
var_dump(dodajZadanie($polaczenie, 2, 'Test funkcji', '', '2026-10-20', 5));
Oczekiwane: int(...) — nowe id. W phpMyAdmin Ola ma zadanie „Test funkcji”. Uruchom ponownie sql/taskquest.sql.
HINT
Z brudnopisu nie przenosisz: require_once, polaczZBaza(), pięciu linii z wartościami ($uzytkownikId = 2; itd. — to teraz parametry funkcji) i var_dump(). Zostają trzy kroki: prepare(), bind_param(), execute().
ROZWIĄZANIE
function dodajZadanie(
mysqli $polaczenie,
int $uzytkownikId,
string $tytul,
string $opis,
string $termin,
int $xp
): int {
$zapytanie = $polaczenie->prepare(
'INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp) VALUES (?, ?, ?, ?, ?)'
);
$zapytanie->bind_param('isssi', $uzytkownikId, $tytul, $opis, $termin, $xp);
$zapytanie->execute();
return $zapytanie->insert_id;
}
data_wykonania nie jest parametrem, bo nowe zadanie jest zawsze niewykonane. Zwracanego id formularz nie potrzebuje, ale przydaje się w testach (i w zadaniu końcowym A).
Krok 2 — dodaj-zadanie.php zapisuje do bazy
Otwórz dodaj-zadanie.php. Zmieniamy tylko część PHP, i to niewiele.
2a. Dołączane pliki. Usuń dwie linie:
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
a pod require_once pliku funkcje_sesji.php dopisz:
require_once __DIR__ . '/includes/funkcje_bazy.php';
Strona nie tworzy już obiektu Zadanie (zapisuje dane prosto do bazy), więc nie potrzebuje klasy.
2b. SAMODZIELNIE: zapis do bazy. Znajdź w dodaj-zadanie.php warunek if (count($bledy) === 0) {. W nim jest blok try, który dziś zapisuje do CSV w trzech krokach: wczytajZadania(PLIK_ZADAN) → $zadania[] = new Zadanie(...) → zapiszZadania(...). Pod nimi są jeszcze trzy linie: ustawKomunikat(...), header(...) i exit;.
Co zrobić:
- Usuń z
trytrzy linie zapisu do CSV (wczytanie listy, dopisanie obiektu, zapis listy). Trzy linie komunikatu, przekierowania iexitzostaw. Całycatchteż zostaw bez zmian. - W ich miejsce (na początku
try) połącz się z bazą i wywołajdodajZadanie()z kroku 1. Przekaż jej połączenie, id zalogowanego gracza i cztery dane z formularza. - Przy każdym argumencie sprawdź typ, którego wymaga nagłówek funkcji.
$xpZadaniato tekst z formularza, a funkcja chceint.
Odczyt pól z $_POST i wywołanie walidujNoweZadanie() zostają bez zmian.
Sprawdzenie — punkt 2d poniżej.
HINT
Id zalogowanego gracza nie przychodzi z formularza, tylko z sesji (Lekcja 18: jedyny klucz tożsamości). Tekst z samymi cyframi zamieniasz na liczbę tak samo jak robiła to dotąd linia z new Zadanie(...).
ROZWIĄZANIE
try {
$polaczenie = polaczZBaza();
dodajZadanie(
$polaczenie,
$_SESSION['uzytkownik_id'],
$tytulZadania,
$opisZadania,
$terminZadania,
(int) $xpZadania
);
ustawKomunikat('Zadanie zostało dodane.');
header('Location: index.php');
exit;
} catch (Exception $wyjatek) {
$bladZapisu = $wyjatek->getMessage();
}
Co zniknęło i dlaczego:
- wczytywanie całej listy przed zapisem — przy CSV trzeba było wczytać plik, dopisać zadanie i zapisać wszystko od nowa (tryb
w). Baza dopisuje jeden wiersz, reszty nie dotyka, nastepneIdZadania()—idnadajeAUTO_INCREMENT,- lokalna zmienna
$zadania— niepotrzebna.
Co zostało: walidacja, zachowywanie wpisanych danych w formularzu, ramka $bladZapisu („Nie udało się dodać zadania: …”), komunikat w sesji i przekierowanie. (int) $xpZadania jest bezpieczne, bo walidacja sprawdziła regexem /^\d+$/, że to same cyfry.
Właściciela zadania bierzemy z sesji, nie z formularza. W formularzu nie ma (i nie będzie) pola „dla kogo” — nikt nie doda zadania na cudze konto.
Po co walidacja, skoro baza sama odrzuci za długi tytuł? Trzy powody. Baza zgłasza wyjątek z technicznym komunikatem, a użytkownik ma dostać listę zrozumiałych błędów (reguła z Lekcji 13). Zakres XP 1–100 sprawdza tylko PHP (Lekcja 17). Walidacja pokazuje wszystkie błędy naraz, baza zatrzymuje się na pierwszym.
2c. Link wraca. W index.php, pod <h2>Twoje zadania</h2>, przywróć:
<p><a href="dodaj-zadanie.php">+ Dodaj zadanie</a></p>
2d. Sprawdzenie. Zaloguj się jako Kama, dodaj zadanie. Oczekiwane: komunikat „Zadanie zostało dodane.”, nowe zadanie na liście. Zaloguj się jako Ola — tego zadania nie widzi.
Krok 3 — sprzątanie po CSV
Od tej chwili nic w TaskQuest nie czyta ani nie zapisuje data/zadania.csv. Martwy kod myli: za miesiąc nikt nie będzie pamiętał, czy ten plik jest jeszcze potrzebny. Dane z CSV są bezpieczne — w Lekcji 17 trafiły do sql/taskquest.sql.
Zanim usuniesz, sprawdź. W edytorze wyszukaj w całym projekcie (VS Code: Ctrl+Shift+F) każdą z nazw:
PLIK_ZADAN
wczytajZadania(
zapiszZadania(
sprawdzWierszZadania(
nastepneIdZadania(
funkcje_plikow
Wpisz wczytajZadania( razem z nawiasem. Bez nawiasu wyszukiwarka znajdzie też wczytajZadaniaUzytkownika. Oczekiwane: nazwy występują tylko w definicjach i w diagnostyka.php (tę poprawisz w kroku 8). Jeśli znajdziesz inne miejsce, najpierw wróć do kroku 2.
Usuń:
- plik
includes/funkcje_plikow.php(funkcjewczytajZadania(),zapiszZadania(),sprawdzWierszZadania()), - katalog
data/razem zzadania.csv, - w
config/config.phpliniędefine('PLIK_ZADAN', ...);, - w
includes/funkcje_zadan.phpcałą funkcjęnastepneIdZadania().walidujNoweZadanie()i funkcje listy zostają.
Odśwież index.php i dodaj jeszcze jedno zadanie — wszystko ma działać jak po kroku 2.
Krok 4 — SAMODZIELNIE: jedno zadanie i jego wykonanie
„Zrobione!” potrzebuje dwóch funkcji w funkcje_bazy.php. Napisz je pod dodajZadanie().
Funkcja 1:
function wczytajZadanieUzytkownika(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): ?Zadanie
- zwraca obiekt
Zadanieo podanymid, ale tylko jeśli należy do podanego użytkownika, - w każdym innym przypadku (nie ma takiego zadania, jest cudze) zwraca
null.
Funkcja 2:
function oznaczZadanieJakoWykonane(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
- wpisuje dzisiejszą datę w
data_wykonania, - tylko w zadaniu tego użytkownika i tylko jeśli zadanie nie jest jeszcze wykonane,
- zwraca
true, jeśli to wywołanie zmieniło wiersz, afalsew każdym innym przypadku.
Test w brudnopisie (dołącz też classes/Zadanie.php). id zadania Oli odczytaj w phpMyAdmin (po świeżym taskquest.sql to ostatni wiersz tabeli zadania):
$zadanieOli = 6; // wpisz id zadania Oli ze swojej bazy
var_dump(wczytajZadanieUzytkownika($polaczenie, $zadanieOli, 1));
var_dump(wczytajZadanieUzytkownika($polaczenie, $zadanieOli, 2)->pobierzTytul());
var_dump(oznaczZadanieJakoWykonane($polaczenie, $zadanieOli, 1));
var_dump(oznaczZadanieJakoWykonane($polaczenie, $zadanieOli, 2));
var_dump(oznaczZadanieJakoWykonane($polaczenie, $zadanieOli, 2));
Przewidź pięć wyników, potem uruchom. Oczekiwane: NULL (zadanie Oli, a pytamy jako Kama), string(28) "Zaprojektować bazę TaskQuest", false (Kama nie może oznaczyć zadania Oli), true, false (już wykonane). Na koniec uruchom ponownie sql/taskquest.sql.
HINT
Funkcja 1 to połączenie dwóch funkcji z Lekcji 18: zapytanie i tworzenie obiektu jak w wczytajZadaniaUzytkownika(), a obsługa „jeden wiersz albo nic” jak w wczytajUzytkownika(). Funkcja 2 to UPDATE z trzema warunkami z 3.3. Wynik funkcji to jedno porównanie.
ROZWIĄZANIE
function wczytajZadanieUzytkownika(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): ?Zadanie
{
$zapytanie = $polaczenie->prepare(
'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE id = ? AND uzytkownik_id = ?'
);
$zapytanie->bind_param('ii', $zadanieId, $uzytkownikId);
$zapytanie->execute();
$wiersz = $zapytanie->get_result()->fetch_assoc();
if ($wiersz === null) {
return null;
}
return new Zadanie(
id: $wiersz['id'],
tytul: $wiersz['tytul'],
opis: $wiersz['opis'],
termin: $wiersz['termin'],
wykonane: $wiersz['data_wykonania'] !== null,
xp: $wiersz['xp'],
);
}
function oznaczZadanieJakoWykonane(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
$zapytanie = $polaczenie->prepare(
'UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL'
);
$zapytanie->bind_param('ii', $zadanieId, $uzytkownikId);
$zapytanie->execute();
return $zapytanie->affected_rows === 1;
}
- „Nie ma takiego zadania” i „zadanie jest cudze” dają ten sam wynik:
null. Celowo. Ola nie powinna się dowiedzieć, że zadanie oid3 istnieje, tylko należy do kogoś innego. oznaczZadanieJakoWykonane()zwracafalsez trzech powodów: nie ma zadania, jest cudze albo już wykonane. Funkcja ich nie rozróżnia. Strona, która ją wywoła, zrobi to sama dzięki funkcji 1.
Krok 5 — prawdziwy streak i XP
5a. Jak dotąd liczyliśmy streak
Funkcja aktualizujStreak() z funkcje_terminow.php zostaje bez zmian:
function aktualizujStreak(int $streak, string $dataOstatniejAktywnosci, bool $zadanieWykonane): int {
if (!$zadanieWykonane) return $streak;
$dzisiaj = new DateTime('today');
$wczoraj = new DateTime('yesterday');
$ostatniaAktywnosc = new DateTime($dataOstatniejAktywnosci);
if ($ostatniaAktywnosc->format('Y-m-d') === $dzisiaj->format('Y-m-d')) return $streak;
if ($ostatniaAktywnosc->format('Y-m-d') === $wczoraj->format('Y-m-d')) return $streak + 1;
return 1;
}
Przypomnienie reguł: aktywność dziś — seria bez zmian; wczoraj — seria +1; wcześniej — seria zaczyna się od nowa (1).
Do tej pory index.php przy każdym wyświetleniu wywoływał $uzytkownik->aktualizujStreak($zadanieWykonane), a $zadanieWykonane znaczyło „na liście jest choć jedno wykonane zadanie”. To była symulacja: nie wiedzieliśmy, kiedy gracz coś zrobił. Wynik nigdzie się nie zapisywał, więc każde odświeżenie liczyło od nowa.
Teraz wiemy dokładnie: gracz był aktywny w chwili, gdy kliknął „Zrobione!”. Streak liczymy więc raz, w tym momencie, i zapisujemy do bazy razem z datą aktywności. Strona główna tylko go pokazuje.
5b. Zmiana w classes/Uzytkownik.php
Otwórz classes/Uzytkownik.php i znajdź metodę aktualizujStreak() (ostatnio zmienianą w Lekcji 18, krok 4). Zmień ją w czterech miejscach:
- Nagłówek. Usuń parametr:
aktualizujStreak(bool $zadanieWykonane): void→aktualizujStreak(): void. Metodę będziemy wywoływać tylko po wykonaniu zadania, więc parametr zawsze miałby wartośćtrue. - Warunek
null. Dziś jest w nim samoreturn;z komentarzem „brak aktywności — … streak zostaje taki jak w bazie”. Przedreturn;dopisz linię, która ustawia właściwośćstreakobiektu na1. Zmień komentarz na:// pierwsze wykonane zadanie w historii — seria się zaczyna. Od teraznullznaczy „gracz jeszcze nigdy nic nie wykonał”, a ta metoda jest wywoływana właśnie wtedy, gdy wykonuje pierwsze zadanie. - Wywołanie funkcji. W ostatniej linii metody trzeci argument funkcji
aktualizujStreak(...)to$zadanieWykonane. Tej zmiennej już nie ma — wpisz w jej miejscetrue. - Komentarz nad metodą (linia nad
public function):// Wywołujemy tylko wtedy, gdy użytkownik właśnie wykonał zadanie (oznacz-wykonane.php).
Nazwa metody się nie zmienia. Nadal aktualizuje streak według reguł funkcji o tej samej nazwie.
data_ostatniej_aktywnosci przed kliknięciem | streak przed | streak po |
|---|---|---|
| dziś | 5 | 5 |
| wczoraj | 5 | 6 |
| trzy dni temu | 5 | 1 |
NULL | 0 | 1 |
Sprawdzenie w brudnopisie (dołącz classes/Uzytkownik.php, includes/funkcje_grywalizacji.php, includes/funkcje_terminow.php):
$wczoraj = new DateTime('yesterday');
$uzytkownik = new Uzytkownik(
id: 1,
login: 'Kama_2026',
imieUzytkownika: 'Kama',
xp: 99,
streak: 5,
dataOstatniejAktywnosci: $wczoraj->format('Y-m-d'),
);
$uzytkownik->aktualizujStreak();
var_dump($uzytkownik->pobierzStreak());
Oczekiwane: int(6). Sprawdź pozostałe wiersze tabeli: zamień datę na '2026-01-01', a potem na null.
index.phpnadal wywołuje$uzytkownik->aktualizujStreak($zadanieWykonane). PHP nie zgłosi błędu (nadmiarowy argument jest pomijany), ale strona może teraz pokazywać zawyżony streak. Usuniemy to wywołanie w kroku 7a.
ROZWIĄZANIE – metoda po zmianach
// Wywołujemy tylko wtedy, gdy użytkownik właśnie wykonał zadanie (oznacz-wykonane.php).
public function aktualizujStreak(): void
{
if ($this->dataOstatniejAktywnosci === null) {
$this->streak = 1; // pierwsze wykonane zadanie w historii — seria się zaczyna
return;
}
$this->streak = aktualizujStreak($this->streak, $this->dataOstatniejAktywnosci, true);
}
5c. SAMODZIELNIE: funkcja zapiszPostepUzytkownika()
Po wykonaniu zadania w tabeli uzytkownicy muszą się zmienić trzy kolumny gracza:
xp— dodajemy nagrodę za zadanie,streak— wpisujemy nową wartość, policzoną w 5b,data_ostatniej_aktywnosci— dzisiejsza data.
W funkcje_bazy.php, pod oznaczZadanieJakoWykonane(), napisz funkcję:
function zapiszPostepUzytkownika(mysqli $polaczenie, int $uzytkownikId, int $zdobyteXp, int $streak): void
Wymagania:
- Jedno zapytanie
UPDATEzmienia wszystkie trzy kolumny. - XP dodajesz do wartości, która jest w bazie (wzór
xp = xp + ...z 3.1), a nie wpisujesz gotowej liczby. - Streak wpisujesz dokładnie taki, jaki funkcja dostała w parametrze.
- Datę ustawia funkcja SQL — ta sama, której używa
oznaczZadanieJakoWykonane(). - Zmienia się tylko wiersz tego jednego użytkownika.
Sprawdzenie w brudnopisie:
zapiszPostepUzytkownika($polaczenie, 2, 30, 1);
Oczekiwane w phpMyAdmin: Ola ma 280 XP, streak 1 i dzisiejszą datę aktywności. Kama bez zmian. Uruchom ponownie sql/taskquest.sql.
HINT
W SET kolumny oddzielasz przecinkami: SET a = ..., b = ..., c = .... Znaków ? będą trzy. Policz, w jakiej kolejności stoją w zapytaniu, i w tej samej kolejności ułóż litery i zmienne w bind_param().
POMOC
UPDATE uzytkownicy SET xp = xp + ?, streak = ?, data_ostatniej_aktywnosci = ... WHERE ...
ROZWIĄZANIE
function zapiszPostepUzytkownika(mysqli $polaczenie, int $uzytkownikId, int $zdobyteXp, int $streak): void
{
$zapytanie = $polaczenie->prepare(
'UPDATE uzytkownicy SET xp = xp + ?, streak = ?, data_ostatniej_aktywnosci = CURDATE() WHERE id = ?'
);
$zapytanie->bind_param('iii', $zdobyteXp, $streak, $uzytkownikId);
$zapytanie->execute();
}
Dlaczego XP dodaje SQL, a streak liczy PHP?
- XP. Moglibyśmy wywołać
$uzytkownik->dodajXp(...)i zapisać gotową liczbę (xp = ?). Problem: obiekt zna XP z chwili wczytania. Jeśli w tym samym czasie drugie żądanie tego gracza doda XP (druga karta), zapis gotowej liczby nadpisze tamten wynik i XP przepadnie.xp = xp + ?dolicza do tego, co jest w bazie w chwili zapisu. - Streak. Reguły serii są w PHP (funkcja
aktualizujStreak()) i nie przepisujemy ich na SQL. - Data.
CURDATE(), ta sama funkcja co przydata_wykonania. Obie daty ustawia ten sam zegar (MySQL).
Krok 6 — strona oznacz-wykonane.php
Ta strona robi jedną rzecz: przyjmuje kliknięcie „Zrobione!”, zapisuje zmiany w bazie i odsyła gracza na listę. Niczego nie wyświetla (poza awarią bazy), podobnie jak wyloguj.php. Zbudujemy ją w pięciu krokach, a po niektórych od razu sprawdzisz efekt. Cały plik znajdziesz w sekcji 9 — zajrzyj tam dopiero po skończeniu.
6a. Nowy plik i dołączane pliki
- W katalogu
taskquest/(obokindex.php) utwórz plikoznacz-wykonane.php. Zacznij od<?php. Plik ma sam PHP, więc bez zamykającego?>. - Pierwsza instrukcja:
session_start();— strona korzysta z sesji (konwencja od Lekcji 14). - Pod nią dołącz przez
require_once __DIR__ . '/...';wszystkie potrzebne pliki. Listę ułóż sama/sam, odpowiadając na pytania:- skąd funkcje bazy wezmą stałe
BAZA_...? - strona dostanie z bazy obiekt
Zadaniei obiektUzytkownik— co musi być dołączone, żeby te obiekty mogły powstać? - metody klasy
Uzytkownikkorzystają z funkcji z dwóch plików (napisane jest to w komentarzu na górzeclasses/Uzytkownik.php) — z których? - w którym pliku są
wymagajLogowania()iustawKomunikat()? - w którym są funkcje bazy?
- skąd funkcje bazy wezmą stałe
- Pod dołączeniami wywołaj
wymagajLogowania();.
Sprawdzenie. Zalogowana/zalogowany wpisz w pasku adresu http://localhost/taskquest/oznacz-wykonane.php. Oczekiwane: pusta biała strona, bez błędów, bo plik jeszcze nic nie robi. Po wylogowaniu ten sam adres przekierowuje na logowanie.
HINT
Plików jest siedem. Porównaj z listą require_once w index.php — ta strona potrzebuje prawie tych samych, ale nie wyświetla listy zadań ani nie czyści tytułów.
ROZWIĄZANIE
<?php
session_start();
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
wymagajLogowania();
6b. Tylko POST
Strona zmienia dane, więc reaguje wyłącznie na POST (reguła z Lekcji 13). Każde inne wejście, np. wpisanie adresu, odsyła na listę bez żadnej zmiany.
Otwórz wyloguj.php. Na jego początku jest warunek, który sprawdza metodę żądania i przekierowuje. Przepisz ten sam warunek do oznacz-wykonane.php, pod wymagajLogowania();. Sprawdź, czy przekierowuje na index.php.
Sprawdzenie. Ten sam adres co w 6a (zalogowana/zalogowany). Oczekiwane: zamiast pustej strony — przekierowanie na listę zadań.
6c. Numer zadania z formularza
Przycisk „Zrobione!” (krok 7) wyśle w formularzu pole id — numer klikniętego zadania. Pod warunkiem z 6b:
- Odczytaj
$_POST['id']do zmiennej$zadanieIdTekst. Zrób to tak samo jak odczyt pól wdodaj-zadanie.php, czyli z?? '', bo pola może w ogóle nie być. - Sprawdź, czy tekst składa się z samych cyfr. Użyj tego samego wyrażenia regularnego, którym
walidujNoweZadanie()sprawdza XP. Jeśli nie pasuje, zrób przekierowanie naindex.phpiexit, bez komunikatu. Zwykły formularz zawsze wysyła liczbę, więc coś innego oznacza, że ktoś przy nim majstrował. - Dopiero po sprawdzeniu zamień tekst na liczbę i zapisz w
$zadanieId. - Numer zalogowanego gracza zapisz w
$uzytkownikId(z$_SESSION['uzytkownik_id']). Nie potrzebujesz tu??, bo wyżej jestwymagajLogowania().
Działanie tego fragmentu sprawdzisz w Teście 6 (F12), gdy będzie już przycisk.
ROZWIĄZANIE
$zadanieIdTekst = $_POST['id'] ?? '';
if (!preg_match('/^\d+$/', $zadanieIdTekst)) {
header('Location: index.php');
exit;
}
$zadanieId = (int) $zadanieIdTekst;
$uzytkownikId = $_SESSION['uzytkownik_id'];
6d. SAMODZIELNIE: trzy możliwe wyniki kliknięcia
Pod kodem z 6c otwórz blok try. W nim połącz się z bazą (polaczZBaza()) i obsłuż trzy sytuacje:
| Sytuacja | Jak ją rozpoznasz | Co robi strona |
|---|---|---|
| zadania nie ma albo jest cudze | wczytajZadanieUzytkownika() zwraca null | komunikat Nie znaleziono takiego zadania. |
| zadanie jest już wykonane | oznaczZadanieJakoWykonane() zwraca false | komunikat To zadanie jest już wykonane. |
| udało się | oznaczZadanieJakoWykonane() zwraca true | wczytuje gracza z bazy, aktualizuje jego streak (5b), zapisuje postęp (5c), komunikat Zadanie wykonane! +N XP, gdzie N to nagroda za zadanie |
Komunikaty ustawiasz funkcją ustawKomunikat() (Lekcja 14).
try wymaga catch. Napiszesz go w 6e — zrób to od razu, zanim uruchomisz stronę. Pod całym try/catch dopisz przekierowanie na index.php i exit. Wykona się ono w każdej z trzech sytuacji, bo komunikat czeka w sesji.
HINT
- Kolejność ma znaczenie: najpierw odczyt zadania (wiersz 1 tabeli), potem zmiana (wiersze 2 i 3). Wystarczy jedno
if/elseif/else. - W wierszu 2 wywołanie funkcji możesz wpisać bezpośrednio w warunek
elseif— funkcja wykonaUPDATEi od razu zwrócitruealbofalse. - Nagrodę za zadanie weźmiesz z obiektu zadania. Nowy streak weźmiesz z obiektu gracza po wywołaniu
aktualizujStreak().
POMOC
try {
$polaczenie = polaczZBaza();
$zadanie = /* zadanie tego gracza albo null */;
if (/* nie ma zadania */) {
// komunikat
} elseif (/* oznaczenie się NIE udało */) {
// komunikat
} else {
// 1. wczytaj gracza
// 2. zaktualizuj jego streak
// 3. zapisz postęp: id gracza, nagroda za zadanie, nowy streak
// 4. komunikat z liczbą XP
}
}
ROZWIĄZANIE
try {
$polaczenie = polaczZBaza();
$zadanie = wczytajZadanieUzytkownika($polaczenie, $zadanieId, $uzytkownikId);
if ($zadanie === null) {
ustawKomunikat('Nie znaleziono takiego zadania.');
} elseif (!oznaczZadanieJakoWykonane($polaczenie, $zadanieId, $uzytkownikId)) {
ustawKomunikat('To zadanie jest już wykonane.');
} else {
$uzytkownik = wczytajUzytkownika($polaczenie, $uzytkownikId);
$uzytkownik->aktualizujStreak();
zapiszPostepUzytkownika($polaczenie, $uzytkownikId, $zadanie->pobierzXp(), $uzytkownik->pobierzStreak());
ustawKomunikat('Zadanie wykonane! +' . $zadanie->pobierzXp() . ' XP');
}
}
Dlaczego tak:
- Najpierw
SELECT, potemUPDATE. Potrzebujemy nagrody XP i chcemy odróżnić „nie ma / cudze” od „już wykonane”, żeby pokazać właściwy komunikat. - O przyznaniu XP decyduje wynik
UPDATE, a nie$zadanie->czyWykonane(). To wzór z 3.3: odczyt mógł być sprzed ułamka sekundy, aUPDATEzIS NULLrozstrzyga ostatecznie. - Gracza wczytujemy dopiero, gdy XP się należy. Nie ma sensu pytać bazy, jeśli nic nie zmieniamy.
wczytajUzytkownika()nie zwróci tunull: zadanie należy do tego gracza, a klucz obcy nie pozwala istnieć zadaniu bez właściciela. - Komunikat „+N XP” zawiera liczbę z bazy, a nie tekst od użytkownika — zasada z Lekcji 14 obowiązuje.
- Wszystkie trzy komunikaty są zielone (
.sukces), bo komunikat jednorazowy z Lekcji 14 ma jeden styl. „To zadanie jest już wykonane.” na zielono to kompromis, na który się godzimy.
6e. Awaria bazy
Gdy baza nie działa, strona nie może po cichu przekierować, bo gracz myślałby, że kliknięcie zadziałało. Pokażemy ramkę błędu, tak jak robi to index.php od Lekcji 18.
- Za klamrą zamykającą
trydopiszcatch (Exception $wyjatek) { }. - Otwórz
index.phpi znajdź blok z komentarzem// --- awaria bazy .... Skopiuj jego wnętrze (od$nazwaStrony = 'TaskQuest';doexit;) do środkacatch. - Linię z
echoi ramką błędu zamień tak, żeby ramka miała tekstNie udało się zapisać zmiany:i komunikat wyjątku. Windex.phpkomunikat był w zmiennej$bladBazy. Tu weź go wprost z$wyjatek->getMessage()i doklej kropką. - Pod linią z ramką, przed dołączeniem stopki, dodaj link powrotu:
<p><a href="index.php">← Wróć do listy zadań</a></p>. Gracz trafił na stronę, która zwykle nic nie pokazuje, więc potrzebuje drogi powrotu.
Sprawdzisz to w Teście 8.
ROZWIĄZANIE
} catch (Exception $wyjatek) {
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
echo '<p class="blad">Nie udało się zapisać zmiany: ' . $wyjatek->getMessage() . '</p>';
echo '<p><a href="index.php">← Wróć do listy zadań</a></p>';
require_once __DIR__ . '/includes/footer.php';
exit;
}
header('Location: index.php');
exit;
Przycisku „Zrobione!” jeszcze nie ma, więc pełne działanie strony sprawdzisz po kroku 7.
Krok 7 — index.php: przycisk „Zrobione!” i koniec symulacji
7a. Usuwamy symulację streaka
W bloku wywołań usuń dwie linie:
$zadanieWykonane = $liczbaWykonanych > 0;
$uzytkownik->aktualizujStreak($zadanieWykonane);
Streak jest teraz liczony i zapisywany w oznacz-wykonane.php. Strona pokazuje wartość z bazy ($uzytkownik->pobierzStreak() w prezentacji — bez zmian). Wyszukaj w pliku $zadanieWykonane — nie może już występować.
Uwaga: gdybyś zostawił(a) drugą linię, PHP nie zgłosi błędu. Nadmiarowy argument przy wywołaniu metody jest po cichu pomijany. Metoda przeliczyłaby streak przy każdym wyświetleniu — tak jakby gracz przy każdym odświeżeniu wykonywał zadanie (pytanie kontrolne 10).
7b. SAMODZIELNIE: przycisk przy zadaniu
W index.php znajdź pętlę foreach ($zadaniaDoWyswietlenia as $zadanie) {. Każde zadanie jest w niej wypisywane przez echo jako <div class="$klasa"> ... </div>. Ostatnia linia pętli to echo "</div>";.
Uwaga — nie ruszaj linii z terminem. Pod echo "<p>Termin: " ... jest warunek if (!$zadanie->czyWykonane()) z komunikatem terminu, a pod nim echo "</p>";. Nie wstawiaj przycisku w ten warunek. Po pierwsze, zniknąłby komunikat terminu. Po drugie, formularz znalazłby się wewnątrz akapitu <p>, a w HTML formularz nie może stać w akapicie. Przeglądarka zamknie wtedy <p> przed <form> na własną rękę, a to końcowe </p> zostanie bez pary.
Co zrobić:
1. Nad linią echo "</div>"; (ostatnia w pętli) dopisz warunek, który jest spełniony tylko dla niewykonanych zadań. Jest to ten sam warunek, który stoi przy komunikacie terminu.
2. W środku warunku wypisz przez echo formularz. Dla zadania o id = 3 w źródle strony ma się pojawić dokładnie taki HTML:
<form class="akcja" method="post" action="oznacz-wykonane.php"><input type="hidden" name="id" value="3"> <button type="submit">Zrobione!</button> </form>
Liczbę w value weź z obiektu zadania i doklej kropką. Pozostałe linie to stały tekst.
type="hidden"to pole, którego nie widać na stronie, a które formularz wysyła jak każde inne. Przenosiid, czyli informację, którego zadania dotyczy kliknięcie.- Dlaczego formularz, a nie link
oznacz-wykonane.php?id=3? Link to GET, a GET nie zmienia danych (Lekcja 13). Przeglądarki i różne narzędzia potrafią same „odwiedzać” linki, np. przy wstępnym ładowaniu stron. Każde takie odwiedzenie wykonałoby zadanie.
Sprawdzenie. Odśwież stronę i otwórz źródło (Ctrl+U):
- każde niewykonane zadanie ma swój formularz z innym
value, równym jegoidz phpMyAdmin, - wykonane zadania nie mają formularza,
- komunikaty terminu („Zostało N dni” itd.) nadal są widoczne.
HINT
Tekst z cudzysłowami podwójnymi w środku najwygodniej pisać w apostrofach: echo '<form class="akcja" ...>';. Doklejanie liczby: '... value="' . wartosc . '">'.
ROZWIĄZANIE
if (!$zadanie->czyWykonane()) {
echo '<form class="akcja" method="post" action="oznacz-wykonane.php">';
echo '<input type="hidden" name="id" value="' . $zadanie->pobierzId() . '">';
echo '<button type="submit">Zrobione!</button>';
echo '</form>';
}
echo "</div>";
pobierzId() zwraca int, więc w atrybucie value jest bezpieczny.
7c. Styl
W includes/header.php, w <style> pod regułą .zadanie.po-terminie, dopisz:
form.akcja { display: inline; }
Formularze są domyślnie blokami (każdy od nowej linii). Przycisk przy zadaniu ma stać w linii. Klasa akcja nie zmienia wyglądu formularzy „Dodaj zadanie” i „Logowanie”.
7d. Sprawdzenie
Zaloguj się jako Ola (250 XP, streak 0). Kliknij „Zrobione!” przy „Zaprojektować bazę TaskQuest”. Oczekiwane: komunikat „Zadanie wykonane! +30 XP”, 280 XP, poziom 3, streak 1, zadanie oznaczone [X] i bez przycisku. W phpMyAdmin: data_wykonania i data_ostatniej_aktywnosci Oli to dzisiejsza data.
Krok 8 — diagnostyka.php i stopka
- W
diagnostyka.php, w$wymaganePliki: usuń'includes/funkcje_plikow.php'i'data/zadania.csv', dopisz'oznacz-wykonane.php'(razem 18 ścieżek; w sekcji 6 dojdzie jeszcze jedna). - Usuń linię
$plikiCsv = listaPlikowWKatalogu('data', 'csv');i całą sekcję<h2>Pliki danych</h2>z jej zawartością. Katalogudata/już nie ma. Funkcja nie zgłosiłaby błędu (dla nieistniejącego katalogu zwraca[]), ale sekcja o nieistniejących danych tylko by myliła. includes/footer.php:TaskQuest 1.9.
Diagnostyka nadal nie łączy się z bazą i nie dołącza config.php (zasada z Lekcji 10).
6. Kolejne zadanie — usuwanie zadania
Kama dodała zadanie z literówką w terminie i chce się go pozbyć. Do tej pory mogła tylko poprosić kogoś z dostępem do phpMyAdmin.
Przy każdym zadaniu na liście (także wykonanym) ma się pojawić przycisk „Usuń”.
Wymagania:
- W
funkcje_bazy.php(podzapiszPostepUzytkownika()) funkcjausunZadanie(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool. Usuwa zadanie tylko wtedy, gdy należy do użytkownika. Zwracatrue, gdy wiersz został usunięty. - Nowa strona
usun-zadanie.php, zbudowana jakoznacz-wykonane.php: tylko dla zalogowanych, tylko POST,idsprawdzone regexem, na końcu przekierowanie naindex.php. - Komunikaty:
Zadanie zostało usunięte.alboNie znaleziono takiego zadania.. Awaria bazy — ramka.blad„Nie udało się usunąć zadania: …” z linkiem powrotu. - W
index.php, w pętli listy, przycisk „Usuń” w formularzu z klasąakcja, zbudowany tak jak „Zrobione!” w 7b. Wstaw go pod zamykającą klamrą warunku z formularzem „Zrobione!”, a nadecho "</div>";— poza warunkiem, żeby był widoczny przy każdym zadaniu. usun-zadanie.phpw$wymaganePlikiwdiagnostyka.php(razem 19).
Nie dodajemy okna „Czy na pewno?”. To zadanie końcowe D.
HINT
usun-zadanie.php jest krótszy niż oznacz-wykonane.php. Zastanów się, czy przed DELETE potrzebujesz SELECT. W oznacz-wykonane.php był potrzebny z dwóch powodów (pierwszy punkt „Dlaczego tak” w kroku 6d). Czy któryś z nich dotyczy usuwania? Których plików strona nie musi dołączać, skoro nie tworzy obiektów?
POMOC
DELETE FROM zadania WHERE id = ? AND uzytkownik_id = ?
Wynik funkcji — tak samo jak w oznaczZadanieJakoWykonane(). Na stronie wystarczy if/else na wyniku funkcji.
ROZWIĄZANIE
includes/funkcje_bazy.php:
function usunZadanie(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
$zapytanie = $polaczenie->prepare('DELETE FROM zadania WHERE id = ? AND uzytkownik_id = ?');
$zapytanie->bind_param('ii', $zadanieId, $uzytkownikId);
$zapytanie->execute();
return $zapytanie->affected_rows === 1;
}
usun-zadanie.php:
<?php
session_start();
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
wymagajLogowania();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: index.php');
exit;
}
$zadanieIdTekst = $_POST['id'] ?? '';
if (!preg_match('/^\d+$/', $zadanieIdTekst)) {
header('Location: index.php');
exit;
}
$zadanieId = (int) $zadanieIdTekst;
try {
$polaczenie = polaczZBaza();
if (usunZadanie($polaczenie, $zadanieId, $_SESSION['uzytkownik_id'])) {
ustawKomunikat('Zadanie zostało usunięte.');
} else {
ustawKomunikat('Nie znaleziono takiego zadania.');
}
} catch (Exception $wyjatek) {
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
echo '<p class="blad">Nie udało się usunąć zadania: ' . $wyjatek->getMessage() . '</p>';
echo '<p><a href="index.php">← Wróć do listy zadań</a></p>';
require_once __DIR__ . '/includes/footer.php';
exit;
}
header('Location: index.php');
exit;
Bez SELECT: usuwanie nie potrzebuje XP zadania, a „nie ma” i „cudze” dają ten sam komunikat. affected_rows mówi wszystko. Bez klas: strona nie tworzy obiektów.
index.php — koniec pętli listy:
if (!$zadanie->czyWykonane()) {
echo '<form class="akcja" method="post" action="oznacz-wykonane.php">';
echo '<input type="hidden" name="id" value="' . $zadanie->pobierzId() . '">';
echo '<button type="submit">Zrobione!</button>';
echo '</form>';
}
echo '<form class="akcja" method="post" action="usun-zadanie.php">';
echo '<input type="hidden" name="id" value="' . $zadanie->pobierzId() . '">';
echo '<button type="submit">Usuń</button>';
echo '</form>';
echo "</div>";
Pytanie do przemyślenia: Kama wykonała zadanie za 20 XP, a potem je usunęła. Ile XP ma teraz? Czy tak powinno być? (Pytanie kontrolne 9.)
7. Testy
Gdzie: przeglądarka i phpMyAdmin. Przed każdym testem zapisz przewidywany wynik. Po testach, które zmieniają dane, uruchom ponownie sql/taskquest.sql.
Test 1 — dodawanie zadania
Zaloguj się jako Kama. W phpMyAdmin sprawdź liczbę zadań Kamy: SELECT COUNT(*) FROM zadania WHERE uzytkownik_id = 1;. Dodaj przez formularz zadanie: tytuł Powtórzyć INSERT , opis pusty, termin za tydzień, 10 XP.
| Co sprawdzasz | Oczekiwane |
|---|---|
| strona po wysłaniu | index.php, komunikat „Zadanie zostało dodane.”, zadanie na liście (tytuł wyświetlony bez nadmiarowych spacji), przycisk „Zrobione!” |
COUNT(*) | o 1 więcej |
| nowy wiersz w phpMyAdmin | uzytkownik_id = 1, data_wykonania = NULL, xp = 10, tytuł bez nadmiarowych spacji (czyści go wyczyscTytul() przy odczycie z formularza) |
| odświeżenie (F5) po dodaniu | brak drugiego wiersza, komunikat znika (POST → przekierowanie → GET) |
| Ola po zalogowaniu | nie widzi tego zadania |
Test 2 — walidacja nadal działa (regresja)
Wyślij formularz z: pustym tytułem; XP 0; XP 101; terminem 2026-02-31. Oczekiwane: ramka „Popraw formularz:” z właściwym komunikatem, pola wypełnione (formularz zachowuje wpisane dane), a COUNT(*) bez zmian — do bazy nie trafił żaden wiersz.
Test 3 — „Zrobione!”, XP i streak
Zaloguj się jako Kama. Przygotuj dane w phpMyAdmin (przed każdym wierszem tabeli):
UPDATE zadania SET data_wykonania = NULL WHERE uzytkownik_id = 1;
UPDATE uzytkownicy SET xp = 99, streak = 5, data_ostatniej_aktywnosci = CURDATE() - INTERVAL 1 DAY WHERE id = 1;
CURDATE() - INTERVAL 1 DAY to „dzisiaj minus jeden dzień”, czyli wczoraj. W kolejnych wierszach zmieniaj tylko datę. Kliknij „Zrobione!” przy dowolnym zadaniu za N XP.
data_ostatniej_aktywnosci przed | XP po | streak po | data aktywności po |
|---|---|---|---|
CURDATE() - INTERVAL 1 DAY | 99 + N | 6 | dziś |
CURDATE() | 99 + N | 5 | dziś |
CURDATE() - INTERVAL 3 DAY | 99 + N | 1 | dziś |
NULL | 99 + N | 1 | dziś |
Kliknij „Zrobione!” przy drugim zadaniu (bez przygotowania danych). Streak się nie zmienia (dziś już była aktywność), XP rośnie o nagrodę drugiego zadania.
Jeśli wyniki w pierwszym wierszu się nie zgadzają tuż przed północą albo tuż po niej, sprawdź, czy PHP i MySQL mają tę samą strefę czasową.
aktualizujStreak()liczy „dziś” według PHP, aCURDATE()według MySQL.
Test 4 — XP tylko raz
- Zaloguj się jako Ola. Otwórz
index.phpw dwóch kartach. - W pierwszej kliknij „Zrobione!” przy jej zadaniu — +30 XP, 280.
- W drugiej karcie (przycisk nadal widoczny, bo karta nie była odświeżana) kliknij „Zrobione!” przy tym samym zadaniu.
Oczekiwane: komunikat „To zadanie jest już wykonane.”, nadal 280 XP. Zadanie w bazie ma jedną datę wykonania.
Test 5 — cudze zadanie
- Odczytaj w phpMyAdmin
iddowolnego zadania Kamy. - Zaloguj się jako Ola. Na stronie otwórz narzędzia przeglądarki (F12) → zakładka z kodem strony (Elementy / Inspektor). Znajdź
<input type="hidden" name="id" ...>przy zadaniu Oli i zmieńvaluenaidzadania Kamy. - Kliknij „Zrobione!”.
Oczekiwane: „Nie znaleziono takiego zadania.”, XP Oli bez zmian, zadanie Kamy w bazie nadal z data_wykonania = NULL. Powtórz to samo z przyciskiem „Usuń” — zadanie Kamy nadal istnieje.
Test 6 — nietypowe żądania
| Co robisz | Oczekiwane |
|---|---|
wpisujesz w pasek adresu oznacz-wykonane.php (GET) | powrót na index.php, nic się nie zmienia |
wpisujesz usun-zadanie.php (GET) | j.w. |
w F12 zmieniasz value pola id na abc, -1, 2.5 albo pusty tekst i klikasz | powrót na listę bez komunikatu, nic się nie zmienia |
wylogowujesz się, potem wpisujesz oznacz-wykonane.php | przekierowanie na zaloguj.php (wymagajLogowania()) |
id nieistniejącego zadania (np. 9999) | „Nie znaleziono takiego zadania.” |
Test 7 — usuwanie
Zaloguj się jako Kama. Kliknij „Usuń” przy zadaniu niewykonanym. Oczekiwane: „Zadanie zostało usunięte.”, zadanie zniknęło z listy i z bazy, liczniki zmniejszyły się o 1. Wykonaj inne zadanie i usuń je. XP nie maleje.
Test 8 — baza nie działa
- Zaloguj się, otwórz
dodaj-zadanie.phpi wypełnij formularz. Zatrzymaj w XAMPP MySQL i wyślij. Oczekiwane: formularz z ramką „Nie udało się dodać zadania: Nie udało się połączyć z bazą danych.”, wpisane dane zostają w polach. - Uruchom MySQL, wejdź na listę, zatrzymaj MySQL i kliknij „Zrobione!”. Oczekiwane: strona z ramką „Nie udało się zapisać zmiany: Nie udało się połączyć z bazą danych.” i linkiem powrotu.
- To samo z „Usuń”: „Nie udało się usunąć zadania: …”. Uruchom MySQL.
Test 9 — ranking się zmienia
Po świeżym taskquest.sql: Ola 250, Kama 99. Zaloguj się jako Kama. Wykonuj zadania, aż Kama ma więcej XP niż Ola (w razie potrzeby dodaj przez formularz zadanie za 100 XP i je wykonaj). Oczekiwane: Kama na 1. miejscu rankingu — bez jednej zmiany w pobierzRanking().
Test 10 — diagnostyka
diagnostyka.php → 19 × [OK] (w tym oznacz-wykonane.php i usun-zadanie.php, bez funkcje_plikow.php i data/zadania.csv), brak sekcji „Pliki danych”.
8. Obowiązkowe eksperymenty
Gdzie: eksperyment 1 w phpMyAdmin, 4 w brudnopisie, pozostałe w projekcie. Po każdym przywróć zmieniony kod i uruchom ponownie sql/taskquest.sql.
Eksperyment 1: UPDATE i DELETE bez WHERE
W phpMyAdmin:
UPDATE zadania SET data_wykonania = CURDATE();
Przewidź liczbę zmienionych wierszy. Zaloguj się jako Kama, potem jako Ola. Wszystkie zadania wszystkich graczy są wykonane — i nikt nie dostał za nie XP. Potem:
DELETE FROM zadania;
Obie listy są puste. Użytkownicy zostali, bo usunęliśmy wiersze z zadania, nie z uzytkownicy. (Odwrotnie ON DELETE CASCADE z Lekcji 17 usunąłby także zadania.) Wniosek: w PHP każde UPDATE i DELETE piszemy od razu z WHERE, a gotowe zapytanie czytamy jeszcze raz przed uruchomieniem. Uruchom ponownie taskquest.sql.
Eksperyment 2: bez warunku stanu
W oznaczZadanieJakoWykonane() usuń z zapytania AND data_wykonania IS NULL. Zaloguj się jako Ola i powtórz Test 4 (dwie karty).
Przewidź wynik. Oczekiwane — niespodzianka: druga karta nadal pokazuje „To zadanie jest już wykonane.”. Dlaczego? Drugi UPDATE wpisuje tę samą datę, która już jest, więc według 3.1 nic się nie zmienia i affected_rows = 0. Warunek wygląda na zbędny.
Teraz zasymuluj, że zadanie zostało wykonane wczoraj, a karta z przyciskiem była otwarta od wczoraj:
UPDATE zadania SET data_wykonania = CURDATE() - INTERVAL 1 DAY WHERE uzytkownik_id = 2;
Kliknij „Zrobione!” w drugiej karcie (tej nieodświeżonej). Oczekiwane: „Zadanie wykonane! +30 XP” — Ola dostała XP drugi raz za to samo zadanie. Data zmieniła się z wczorajszej na dzisiejszą, więc wiersz „się zmienił”.
Wniosek: bez warunku stanu przyznanie XP zależy od przypadku — od tego, czy nowa wartość akurat różni się od starej. Z AND data_wykonania IS NULL odpowiedź jest zawsze jednoznaczna: wykonane zadanie nie pasuje do WHERE. Przywróć warunek.
Eksperyment 3: bez warunku właściciela
W oznaczZadanieJakoWykonane() i w wczytajZadanieUzytkownika() usuń AND uzytkownik_id = ?, a w bind_param() zostaw jeden parametr ('i', $zadanieId). Powtórz Test 5.
Oczekiwane: Ola wykonuje zadanie Kamy i Ola dostaje za nie XP. Prepared statement działa bez zarzutu — nie ma tu SQL injection. Zapytanie robi dokładnie to, o co je poproszono. Błąd polega na tym, że nie zapytaliśmy, czy wolno. Przywróć oba warunki.
Eksperyment 4: co liczy affected_rows
W brudnopisie:
$xp = 99;
$uzytkownikId = 1;
$zapytanie = $polaczenie->prepare('UPDATE uzytkownicy SET xp = ? WHERE id = ?');
$zapytanie->bind_param('ii', $xp, $uzytkownikId);
$zapytanie->execute();
var_dump($zapytanie->affected_rows);
$zapytanie->execute();
var_dump($zapytanie->affected_rows);
Przewidź oba wyniki. Po świeżym taskquest.sql Kama ma 99 XP. Oczekiwane: int(0), int(0). Zmień $xp na 100: int(1), int(0). Ten sam przygotowany $zapytanie można wykonać wiele razy — tu drugi raz nic już nie zmienia. Wniosek: affected_rows = 0 nie znaczy „nie ma takiego wiersza”. Znaczy „nic się nie zmieniło”.
Eksperyment 5: połowa zmiany
W zapiszPostepUzytkownika() zrób literówkę w nazwie kolumny: xp = xp + ? → xpp = xp + ?. Zaloguj się jako Ola i kliknij „Zrobione!”.
Oczekiwane: ramka „Nie udało się zapisać zmiany: Unknown column 'xpp’ …”. Wróć na listę. Zadanie jest oznaczone jako wykonane, a XP nadal 250. Kliknąć drugi raz się nie da — przycisku nie ma, a nawet gdyby był, oznaczZadanieJakoWykonane() zwróci false. 30 XP przepadło na zawsze.
Dlaczego? Pierwsze zapytanie (UPDATE zadania) się udało i jego zmiana została w bazie. Drugie (UPDATE uzytkownicy) rzuciło wyjątek. Baza nie wie, że te dwie zmiany „należą do siebie”. Dane są niespójne. Tu literówka jest sztuczna, ale w prawdziwej aplikacji drugie zapytanie może przerwać zanik połączenia, restart serwera czy przekroczenie czasu. Rozwiązanie — transakcja: „wykonaj obie zmiany albo żadną” (zadanie końcowe C). Przywróć xp.
Eksperyment 6: stary streak na stronie
W index.php dopisz tymczasowo w bloku wywołań (przed prezentacją):
$uzytkownik->aktualizujStreak();
Ustaw w phpMyAdmin Kamie streak = 5 i data_ostatniej_aktywnosci = CURDATE() - INTERVAL 1 DAY. Odśwież stronę kilka razy.
Oczekiwane: strona pokazuje 6, choć w bazie jest 5 i Kama nic dziś nie zrobiła. Metoda „udaje” wykonanie zadania przy każdym wyświetleniu (tak działała symulacja do Lekcji 18). Wynik nie trafia do bazy, więc każde odświeżenie znowu pokazuje 6. Usuń dopisaną linię.
9. ROZWIĄZANIE
Poniżej wszystkie pliki, które zmieniły się w Lekcji 19. Pliki niewymienione są identyczne jak po Lekcji 18 (wersja w 18_TaskQuest.md, sekcja 9, a tam, gdzie L18 odsyła dalej — w 15_TaskQuest.md i 17_TaskQuest.md).
ROZWIĄZANIE
Usunięte
includes/funkcje_plikow.php (wczytajZadania(), zapiszZadania(), sprawdzWierszZadania())
data/zadania.csv i katalog data/
config/config.php: define('PLIK_ZADAN', ...);
includes/funkcje_zadan.php: function nastepneIdZadania(...)
includes/funkcje_bazy.php (cały plik)
<?php
// Funkcje TaskQuest korzystające z bazy danych (Lekcje 18–19).
// 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);
}
function dodajZadanie(
mysqli $polaczenie,
int $uzytkownikId,
string $tytul,
string $opis,
string $termin,
int $xp
): int {
$zapytanie = $polaczenie->prepare(
'INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp) VALUES (?, ?, ?, ?, ?)'
);
$zapytanie->bind_param('isssi', $uzytkownikId, $tytul, $opis, $termin, $xp);
$zapytanie->execute();
return $zapytanie->insert_id;
}
function wczytajZadanieUzytkownika(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): ?Zadanie
{
$zapytanie = $polaczenie->prepare(
'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE id = ? AND uzytkownik_id = ?'
);
$zapytanie->bind_param('ii', $zadanieId, $uzytkownikId);
$zapytanie->execute();
$wiersz = $zapytanie->get_result()->fetch_assoc();
if ($wiersz === null) {
return null;
}
return new Zadanie(
id: $wiersz['id'],
tytul: $wiersz['tytul'],
opis: $wiersz['opis'],
termin: $wiersz['termin'],
wykonane: $wiersz['data_wykonania'] !== null,
xp: $wiersz['xp'],
);
}
function oznaczZadanieJakoWykonane(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
$zapytanie = $polaczenie->prepare(
'UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL'
);
$zapytanie->bind_param('ii', $zadanieId, $uzytkownikId);
$zapytanie->execute();
return $zapytanie->affected_rows === 1;
}
function zapiszPostepUzytkownika(mysqli $polaczenie, int $uzytkownikId, int $zdobyteXp, int $streak): void
{
$zapytanie = $polaczenie->prepare(
'UPDATE uzytkownicy SET xp = xp + ?, streak = ?, data_ostatniej_aktywnosci = CURDATE() WHERE id = ?'
);
$zapytanie->bind_param('iii', $zdobyteXp, $streak, $uzytkownikId);
$zapytanie->execute();
}
function usunZadanie(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
$zapytanie = $polaczenie->prepare('DELETE FROM zadania WHERE id = ? AND uzytkownik_id = ?');
$zapytanie->bind_param('ii', $zadanieId, $uzytkownikId);
$zapytanie->execute();
return $zapytanie->affected_rows === 1;
}
classes/Uzytkownik.php — zmiana
Metoda aktualizujStreak(bool $zadanieWykonane) zastąpiona wersją bez parametru (kod w kroku 5b). Reszta klasy bez zmian.
dodaj-zadanie.php — zmiany
- Usunięte
require_onceplikówclasses/Zadanie.phpiincludes/funkcje_plikow.php; podincludes/funkcje_sesji.phpdopisanerequire_once __DIR__ . '/includes/funkcje_bazy.php';. - Zawartość warunku „walidacja bez błędów” zastąpiona blokiem
tryzdodajZadanie()(kod w kroku 2b). - Odczyt pól, walidacja i cała część HTML bez zmian.
oznacz-wykonane.php (nowy plik)
Cały plik (kroki 6a–6e razem):
<?php
session_start();
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';
wymagajLogowania();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: index.php');
exit;
}
$zadanieIdTekst = $_POST['id'] ?? '';
if (!preg_match('/^\d+$/', $zadanieIdTekst)) {
header('Location: index.php');
exit;
}
$zadanieId = (int) $zadanieIdTekst;
$uzytkownikId = $_SESSION['uzytkownik_id'];
try {
$polaczenie = polaczZBaza();
$zadanie = wczytajZadanieUzytkownika($polaczenie, $zadanieId, $uzytkownikId);
if ($zadanie === null) {
ustawKomunikat('Nie znaleziono takiego zadania.');
} elseif (!oznaczZadanieJakoWykonane($polaczenie, $zadanieId, $uzytkownikId)) {
ustawKomunikat('To zadanie jest już wykonane.');
} else {
$uzytkownik = wczytajUzytkownika($polaczenie, $uzytkownikId);
$uzytkownik->aktualizujStreak();
zapiszPostepUzytkownika($polaczenie, $uzytkownikId, $zadanie->pobierzXp(), $uzytkownik->pobierzStreak());
ustawKomunikat('Zadanie wykonane! +' . $zadanie->pobierzXp() . ' XP');
}
} catch (Exception $wyjatek) {
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
echo '<p class="blad">Nie udało się zapisać zmiany: ' . $wyjatek->getMessage() . '</p>';
echo '<p><a href="index.php">← Wróć do listy zadań</a></p>';
require_once __DIR__ . '/includes/footer.php';
exit;
}
header('Location: index.php');
exit;
usun-zadanie.php (nowy plik)
Cały plik — kod w sekcji 6, ROZWIĄZANIE.
index.php — zmiany
- Usunięte linie
$zadanieWykonane = $liczbaWykonanych > 0;i$uzytkownik->aktualizujStreak($zadanieWykonane);. - Pod
<h2>Twoje zadania</h2>przywrócone<p><a href="dodaj-zadanie.php">+ Dodaj zadanie</a></p>. - W pętli listy, nad ostatnią linią
echo "</div>";, dopisane dwa formularze. Linie z terminem i komunikatem terminu bez zmian:
if (!$zadanie->czyWykonane()) {
echo '<form class="akcja" method="post" action="oznacz-wykonane.php">';
echo '<input type="hidden" name="id" value="' . $zadanie->pobierzId() . '">';
echo '<button type="submit">Zrobione!</button>';
echo '</form>';
}
echo '<form class="akcja" method="post" action="usun-zadanie.php">';
echo '<input type="hidden" name="id" value="' . $zadanie->pobierzId() . '">';
echo '<button type="submit">Usuń</button>';
echo '</form>';
Reszta pliku (blok danych z bazy, liczniki, filtr widoku, pasek XP, komunikat, ranking) bez zmian.
includes/header.php
W <style>, pod .zadanie.po-terminie: form.akcja { display: inline; }.
diagnostyka.php
W $wymaganePliki usunięte 'includes/funkcje_plikow.php' i 'data/zadania.csv', dopisane 'oznacz-wykonane.php' i 'usun-zadanie.php' (razem 19 ścieżek). Usunięta zmienna $plikiCsv i sekcja „Pliki danych”. Reszta bez zmian.
includes/footer.php
<p>TaskQuest 1.9</p>
</div>
</body>
</html>
10. Co zmieniliśmy w TaskQuest?
Zostaje
klasa Zadanie (bez zmian), funkcje listy, grywalizacji, terminów (także aktualizujStreak()), tekstowe, sesji
walidujNoweZadanie(), formularz dodawania zachowujący wpisane dane, ramka $bladZapisu, komunikat jednorazowy, POST → przekierowanie → GET
logowanie jak po Lekcji 18 (login bez hasła), w sesji tylko uzytkownik_id
index.php: blok danych z bazy, obsługa awarii bazy, liczniki, filtr widoku + cookie, pasek XP, .po-terminie, ranking
baza taskquest i sql/taskquest.sql (bez zmian)
Uzytkownik::dodajXp(), Zadanie::oznaczJakoWykonane() — nadal nieużywane przez aplikację
Dochodzi
includes/funkcje_bazy.php:
dodajZadanie(mysqli, int, string, string, string, int): int — INSERT, zwraca insert_id
wczytajZadanieUzytkownika(mysqli, int $zadanieId, int $uzytkownikId): ?Zadanie
oznaczZadanieJakoWykonane(mysqli, int, int): bool — UPDATE z IS NULL
zapiszPostepUzytkownika(mysqli, int $uzytkownikId, int $zdobyteXp, int $streak): void
usunZadanie(mysqli, int, int): bool — DELETE
oznacz-wykonane.php, usun-zadanie.php — tylko POST, bez HTML
index.php: przyciski „Zrobione!” i „Usuń” (form.akcja, pole ukryte id), link „+ Dodaj zadanie”
header.php: form.akcja
Nowe narzędzia: SQL INSERT INTO ... (...) VALUES (...), UPDATE ... SET ... WHERE, SET kolumna = kolumna + ?, DELETE FROM ... WHERE, CURDATE(), CURDATE() - INTERVAL n DAY; affected_rows, insert_id; <input type="hidden">; warunek właściciela i warunek stanu w WHERE.
Zmieniamy
dodaj-zadanie.php: zapis do bazy zamiast CSV,Uzytkownik::aktualizujStreak(): bez parametru, wywoływana tylko po wykonaniu zadania;null→ seria 1,index.php: streak pokazywany z bazy (bez przeliczania), link do formularza wraca, przyciski przy zadaniach,diagnostyka.php: 19 plików, bez sekcji „Pliki danych”; stopka1.9.
Usuwamy
| Co | Dlaczego |
|---|---|
$zadanieWykonane i wywołanie aktualizujStreak() w index.php | była to symulacja aktywności; teraz aktywność to kliknięcie „Zrobione!”, a streak jest zapisany w bazie |
parametr bool $zadanieWykonane metody aktualizujStreak() | metoda jest wywoływana tylko po wykonaniu zadania — parametr zawsze byłby true |
data/zadania.csv, funkcje_plikow.php, PLIK_ZADAN | nic nie czyta ani nie zapisuje pliku; dane są w bazie od Lekcji 17 |
nastepneIdZadania() | id nadaje AUTO_INCREMENT |
classes/Zadanie.php w dodaj-zadanie.php | strona nie tworzy obiektu — dane idą wprost do INSERT |
sekcja „Pliki danych” w diagnostyka.php | nie ma katalogu data/ |
Co nowego potrafi TaskQuest po tej lekcji?
Po Lekcji 19 TaskQuest zapisuje zmiany w bazie. Gracz dodaje zadania przez formularz, oznacza je jako wykonane i dostaje za nie XP, a jego seria dni jest liczona w chwili wykonania zadania i zapisywana. Może też usuwać zadania. Aplikacja nie pozwala zmienić ani usunąć cudzego zadania i nie przyznaje XP dwa razy za to samo zadanie. Ranking zmienia się sam, gdy gracze zdobywają XP. Plik CSV zniknął — jedynym miejscem danych jest baza.
11. Zadania końcowe — rozbuduj TaskQuest samodzielnie
Nie są częścią wersji referencyjnej.
A. Edycja zadania. Nowa strona edytuj-zadanie.php?id=N. GET: id z adresu sprawdzone regexem, zadanie wczytane przez wczytajZadanieUzytkownika() (cudze albo nieistniejące → komunikat i powrót na listę), formularz jak w dodaj-zadanie.php wypełniony danymi zadania. POST: te same pola + ukryte id, walidacja przez walidujNoweZadanie(), zapis funkcją edytujZadanie(mysqli $polaczenie, int $zadanieId, int $uzytkownikId, string $tytul, string $opis, string $termin, int $xp): bool (UPDATE ... SET tytul = ?, opis = ?, termin = ?, xp = ? WHERE id = ? AND uzytkownik_id = ?). Przy zadaniu link „Edytuj” (GET — tylko otwiera formularz, niczego nie zmienia). Pytania: co zwróci funkcja, gdy użytkownik zapisze formularz bez żadnej zmiany (Eksperyment 4)? Czy wtedy pokażesz „Nie znaleziono takiego zadania.”? Czy wykonane zadanie można edytować i zmienić mu XP — co to znaczy dla XP gracza?
B. Przerwana seria. Kama miała ostatnią aktywność tydzień temu, a strona nadal pokazuje „streak 5”. Seria jest przerwana, choć baza tego jeszcze nie wie (dowie się przy następnym „Zrobione!”). Dodaj do Uzytkownik metodę pobierzAktualnyStreak(): int: gdy data ostatniej aktywności to dziś albo wczoraj — streak z bazy, w przeciwnym razie (także null) — 0. Użyj jej w prezentacji zamiast pobierzStreak(). Nie zapisuj niczego do bazy. Przetestuj daty: dziś, wczoraj, przedwczoraj, NULL. Dlaczego aktualizujStreak() nie może używać tej metody?
C. Transakcja. Napraw problem z Eksperymentu 5. W oznacz-wykonane.php, zaraz po polaczZBaza(), wywołaj $polaczenie->begin_transaction();. Po zapiszPostepUzytkownika() — $polaczenie->commit();. Do czasu commit() zmiany nie są ostateczne. W catch, jeśli połączenie istnieje (isset($polaczenie)), wywołaj $polaczenie->rollback(); — baza cofnie wszystko od begin_transaction(). Powtórz Eksperyment 5: po błędzie zadanie ma być niewykonane, XP bez zmian. Pytania: dlaczego transakcja działa w naszych tabelach (wskazówka: ENGINE=InnoDB)? Po co warunek isset($polaczenie)? Czy transakcja zastępuje warunek data_wykonania IS NULL?
D. Potwierdzenie usuwania. Przycisk „Usuń” prowadzi (GET, usun-zadanie.php?id=N) do strony z pytaniem „Czy na pewno usunąć zadanie „…”?” i dwoma przyciskami: „Tak, usuń” (formularz POST z ukrytym id) i „Anuluj” (link na listę). Dopiero POST usuwa. Na stronie potwierdzenia wyświetl tytuł zadania — skąd go weźmiesz i dlaczego musisz przy tym sprawdzić właściciela?
Sprzątanie
- Usuń
brudnopis.php. - Sprawdź, że przywrócone są:
AND data_wykonania IS NULLiAND uzytkownik_id = ?w zapytaniach (Eksperymenty 2–3),xpwzapiszPostepUzytkownika()(Eksperyment 5), a windex.phpnie ma wywołaniaaktualizujStreak()(Eksperyment 6). - Uruchom ponownie
sql/taskquest.sql. - Zaloguj się jako Ola, kliknij „Zrobione!” — 280 XP, streak 1. Zaloguj się jako Kama, dodaj i usuń zadanie.
Do samodzielnego sprawdzenia
[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Potrafię napisać INSERT z listą kolumn i wiem, które kolumny mogę pominąć.
[ ] Potrafię napisać UPDATE i DELETE z WHERE — i wiem, co się stanie bez WHERE.
[ ] Potrafię zwiększyć wartość w bazie względem obecnej (xp = xp + ?) i wiem, czemu to lepsze niż zapis gotowej liczby.
[ ] Wiem, co liczy affected_rows przy UPDATE i dlaczego 0 nie znaczy „nie ma wiersza”.
[ ] Wiem, do czego służy insert_id.
[ ] Potrafię wyjaśnić, dlaczego każde UPDATE/DELETE zadania ma warunek uzytkownik_id = ?.
[ ] Potrafię wyjaśnić, dlaczego XP przyznajemy na podstawie wyniku UPDATE z IS NULL, a nie wcześniejszego odczytu.
[ ] Wiem, dlaczego „Zrobione!” i „Usuń” to formularze POST, a nie linki.
[ ] Wiem, gdzie i kiedy liczony jest teraz streak.
[ ] Potrafię opisać, co się dzieje z danymi, gdy z dwóch zapytań uda się tylko pierwsze.
[ ] W projekcie nie ma już CSV ani funkcji plikowych.
[ ] Usunęłam/usunąłem brudnopis.
Pytania kontrolne
1. Tabela zadania po świeżym taskquest.sql. Podaj affected_rows każdego polecenia, wykonanych po kolei:
UPDATE zadania SET data_wykonania = CURDATE() WHERE uzytkownik_id = 2;
UPDATE zadania SET data_wykonania = CURDATE() WHERE uzytkownik_id = 2;
UPDATE zadania SET data_wykonania = CURDATE() WHERE uzytkownik_id = 2 AND data_wykonania IS NULL;
DELETE FROM zadania WHERE uzytkownik_id = 3;
2. Kolega uważa, że warunek AND data_wykonania IS NULL jest zbędny, bo oznacz-wykonane.php i tak najpierw wczytuje zadanie i mógłby sprawdzić $zadanie->czyWykonane(). Podaj dwie sytuacje, w których jego wersja przyzna XP dwa razy (Test 4, Eksperyment 2).
3. Koleżanka napisała UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ?. Zapytanie działa, a prepared statement chroni przed SQL injection. Co może zrobić zalogowana Ola i jak? Czym ten błąd różni się od SQL injection?Znajdź trzy błędy:
$zapytanie = $polaczenie->prepare('DELETE FROM zadania WHERE id = ? AND uzytkownik_id = ?');
$zapytanie->bind_param('i', $zadanieId, (int) $_SESSION['uzytkownik_id']);
$zapytanie->execute();
$wynik = $zapytanie->get_result();
4. Dlaczego „Zrobione!” jest przyciskiem w formularzu POST, a nie linkiem oznacz-wykonane.php?id=3? Podaj dwa konkretne problemy wersji z linkiem.
5. Kama ma streak 4. Co pokaże strona po kliknięciu „Zrobione!”, jeśli jej data_ostatniej_aktywnosci to: (a) dziś, (b) wczoraj, (c) 10 dni temu, (d) NULL? Która linia kodu decyduje o przypadku (d)?
6. zapiszPostepUzytkownika() dodaje XP przez xp = xp + ?, a streak zapisuje jako gotową liczbę (streak = ?). Dlaczego te dwie kolumny potraktowaliśmy różnie? Czy przy streaku może wystąpić ten sam problem co przy XP?
7. W Eksperymencie 5 zadanie zostało oznaczone jako wykonane, a XP nie wpadło. Opisz stan bazy po błędzie. Czy odwrócenie kolejności zapytań (najpierw XP, potem zadanie) rozwiązuje problem? Co go rozwiązuje?
8. Kama wykonała zadanie za 20 XP, a potem je usunęła. XP zostało. Podaj argument za takim działaniem i argument przeciw. Jak zmieniłbyś/zmieniłabyś usun-zadanie.php, gdyby XP miało być odbierane?
9. Po zmianie metody na aktualizujStreak(): void w index.php została przez pomyłkę linia $uzytkownik->aktualizujStreak($zadanieWykonane); (a $zadanieWykonane nadal istnieje). Czy PHP zgłosi błąd? Co zobaczy Kama, która wczoraj wykonała zadanie, a dziś tylko otwiera stronę (Eksperyment 6)?
10. Baza odrzuci tytuł dłuższy niż 100 znaków (VARCHAR(100)). Po co więc walidujNoweZadanie() nadal to sprawdza? Podaj dwa powody.
PODSUMOWANIE LEKCJI
W skrócie
INSERT,UPDATEiDELETEnie zwracają wierszy. Znika więc ostatni krok znany zSELECT— poexecute()nie maget_result(). Efekt sprawdzamy przezaffected_rows(PDO:rowCount()).affected_rowsprzyUPDATEto liczba wierszy, które naprawdę się zmieniły, a nie tych, które pasowały doWHERE. Zapis tej samej wartości daje 0. Dlatego0nie znaczy „nie ma takiego wiersza”.UPDATEiDELETEbezWHEREdziałają na całej tabeli. W PHP nie ma pytania „czy na pewno?” i nie ma cofnięcia.WHEREpełni rolę bezpiecznika i ma zwykle dwa rodzaje warunków:- właściciela (
AND uzytkownik_id = ?) — żeby nie dało się zmienić cudzego rekordu, - stanu (
AND data_wykonania IS NULL) — żeby ta sama operacja nie zadziałała drugi raz.
- właściciela (
- Prepared statement chroni przed SQL injection, ale nie przed brakiem sprawdzenia uprawnień. To dwa różne błędy: pierwszy dotyczy sklejania SQL, drugi — zaufania danym z formularza.
- O tym, czy nagroda (XP) się należy, decyduje wynik
UPDATEz warunkiem stanu, a nie wcześniejszy odczyt przezSELECT. Odczyt może być nieaktualny (dwie karty, dwa kliknięcia). xp = xp + ?zmienia wartość względem obecnej — baza dolicza do tego, co ma w chwili zapisu. Odczyt XP do PHP, dodanie i zapisanie gotowej liczby nadpisuje zmianę zrobioną w międzyczasie.insert_id(PDO:lastInsertId()) dajeidnadane przezAUTO_INCREMENT. Nie trzeba go zgadywać ani wyliczać.bind_param()przyjmuje tylko zmienne.bind_param('i', 5)albobind_param('i', (int) $_POST['id'])kończy się błędem („cannot be passed by reference”).- Dwa zapytania to nie jedna operacja. Jeśli pierwsze się uda, a drugie nie, pierwsza zmiana zostaje w bazie (zadanie wykonane, XP nieprzyznane). Rozwiązaniem jest transakcja — zadanie końcowe C.
- Każda strona zmieniająca dane działa w schemacie POST → przekierowanie → GET: sprawdź metodę żądania, zapisz zmianę,
header('Location: ...')iexit;. Bez tego F5 powtarza operację.
Minimum, które powinno wejść w pamięć
Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:
INSERT INTO tabela (kolumny) VALUES (wartości)— kolumny i wartości w tej samej kolejności;idzAUTO_INCREMENTi kolumny z wartością domyślną można pominąć.UPDATEiDELETEzawsze zWHERE. BezWHERE= cała tabela, nieodwracalnie.- Po
INSERT/UPDATE/DELETEnie maget_result(). Efekt toaffected_rows(PDO:rowCount()), noweidtoinsert_id(PDO:lastInsertId()). affected_rows == 0znaczy „nic się nie zmieniło”, a nie „nie ma takiego wiersza”.- Każde zapytanie, które mogłoby trafić w cudzy rekord, ma
AND uzytkownik_id = ?. Prepared statement tego nie załatwia. - Warunek stanu (
AND data_wykonania IS NULL) umieszczamy w tym samymUPDATE, który zmienia dane — nie w osobnymifw PHP. xp = xp + ?zamiast odczytu i zapisu gotowej liczby.bind_param()przyjmuje zmienne, nie stałe i nie wyrażenia; liczba liter typów = liczba?(i,s,d).- Dane zmieniamy tylko przez POST, a po zapisie:
header('Location: ...'); exit;. IS NULL, nie= NULL.
Ściągawka – najważniejsze polecenia i składnia
SQL
INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp)
VALUES (2, 'Tytuł', '', '2026-10-10', 10);
UPDATE zadania SET data_wykonania = CURDATE()
WHERE id = 5 AND uzytkownik_id = 2 AND data_wykonania IS NULL;
UPDATE uzytkownicy SET xp = xp + 10 WHERE id = 2; -- względem obecnej wartości
DELETE FROM zadania WHERE id = 5 AND uzytkownik_id = 2;
CURDATE() → dzisiejsza data (RRRR-MM-DD), NOW() → data z godziną.
MySQLi obiektowo (TaskQuest)
$zapytanie = $polaczenie->prepare('INSERT INTO zadania (uzytkownik_id, tytul) VALUES (?, ?)');
$zapytanie->bind_param('is', $uzytkownikId, $tytul);
$zapytanie->execute();
$noweId = $zapytanie->insert_id; // id nadane przez AUTO_INCREMENT
$ile = $zapytanie->affected_rows; // ile wierszy się zmieniło
Typy w bind_param(): i — liczba całkowita, s — tekst (także data), d — liczba z przecinkiem.
Schemat funkcji zmieniającej dane
function oznaczZadanieJakoWykonane(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
$zapytanie = $polaczenie->prepare(
'UPDATE zadania SET data_wykonania = CURDATE()
WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL'
);
$zapytanie->bind_param('ii', $zadanieId, $uzytkownikId);
$zapytanie->execute();
return $zapytanie->affected_rows === 1;
}
PDO
$stmt = $pdo->prepare('UPDATE players SET email = ? WHERE id = ?');
$stmt->execute([$email, $id]);
$ile = $stmt->rowCount();
$noweId = $pdo->lastInsertId(); // na POŁĄCZENIU, nie na zapytaniu
Strona zmieniająca dane (POST → przekierowanie → GET)
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: index.php');
exit;
}
// walidacja → zapis → komunikat w sesji
header('Location: index.php');
exit; // ZAWSZE exit po header()
Wersja proceduralna — tylko na egzaminie
Arkusz INF.03 udostępnia wyłącznie te funkcje, więc zapis wygląda tak:
$polaczenie = mysqli_connect('localhost', 'root', '', 'wodospady');
if (isset($_POST['idObiekt'])) {
$idObiekt = $_POST['idObiekt'];
$idTurysta = $_POST['idTurysta'];
mysqli_query($polaczenie, "INSERT INTO osiagniecia (idObiekt, idTurysta)
VALUES ('$idObiekt', '$idTurysta')");
}
mysqli_close($polaczenie);
Poza egzaminem tak nie piszemy — to sklejanie SQL z danymi użytkownika, czyli podatność na SQL injection z Lekcji 18.
Quiz – sprawdź się
Uzupełnij polecenie i odpowiedz, co stanie się z pominiętymi kolumnami
idorazdata_wykonania:INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp)
______ (2, 'Powtórka SQL', '', '2026-11-05', 15);Ola ma 240 XP. Podaj
affected_rowspo pierwszym i po drugim wykonaniu tego polecenia:UPDATE uzytkownicy SET xp = 250 WHERE id = 2;Znajdź dwa błędy:
$zapytanie = $polaczenie->prepare('UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ?');
$zapytanie->bind_param('i', $zadanieId);
$zapytanie->execute();
$wynik = $zapytanie->get_result();Dlaczego
UPDATE uzytkownicy SET xp = xp + ? WHERE id = ?jest lepsze niż odczytanie XPSELECT-em, dodanie w PHP i zapisanie gotowej liczby przezSET xp = ??Co się stanie po uruchomieniu tej linii i jak ją poprawić?
$zapytanie->bind_param('ii', $zadanieId, (int) $_SESSION['uzytkownik_id']);Kolega usunął z zapytania warunekAND uzytkownik_id = ?:DELETE FROM zadania WHERE id = ?Twierdzi, że jest bezpiecznie, bo to prepared statement. Co może zrobić zalogowana Ola i dlaczego prepared statement tu nie pomaga?Uzupełnij warunek tak, żeby gracz nie dostał XP dwa razy za to samo zadanie:
UPDATE zadania SET data_wykonania = CURDATE()
WHERE id = ? AND uzytkownik_id = ? ______Skrypt zapisuje dane i kończy się linią
header('Location: index.php');. Czego brakuje i jakie dwa problemy może to spowodować?Po
INSERTchcemy przekierować użytkownika naprofil.php?id=...nowo dodanego gracza. Skąd wziąć toidw MySQLi, a skąd w PDO?Co zrobi to polecenie uruchomione w phpMyAdmin i czy da się je cofnąć?
DELETE FROM zadania;Przycisk „Zrobione!” można było zrobić jako zwykły link
oznacz-wykonane.php?id=3. Podaj dwa konkretne problemy takiej wersji.Na egzaminie masz napisać skrypt, który po wysłaniu formularza wstawia do tabeli
osiagnieciawartościidObiektiidTurystapobrane z formularza. Wypisz kolejne kroki skryptu (od połączenia do zamknięcia) i wskaż, który z nich decyduje o tym, że skrypt nie zapisuje niczego przy zwykłym wejściu na stronę.Zadanie praktyczne. Napisz funkcję
zmienEmailGracza(mysqli $polaczenie, int $graczId, string $email): bool, która zmienia e-mail gracza w tabeliplayersi zwracatruetylko wtedy, gdy wiersz faktycznie się zmienił. Wyjaśnij, kiedy zwrócifalsemimo poprawnegoid.
Odpowiedzi
VALUES.idnada baza (AUTO_INCREMENT), adata_wykonaniadostanieNULL— nowe zadanie jest od razu niewykonane.Pierwsze wykonanie:
1(240 → 250). Drugie:0— wiersz pasuje doWHERE, ale wartość jest już taka sama, więc nic się nie zmieniło.(a)
get_result()poUPDATE— nie ma wyniku do odebrania, efekt sprawdza się przezaffected_rows; (b) brak warunku właścicielaAND uzytkownik_id = ?, więc każdy zalogowany może oznaczyć cudze zadanie, podmieniającidw formularzu.Baza dolicza XP do wartości aktualnej w chwili zapisu. Przy odczycie i zapisie gotowej liczby zmiana zrobiona w międzyczasie (druga karta, drugie kliknięcie, inny skrypt) zostaje nadpisana i część XP przepada.
Error—bind_param()przekazuje argumenty przez referencję, więc nie przyjmuje wyrażenia. Wartość trzeba najpierw zapisać w zmiennej:$uzytkownikId = (int) $_SESSION['uzytkownik_id'];, a potembind_param('ii', $zadanieId, $uzytkownikId).Ola może w narzędziach przeglądarki (F12) zmienić wartość ukrytego pola
idna numer zadania Kamy i je usunąć. Prepared statement pilnuje tylko tego, żeby dane nie stały się kodem SQL — nie sprawdza, czy użytkownik ma prawo do tego wiersza. To brak kontroli uprawnień, nie SQL injection.AND data_wykonania IS NULL.Brakuje
exit;. Skrypt wykonuje się dalej: może zacząć wypisywać HTML (a przy wcześniejszym wyjściu nagłówekLocationw ogóle nie zadziała) i może wykonać kolejne operacje na bazie, mimo że przeglądarka jest już przekierowywana.MySQLi:
$zapytanie->insert_id(proceduralniemysqli_insert_id($polaczenie)), odczytane zaraz poexecute(), przed zamknięciem połączenia. PDO:$pdo->lastInsertId()— na obiekcie połączenia, nie na zapytaniu.Usunie wszystkie wiersze tabeli
zadania, bo nie maWHERE. Nie da się tego cofnąć — dane wracają tylko z kopii zapasowej albo przez ponowne uruchomieniesql/taskquest.sql.Przykładowe: (a) odświeżenie strony (F5) powtarza żądanie GET, więc powtarza operację; (b) adres z
idmożna wpisać ręcznie albo podstawić cudzy numer; (c) link da się kliknąć przypadkiem lub otworzyć z historii, a operacja zmieniająca dane nie powinna się wykonywać przy zwykłym wejściu na adres. Zmiana danych należy do POST.Kroki: połączenie (
mysqli_connect) → sprawdzenie, czy formularz został wysłany (isset($_POST[...])) → pobranie wartości z$_POSTdo zmiennych → zbudowanie i wykonanieINSERTprzezmysqli_query()→ zamknięcie połączenia (mysqli_close). O tym, że zwykłe wejście na stronę niczego nie zapisuje, decyduje warunek zisset().Przykładowe rozwiązanie:
function zmienEmailGracza(mysqli $polaczenie, int $graczId, string $email): boolZwróci
{
$zapytanie = $polaczenie->prepare('UPDATE players SET email = ? WHERE id = ?');
$zapytanie->bind_param('si', $email, $graczId);
$zapytanie->execute();
return $zapytanie->affected_rows === 1;
}falsetakże wtedy, gdy gracz istnieje, ale ma już dokładnie ten sam e-mail —UPDATEnie zmienił żadnej wartości, więcaffected_rowswynosi 0. Dlatego tego wyniku nie wolno czytać jako „nie ma takiego gracza”.