Aplikacja do przeglądania koncertów, wybierania miejsc, składania rezerwacji z limitem czasu (hold 10 minut), symulacji płatności oraz pobierania biletu w PDF z kodem QR. Backend kładzie nacisk na spójność danych w transakcjach (blokady pesymistyczne w Doctrine) oraz asynchroniczne zwalnianie nieopłaconych rezerwacji przez Symfony Messenger.
| Element | Opis |
|---|---|
backend/ |
API Symfony 7, API Platform, Doctrine ORM, Messenger, testy |
frontend/ |
SPA Vue 3 (Vite) + Vuetify, proxy do API |
docker-compose.yml |
MySQL 8 do lokalnego developmentu (port hosta domyślnie 3307) |
.github/workflows/ |
CI na GitHub Actions (PHPUnit, Psalm, PHP CS Fixer, ESLint, build Vite) |
preview.webp |
Zrzut ekranu interfejsu (mapa miejsc) |
.env.docker.example |
Szablon zmiennych dla Dockera (skopiuj do .env w katalogu głównym) |
| Ścieżka | Zawartość |
|---|---|
config/ |
Bundles, routing, packages/ (Doctrine, API Platform, Messenger, Nelmio CORS, itd.) |
migrations/ |
Migracje schematu bazy |
public/ |
Front kontrolera (index.php) |
src/Command/ |
Komendy konsolowe (app:seed-demo) |
src/Controller/ |
Akcje poza API Platform (np. PDF biletu, symulacja płatności) |
src/Entity/ |
Encje Doctrine: Concert, Seat, Reservation |
src/Enum/ |
SeatStatus, ReservationStatus |
src/Message/ + src/MessageHandler/ |
Wiadomość ExpireReservationMessage i jej obsługa (Messenger) |
src/Repository/ |
Repozytoria z metodami findAndLock (blokady pesymistyczne) |
src/Service/ |
Logika domenowa m.in. BookingService (rezerwacja, płatność) |
src/State/ |
Procesory API Platform (np. tworzenie rezerwacji) |
tests/Integration/, tests/Unit/ |
Testy API (SQLite) i jednostkowe |
| Ścieżka | Zawartość |
|---|---|
src/views/BookingView.vue |
Główny widok: wybór koncertu, mapa miejsc, kroki rezerwacji |
src/api/client.js |
Klient HTTP (Axios) |
src/router/ |
Vue Router |
vite.config.js |
Proxy /api → backend w trybie dev |
Pessimistic locking w tym projekcie to pesymistyczne blokady zapisu Doctrine: LockMode::PESSIMISTIC_WRITE. W MySQL (InnoDB) odpowiada to zwykle zapytaniu SELECT … FOR UPDATE — wiersz jest zablokowany do zakończenia bieżącej transakcji, więc inne sesje nie mogą w tym czasie zmodyfikować tego samego rekordu w sposób powodujący wyścig.
Dlaczego w transakcji: blokada ma sens tylko w obrębie aktywnej transakcji SQL. W kodzie operacje biznesowe są owinięte w EntityManager::wrapInTransaction(...), żeby odczyt z blokadą, walidacja i zapis (flush) działy jako jedna atomowa całość.
Gdzie w projekcie:
-
Rezerwacja miejsca —
SeatRepository::findAndLock()ładuje wierszSeatzPESSIMISTIC_WRITE. Dopiero po sprawdzeniu, że miejsce jestAvailable, tworzona jest rezerwacjaPending, a status miejsca zmieniany naReserved. Dwie równoległe prośby o to samo miejsce są szeregowane przez bazę: druga transakcja czeka na pierwszą; po zwolnieniu blokady widzi już zaktualizowany stan i może zakończyć się błędem „miejsce niedostępne” zamiast nadpisać pierwszą rezerwację. -
Płatność — w
BookingService::markPaid()rezerwacja jest ładowana przezEntityManager::find(..., LockMode::PESSIMISTIC_WRITE), aby nie kolidować z równoległą zmianą statusu (np. wygaśnięciem holdu). -
Wygaszenie rezerwacji —
ExpireReservationMessageHandlerużywaReservationRepository::findAndLock(), żeby bezpiecznie sprawdzić status, ewentualnie zwolnić miejsce i ustawić rezerwację jako wygasłą bez wyścigu z inną operacją na tym samym wierszu.
To uzupełnia model „najpierw zablokuj wiersz, potem decyduj” zamiast polegania wyłącznie na optymistycznej wersji rekordu przy dużej konkurencji o te same miejsca.
- PHP 8.3+
- Symfony 7.x
- API Platform 4.x (REST, JSON i JSON-LD)
- Doctrine ORM 3.x, MySQL 8
- Symfony Messenger z transportem Doctrine (kolejka
booking, opóźnione wiadomościDelayStamp) - Dompdf — generowanie PDF
- endroid/qr-code — kod QR na bilecie
- NelmioCorsBundle — CORS dla frontendu deweloperskiego
- Vue 3 (Composition API)
- Vite
- Vuetify 3
- Vue Router 4
- Axios (proxy
/api→ backend w trybie dev) - ESLint 9 (flat config) +
eslint-plugin-vue—npm run lintw katalogufrontend/
- PHPUnit + Symfony PHPUnit Bridge
- Psalm
- PHP CS Fixer (reguła
@Symfony) - GitHub Actions — workflow
.github/workflows/ci.ymluruchamia na push/PR domain/masterjoby backendu (Composer, PHPUnit, Psalm, CS Fixer dry-run) oraz frontendu (npm ci, ESLint, produkcyjny build Vite)
Przydatne skrypty Composer w backend/: composer psalm, composer cs-fix, composer test.
- Integracja (API):
tests/Integration/Api/— SymfonyWebTestCase, baza SQLite (var/test.db, tworzona przy testach), m.in. kolekcja koncertów, filtrowanie miejsc, pełny przepływ rezerwacja → płatność → PDF. - Jednostkowe:
tests/Unit/— m.in.BookingReservationProcessorz mockami.
Uruchomienie:
cd backend
composer test
# lub: php bin/phpunitDo testów funkcjonalnych wymagany jest pakiet symfony/browser-kit (już w require-dev).
- Docker: obraz
mysql:8.0, bazaconcert_booking, użytkownikconcert/ hasłoconcert, port hosta 3306.
Wymagania: PHP 8.3+, Composer, Node.js (npm), opcjonalnie Docker Desktop (MySQL) oraz Symfony CLI albo wbudowany serwer PHP.
Z katalogu głównego repozytorium:
W katalogu głównym projektu utwórz plik .env (obok docker-compose.yml, nie commituj — jest w .gitignore) na podstawie .env.docker.example i ustaw MYSQL_PASSWORD na to samo hasło, które podajesz w backend/.env w DATABASE_URL dla użytkownika concert. Domyślnie w compose jest concert, jeśli nie ustawisz .env.
Uwaga: hasło użytkownika concert w MySQL ustawia się tylko przy pierwszym starcie pustego wolumenu. Zmiana MYSQL_PASSWORD w .env nie zmienia hasła w już istniejącej bazie — wtedy albo docker compose down -v (kasuje dane w wolumenie) i ponownie up -d, albo ręcznie ALTER USER w MySQL.
docker compose up -dNa Windows wymagany jest działający Docker Desktop (ikona w zasobniku = „Running”). Błąd w stylu dockerDesktopLinuxEngine: The system cannot find the file specified oznacza, że silnik Docker nie działa — uruchom Docker Desktop i spróbuj ponownie. Bez Dockera możesz zainstalować MySQL lokalnie i ustawić DATABASE_URL w backend/.env.
Domyślne mapowanie w docker-compose.yml to port hosta 3307 (wewnątrz kontenera nadal 3306), żeby nie kolidować z innym MySQL/MariaDB już nasłuchującym na 3306. W backend/.env ustaw DATABASE_URL z 127.0.0.1:3307 (i hasłem zgodnym z MYSQL_PASSWORD w compose, domyślnie concert).
Jeśli wolisz port 3306 dla Dockera, zatrzymaj lokalny serwis MySQL albo zmień mapowanie portów w docker-compose.yml i port w DATABASE_URL tak, aby się zgadzały.
Przy własnej instancji MySQL (bez Dockera) dostosuj DATABASE_URL w backend/.env lub backend/.env.local.
cd backend
composer installJeśli Composer zgłasza konflikt z rozszerzeniem redis w środowisku lokalnym, możesz tymczasowo użyć:
composer install --ignore-platform-req=ext-redisMigracje i przykładowe dane (jeden koncert + siatka miejsc):
php bin/console doctrine:migrations:migrate --no-interaction
php bin/console app:seed-demo
# po zmianie siatki miejsc: php bin/console app:seed-demo --resetUruchom serwer HTTP (wybierz jedną z opcji):
# Symfony CLI (jeśli zainstalowane)
symfony server:start
# lub wbudowany serwer PHP (port 8000)
php -S 127.0.0.1:8000 -t publicAPI (dokumentacja): http://127.0.0.1:8000/api (ścieżka może się różnić w zależności od narzędzia).
Bez działającego konsumenta kolejki rezerwacje pending nie zostaną automatycznie zwolnione po 10 minutach. W osobnym terminalu:
cd backend
php bin/console messenger:consume async -vvcd frontend
npm install
npm run lint
npm run devVite nasłuchuje zwykle na http://127.0.0.1:5173 i proxy przekazuje /api na backend. Domyślny cel proxy to http://127.0.0.1:8000 (plik frontend/.env.development, zmienna API_PROXY_TARGET). Najpierw uruchom backend (krok 2), potem npm run dev.
Oznacza to zwykle, że Vite nie może połączyć się z serwerem API (proxy zwraca Bad Gateway). Sprawdź: czy Symfony/PHP nasłuchuje na tym samym hoście i porcie co API_PROXY_TARGET, oraz czy w konsoli Vite nie ma komunikatu o błędzie proxy. Po uruchomieniu backendu odśwież stronę.
- Wybór koncertu i miejsca (mapa siedzeń).
- Podanie adresu e-mail i utworzenie rezerwacji (hold 10 min).
- Podsumowanie: przycisk symulacji płatności, potem otwarcie biletu PDF.
Uwaga: APP_SECRET w backend/.env powinien być ustawiony na unikalną wartość przed wdrożeniem produkcyjnym; w repozytorium nie commituj plików z sekretami produkcyjnymi (/.env.local).
