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(); }
<?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;
}
- begin_transaction zaczyna transakcję na tym samym połączeniu. try zawiera instrukcje, które mogą zgłosić wyjątek.
- commit zatwierdza wszystkie zapisy. Czyścimy koszyk dopiero po jego sukcesie.
- catch przechwytuje obiekt błędu do zmiennej blad, rollback wycofuje zapisy; throw przekazuje problem dalej do obsługi błędów projektu.
- 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
| Zapis | Znaczenie | Przykł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 / throw | try 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 $_POST | Specjalne 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.
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
<?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.
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ć
- Wykonasz zapis nagłówka i pozycji w transakcji oraz zabezpieczysz ponowienie formularza.
- Potrafisz wskazać w kodzie przyczynę wyniku i zaplanować próbę błędnych danych.
- 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ę →