Pełny CRUD — dodaj, czytaj, zmień, usuń
🎯 Po tej lekcji będziesz wykonywać pełny CRUD na tabeli MySQL przez prepared statements PDO i odczytywać lastInsertId() oraz rowCount().
Masz już SELECT przez query() i prepare(). Czas opanować pozostałe trzy operacje tworzące akronim CRUD — Create, Read, Update, Delete. Wszystkie wykonujemy wyłącznie przez prepared statements.
CREATE — INSERT z lastInsertId()
Dodawanie nowego zadania:
<?php
declare(strict_types=1);
require 'db.php';
$tytul = trim($_POST['tytul'] ?? '');
$opis = trim($_POST['opis'] ?? '');
if ($tytul === '') {
exit('Tytuł jest wymagany.');
}
$stmt = $pdo->prepare(
'INSERT INTO zadania (tytul, opis) VALUES (:tytul, :opis)'
);
$stmt->execute([
':tytul' => $tytul,
':opis' => $opis,
]);
$noweId = (int) $pdo->lastInsertId();
echo 'Dodano zadanie o id: ' . $noweId;
lastInsertId() to metoda na obiekcie PDO (nie PDOStatement) — wywołujesz ją bezpośrednio po INSERT. Zwraca wartość AUTO_INCREMENT nadaną nowemu wierszowi jako string — rzuć na (int) gdy potrzebujesz liczby.
READ — SELECT z prepared statement
Gdy filtrujesz po danych od użytkownika (np. id z URL), zawsze używaj prepare():
<?php
declare(strict_types=1);
require 'db.php';
$id = (int) ($_GET['id'] ?? 0);
if ($id <= 0) {
exit('Nieprawidłowe id.');
}
$stmt = $pdo->prepare('SELECT id, tytul, opis, ukonczone FROM zadania WHERE id = :id');
$stmt->execute([':id' => $id]);
$zadanie = $stmt->fetch();
if ($zadanie === false) {
http_response_code(404);
exit('Nie znaleziono zadania.');
}
echo htmlspecialchars($zadanie['tytul'], ENT_QUOTES, 'UTF-8');
echo htmlspecialchars($zadanie['opis'], ENT_QUOTES, 'UTF-8');
Lista z filtrem — fetchAll()
<?php
declare(strict_types=1);
require 'db.php';
// Filtruj po statusie przekazanym z formularza
$ukonczone = (int) ($_GET['ukonczone'] ?? 0);
$stmt = $pdo->prepare(
'SELECT id, tytul, created_at FROM zadania WHERE ukonczone = :ukonczone ORDER BY id DESC'
);
$stmt->execute([':ukonczone' => $ukonczone]);
$zadania = $stmt->fetchAll();
UPDATE — zmiana danych z rowCount()
Aktualizacja polega na prepare() z klauzulą SET i WHERE. Po execute() sprawdzasz rowCount(), żeby wiedzieć, ile wierszy zostało faktycznie zmienionych:
<?php
declare(strict_types=1);
require 'db.php';
$id = (int) ($_POST['id'] ?? 0);
$tytul = trim($_POST['tytul'] ?? '');
if ($id <= 0 || $tytul === '') {
exit('Nieprawidłowe dane.');
}
$stmt = $pdo->prepare(
'UPDATE zadania SET tytul = :tytul WHERE id = :id'
);
$stmt->execute([':tytul' => $tytul, ':id' => $id]);
$zmieniono = $stmt->rowCount();
if ($zmieniono === 0) {
echo 'Nic nie zmieniono (być może zadanie nie istnieje).';
} else {
echo 'Zaktualizowano zadanie.';
}
Oznaczanie zadania jako ukończonego
<?php
declare(strict_types=1);
require 'db.php';
$id = (int) ($_POST['id'] ?? 0);
$stmt = $pdo->prepare('UPDATE zadania SET ukonczone = 1 WHERE id = :id');
$stmt->execute([':id' => $id]);
echo $stmt->rowCount() > 0
? 'Zadanie oznaczone jako ukończone.'
: 'Zadanie nie istnieje.';
UPDATE zadania SET ukonczone = 1 (bez WHERE) oznaczy wszystkie zadania naraz. Zawsze sprawdzaj, czy masz klauzulę WHERE gdy chcesz zmodyfikować konkretny rekord. To samo dotyczy DELETE.DELETE — usuwanie wierszy
<?php
declare(strict_types=1);
require 'db.php';
$id = (int) ($_POST['id'] ?? 0);
if ($id <= 0) {
exit('Nieprawidłowe id.');
}
$stmt = $pdo->prepare('DELETE FROM zadania WHERE id = :id');
$stmt->execute([':id' => $id]);
if ($stmt->rowCount() > 0) {
echo 'Zadanie usunięte.';
} else {
echo 'Nie znaleziono zadania o podanym id.';
}
Pełny przykład — aplikacja TODO
Oto miniaturowa aplikacja do zarządzania zadaniami, łącząca wszystkie cztery operacje:
<?php
declare(strict_types=1);
require 'db.php';
// ── Obsługa formularzy (POST) ──────────────────────────────────────────────
// Dodaj zadanie
if (($_POST['akcja'] ?? '') === 'dodaj') {
$tytul = trim($_POST['tytul'] ?? '');
if ($tytul !== '') {
$stmt = $pdo->prepare('INSERT INTO zadania (tytul) VALUES (:tytul)');
$stmt->execute([':tytul' => $tytul]);
}
}
// Oznacz jako ukończone
if (($_POST['akcja'] ?? '') === 'ukoncz') {
$id = (int) ($_POST['id'] ?? 0);
if ($id > 0) {
$stmt = $pdo->prepare('UPDATE zadania SET ukonczone = 1 WHERE id = :id');
$stmt->execute([':id' => $id]);
}
}
// Usuń zadanie
if (($_POST['akcja'] ?? '') === 'usun') {
$id = (int) ($_POST['id'] ?? 0);
if ($id > 0) {
$stmt = $pdo->prepare('DELETE FROM zadania WHERE id = :id');
$stmt->execute([':id' => $id]);
}
}
// ── Pobierz listę (READ) ───────────────────────────────────────────────────
$stmt = $pdo->query('SELECT id, tytul, ukonczone FROM zadania ORDER BY id DESC');
$zadania = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="pl">
<head><meta charset="UTF-8"><title>Moje zadania</title></head>
<body>
<h1>Moje zadania</h1>
<form method="post">
<input type="hidden" name="akcja" value="dodaj">
<input type="text" name="tytul" placeholder="Nowe zadanie" required>
<button type="submit">Dodaj</button>
</form>
<ul>
<?php foreach ($zadania as $z): ?>
<li style="<?= $z['ukonczone'] ? 'text-decoration:line-through' : '' ?>">
<?= htmlspecialchars($z['tytul'], ENT_QUOTES, 'UTF-8') ?>
<?php if (!$z['ukonczone']): ?>
<form method="post" style="display:inline">
<input type="hidden" name="akcja" value="ukoncz">
<input type="hidden" name="id" value="<?= (int) $z['id'] ?>">
<button type="submit">✓</button>
</form>
<?php endif; ?>
<form method="post" style="display:inline">
<input type="hidden" name="akcja" value="usun">
<input type="hidden" name="id" value="<?= (int) $z['id'] ?>">
<button type="submit">✗</button>
</form>
</li>
<?php endforeach; ?>
</ul>
</body>
</html>
W następnej lekcji zamkniesz całą logikę bazy danych w klasę repozytorium — łącząc PDO z programowaniem obiektowym z Modułu 6.
Sprawdź się
Co zwraca lastInsertId() po udanym INSERT?
lastInsertId() zwraca wartość klucza PRIMARY KEY nadaną przez AUTO_INCREMENT dla ostatnio wstawionego wiersza. Zwraca ją jako string — często rzutuj na (int) gdy potrzebujesz liczby.
Co zwraca rowCount() po wykonaniu UPDATE lub DELETE?
rowCount() zwraca liczbę wierszy dotkniętych ostatnim zapytaniem modyfikującym (INSERT/UPDATE/DELETE). Dla UPDATE — jeśli nowa wartość jest identyczna ze starą, wiersz może nie być liczony jako zmieniony (zależy od konfiguracji PDO i sterownika).
Dlaczego przy operacji DELETE przez prepared statement NIE wolno wstawiać id bezpośrednio do SQL?
Nawet gdy id wygląda jak liczba, nie wolno sklejać go z SQL. Użytkownik może przekazać '1 OR 1=1' i usunąć wszystkie wiersze. Prepared statement gwarantuje, że wartość jest traktowana jako dane, nie kod SQL.
Ćwiczenie
Napisz cztery funkcje CRUD dla tabeli zadania korzystające z PDO: (1) dodajZadanie(PDO $pdo, string $tytul, string $opis): int — zwraca id nowego zadania; (2) pobierzZadanie(PDO $pdo, int $id): ?array — zwraca wiersz lub null; (3) oznaczUkonczone(PDO $pdo, int $id): bool — ustawia ukonczone=1, zwraca true jeśli wiersz istniał; (4) usunZadanie(PDO $pdo, int $id): bool — usuwa, zwraca true jeśli coś usunięto.
Pokaż rozwiązanie
<?php
declare(strict_types=1);
function dodajZadanie(PDO $pdo, string $tytul, string $opis): int
{
$stmt = $pdo->prepare(
'INSERT INTO zadania (tytul, opis) VALUES (:tytul, :opis)'
);
$stmt->execute([':tytul' => $tytul, ':opis' => $opis]);
return (int) $pdo->lastInsertId();
}
function pobierzZadanie(PDO $pdo, int $id): ?array
{
$stmt = $pdo->prepare('SELECT * FROM zadania WHERE id = :id');
$stmt->execute([':id' => $id]);
$wiersz = $stmt->fetch();
return $wiersz === false ? null : $wiersz;
}
function oznaczUkonczone(PDO $pdo, int $id): bool
{
$stmt = $pdo->prepare('UPDATE zadania SET ukonczone = 1 WHERE id = :id');
$stmt->execute([':id' => $id]);
return $stmt->rowCount() > 0;
}
function usunZadanie(PDO $pdo, int $id): bool
{
$stmt = $pdo->prepare('DELETE FROM zadania WHERE id = :id');
$stmt->execute([':id' => $id]);
return $stmt->rowCount() > 0;
}