INF.03 · Aplikacje internetowe PHP

Podsumowanie koszyka i symulacja płatności

Zamówienie zapisuje się w całości albo wcale.

  • PHP8.3
  • mysqli · MariaDB
  • Praktyka i diagnoza

Po tej lekcji: Wykonasz zapis nagłówka i pozycji w transakcji oraz zabezpieczysz ponowienie formularza.

Zrozum mechanizm

Zamówienie obejmuje kilka zapisów. Begin_transaction otwiera transakcję; commit zatwierdza całość, rollback cofa zmiany po błędzie. Tabele muszą używać InnoDB. Ceny i istnienie filmów odczytujemy ponownie w transakcji, z blokadą FOR UPDATE. Do pozycji zapisujemy cenę z chwili zamówienia, aby późniejsza zmiana katalogu nie zmieniała historii.

Losowy klucz formularza zapisany z UNIQUE zapobiega utworzeniu drugiego zamówienia przy powtórzeniu tej samej operacji. Nie zastępuje tokenu CSRF — mają różne role. Koszyk czyścimy dopiero po commit. W razie błędu zachowujemy go do ponownej próby. PHP standardowo blokuje jedną sesję podczas żądania; mimo tego UNIQUE w bazie chroni także integralność zapisu.

Przycisk zapisuje wyłącznie zamówienie ze statusem symulacja. Nie pobiera pieniędzy i nie udaje potwierdzenia operatora płatności. Rzeczywiste płatności wymagają m.in. weryfikacji podpisanych powiadomień i osobnego procesu uzgadniania.

Jak uruchamiać krótkie przykłady?

Każdy krótki przykład oznaczony własnym plikiem uruchamiaj osobno, nie doklejaj wszystkich definicji do jednego pliku. W edytorze zapisz go jako .php, a w terminalu tego folderu wpisz np. php for.php. Wtedy pokazane końce wierszy PHP_EOL są widoczne. W odpowiedzi HTML znak końca wiersza sam nie robi nowej linii na ekranie: użyj <br> albo znacznika <pre>. Wyświetl źródło pozwala zobaczyć tekst odpowiedzi.

Przykłady opisane jako fragment projektu wymagają paczki KINO / LAB i wcześniejszej konfiguracji. Nie uruchamiaj ich jako samodzielnych endpointów. Pełny plik niżej pokazuje kontekst. Funkcje h(), pole() i wymagajPost() są nasze; ich definicje wczytuje bootstrap.php.

Transakcja: zatwierdź całość albo cofnij

Nagłówek zamówienia i jego pozycje muszą powstać razem. Nie może zostać połowa zamówienia po błędzie. To fragment pracy skonfigurowanej aplikacji, nie samodzielny plik.

Podstawowa forma: begin_transaction(); try { zapisy; commit(); } catch (...) { rollback(); }

transaction.phpphp
<?php
$polaczenie->begin_transaction();
try {
    // Tutaj w zamow.php: odczyt cen, INSERT nagłówka i pozycji.
    // Każdy zapis korzysta z przygotowanego zapytania.
    $polaczenie->commit();
    $_SESSION['koszyk'] = [];
} catch (Throwable $blad) {
    $polaczenie->rollback();
    // Koszyk pozostaje do ponownej próby; obsługujemy błąd.
    throw $blad;
}
  1. begin_transaction zaczyna transakcję na tym samym połączeniu. try zawiera instrukcje, które mogą zgłosić wyjątek.
  2. commit zatwierdza wszystkie zapisy. Czyścimy koszyk dopiero po jego sukcesie.
  3. catch przechwytuje obiekt błędu do zmiennej blad, rollback wycofuje zapisy; throw przekazuje problem dalej do obsługi błędów projektu.
  4. Throwable to wspólny typ przechwytywanych błędów/wyjątków. Komentarze wskazują miejsce prawdziwych zapisów; sam ten skrót nie tworzy zamówienia. Pełny plik poniżej je implementuje.

Jak czytać pozostałe zapisy w pełnym przykładzie

ZapisZnaczeniePrzykład użycia
pole($dane, $nazwa)Nasza funkcja: pobiera wskazany klucz tylko wtedy, gdy ma wartość string; inaczej oddaje pusty napis. isset sprawdza obecność i brak null, is_string typ.pole($_POST, "email")
wymagajPost()Nasza funkcja: sprawdza metodę POST oraz CSRF; przy błędzie przerywa odpowiedź z405/403.wymagajPost();
wymagajLogowania()Nasza funkcja: oddaje int ID konta z sesji lub przekierowuje do logowania. $id = wymagajLogowania();
przekieruj($adres)Nasza funkcja wysyła Location ze statusem303 i kończy wykonanie przez exit. Typ wyniku never oznacza, że nie wraca do dalszej instrukcji.przekieruj("/panel.php");
dirname(__DIR__)dirname daje katalog nadrzędny; __DIR__ to katalog bieżącego pliku. W public prowadzi to do katalogu projektu, a dopisana ścieżka wskazuje src.dirname(__DIR__)."/src/bootstrap.php"
try / catch / throwtry obejmuje próbę, catch przechwytuje wyjątek, throw go zgłasza; $blad jest obiektem błędu. W mysqli raportowanie wyjątków trzeba włączyć.try { /* operacja */ } catch (Throwable $blad) { /* obsługa */ }
$_SESSION i $_POSTSpecjalne tablice PHP: pierwsza to stan sesji, druga dane formularza POST. Zewnętrzne wartości trzeba sprawdzić, nie ufać samym nazwom kluczy.$_SESSION["user_id"]
?? i ??=a ?? b wybiera b dla braku/null; a ??= b zapisuje b tylko, gdy a nie istnieje lub ma null. Nie reagują na0 ani pusty napis.$_SESSION["koszyk"] ??= [];
-> oraz ::-> wybiera właściwość lub metodę obiektu; :: odnosi się do elementu statycznego klasy. To nie jest zapis dostępu do tablicy.$zapytanie->execute();

Sprawdź w działaniu

Zamówienie zapisuje się w całości albo wcale — sprawdź w modelu

Model w przeglądarce wyjaśnia mechanizm na fikcyjnych danych. Nie wykonuje PHP ani zapytań SQL i nie tworzy kont. Prawdziwe skrypty uruchomisz lokalnie z paczki.

Ta interakcja wymaga włączonego JavaScriptu.

Przewidź rezultat, wykonaj próbę i wyjaśnij, która decyzja po stronie serwera decyduje o wyniku.

Uruchom krok po kroku

Zaloguj się, dodaj pozycje i zapisz zamówienie. W bazie porównaj sumę w Zamowienia z ilosc*cena_grosze w Pozycje. Powtórz to samo POST: liczba zamówień nie rośnie. W testowej kopii usuń wybrany film przed finalizacją — brak częściowego zamówienia, a koszyk nie jest czyszczony.

Plik wzorcowy: public/zamow.php

public/zamow.phpphp
<?php

require_once dirname(__DIR__).'/src/db.php';
$idUzytkownika = wymagajLogowania();
wymagajPost();
$klucz = pole($_POST, 'klucz');
if (strlen($klucz) !== 64 || ! hash_equals($_SESSION['zamowienie_klucz'] ?? '', $klucz)) {
    http_response_code(403);
    exit('Nieaktualny formularz zamówienia.');
}
$poprzednie = $polaczenie->prepare('SELECT id FROM Zamowienia WHERE klucz=? AND uzytkownik_id=?');
$poprzednie->bind_param('si', $klucz, $idUzytkownika);
$poprzednie->execute();
if ($poprzednie->get_result()->fetch_assoc()) {
    przekieruj('/panel.php');
}
$koszyk = $_SESSION['koszyk'] ?? [];
if (! $koszyk) {
    http_response_code(422);
    exit('Koszyk jest pusty.');
}
ksort($koszyk); // Stała kolejność blokowania rekordów.
$polaczenie->begin_transaction();
try {
    $suma = 0;
    $pozycje = [];
    $odczyt = $polaczenie->prepare('SELECT id, cena_grosze FROM Filmy WHERE id=? FOR UPDATE');
    foreach ($koszyk as $id => $ilosc) {
        $odczyt->bind_param('i', $id);
        $odczyt->execute();
        $film = $odczyt->get_result()->fetch_assoc();
        if (! $film || $ilosc < 1 || $ilosc > 10) {
            throw new RuntimeException('Nieprawidłowy koszyk.');
        }
        $cena = (int) $film['cena_grosze']; // Cena z bazy, nigdy z formularza.
        $suma += $cena * $ilosc;
        $pozycje[] = [$id, $ilosc, $cena];
    }
    $zapis = $polaczenie->prepare('INSERT INTO Zamowienia (uzytkownik_id, klucz, suma_grosze) VALUES (?, ?, ?)');
    $zapis->bind_param('isi', $idUzytkownika, $klucz, $suma);
    $zapis->execute();
    $idZamowienia = $polaczenie->insert_id;
    $wiersz = $polaczenie->prepare('INSERT INTO Pozycje (zamowienie_id, film_id, ilosc, cena_grosze) VALUES (?, ?, ?, ?)');
    foreach ($pozycje as [$id, $ilosc, $cena]) {
        $wiersz->bind_param('iiii', $idZamowienia, $id, $ilosc, $cena);
        $wiersz->execute();
    }
    $polaczenie->commit();
    $_SESSION['koszyk'] = []; // Dopiero po zatwierdzeniu.
} catch (Throwable $blad) {
    $polaczenie->rollback();
    http_response_code(409);
    exit('Zamówienie nie zostało zapisane. Odśwież koszyk i spróbuj ponownie.');
}
przekieruj('/panel.php');

Pobierz i odtwórz projekt

W „Źródłach i materiałach” na końcu pobierz KINO / LAB.zip. Rozpakuj całość; instrukcja START.txt opisuje konfigurację. Pierwsze 6 przykładów w public/podstawy działa bez bazy. Dalsze wymagają lokalnej MariaDB 11.4, importu schema.sql i lokalnego konta bazy z minimalnymi prawami. Gotowa paczka pokazuje całość, ale w tej lekcji wykonaj opisane kroki i własne zadanie.

Ćwiczenie z pomocą · samodzielna praca

Twoja próba — podpowiedzi otwieraj po kolei

Sprawdź poprawny zapis, puste zamówienie, powtórzenie klucza i błąd jednej pozycji.

Oczekiwany rezultat
Brak częściowego zapisu i brak podwójnego zamówienia; suma wynika z cen bazy, nie z POST.
Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź

Odszukaj wskazany plik i zapisz przewidywany rezultat przed zmianą.

Krok 2 — pokaż podpowiedź

Zmień jedną rzecz. Sprawdź metodę żądania, dane i odpowiedź lub wynik programu.

Krok 3 — pokaż podpowiedź

Wykonaj próbę graniczną lub błędną. Porównaj także stan bazy lub sesji, nie tylko komunikat.

Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.

Podsumowanie

Co warto zapamiętać

  1. Wykonasz zapis nagłówka i pozycji w transakcji oraz zabezpieczysz ponowienie formularza.
  2. Potrafisz wskazać w kodzie przyczynę wyniku i zaplanować próbę błędnych danych.
  3. Nie oddajesz haseł, pliku config.local.php ani wartości cookie.

Dalej na swoim koncie

Rozgrzewki, quiz i zadania są w biurku ucznia.

Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.

Zaloguj się →
Źródła i dalsza nauka