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.

💡 lastInsertId() tylko zaraz po INSERT: Wywołaj ją bezpośrednio po execute() dla INSERT. Kolejne zapytania (nawet SELECT) mogą zmienić wewnętrzny stan. W środowisku wielowątkowym (wiele żądań równocześnie) PDO zwraca id z aktualnego połączenia — nie będziesz miał kolizji z innymi użytkownikami.

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 bez WHERE ZMIENI dane we WSZYSTKICH wierszach: Zapytanie 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?

Co zwraca rowCount() po wykonaniu UPDATE lub DELETE?

Dlaczego przy operacji DELETE przez prepared statement NIE wolno wstawiać id bezpośrednio do 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;
}