Blok 2 · rozdział 6
Formularz i walidacja
Zanim zapiszesz zdobycie w bazie, zrób formularz „Planuję wyjście”.
Sprawdza datę, wraca z błędami i pamięta wpis w sesji.
W bloku 4 ten sam układ, czyli walidacja i przekierowanie, zapisze wiersz w tabeli zdobycia.
Po tym rozdziale powinieneś umieć
- przyjąć
POSTw kontrolerze przezRequest; - wstawić
@csrfi powiedzieć, przed czym chroni; - zwrócić błędy przy polach (
@error) i odtworzyć wpis (old()); - po sukcesie zrobić redirect, żeby odświeżenie nie wysłało formularza drugi raz;
- odróżnić komunikat na jedno odświeżenie od planu trzymanego w sesji do końca wizyty.
1. Formularz POST
Na stronie listy, pod siatką kart i wewnątrz @section('content'):
<form class="formularz" method="post" action="{{ route('plan.store') }}">
@csrf
<label>
Szczyt
<select name="slug">
@foreach ($szczyty as $szczyt)
<option value="{{ $szczyt['slug'] }}" @selected(old('slug') === $szczyt['slug'])>
{{ $szczyt['nazwa'] }}
</option>
@endforeach
</select>
</label>
@error('slug')
<p class="blad">{{ $message }}</p>
@enderror
<label>
Data
<input type="date" name="data" value="{{ old('data') }}">
</label>
@error('data')
<p class="blad">{{ $message }}</p>
@enderror
<label>
Notatka
<input type="text" name="notatka" value="{{ old('notatka') }}" maxlength="200">
</label>
@error('notatka')
<p class="blad">{{ $message }}</p>
@enderror
<button type="submit">Zapisz plan</button>
</form>
Formularz stoi na stronie listy, wewnątrz @section('content'), pod siatką kart. index już przekazuje $szczyty, więc @foreach ma po czym przejść.
Co jest w znaczniku, pole po polu:
method="post"wysyła dane w treści żądania, nie w adresie. Data i notatka nie zostają w pasku przeglądarki.actionto adres trasyplan.store. Samą trasę dopiszesz w podrozdziale 3. Do tego czasu wysłanie formularza zgłosi brak trasy. Tak ma być, dopóki trasy nie ma.nameprzyselectiinputjest kluczem, po którym kontroler czyta wartość. Beznamepole nie dociera.@foreachbuduje opcje z tej samej tablicy co karty.valueto slug, tekst opcji to nazwa.@error('data')wypisuje komunikat tylko wtedy, gdy walidacja odrzuciła poledata. W zmiennej$messagejest zdanie z kontrolera. To samo przy slugu i notatce.
@selected dopisuje atrybut selected, gdy warunek jest prawdziwy. Znasz ten mechanizm z @checked na stronie składni.
old('slug') oddaje wartość z poprzedniego, nieudanego wysłania. Po błędzie zostaje wybrany ten sam szczyt, a data i notatka wracają do pól value.
Przy pierwszej wizycie old() nie ma czego oddać i pola są puste.
2. CSRF
@csrf wstawia ukryte pole _token z jednorazową wartością trzymaną w sesji.
Laravel porównuje token z formularza z tokenem sesji i odrzuca POST, gdy się nie zgadzają albo gdy pola nie ma (kod 419).
Po dopięciu trasy z następnego podrozdziału zrób próbę: usuń na chwilę @csrf, wyślij formularz, zobacz stronę 419, przywróć dyrektywę.
W źródle działającego formularza ma być <input type="hidden" name="_token">. Tego pola nie piszesz ręcznie.
3. Walidacja i old()
Na górze kontrolera, jeśli jeszcze jej nie ma, dopisz use Illuminate\Http\Request;.
W routes/web.php dopisz trasę POST pod trasami GET. GET otwiera stronę, POST przyjmuje formularz.
Route::post('/plan', [SzczytController::class, 'zapiszPlan'])->name('plan.store');
public function zapiszPlan(Request $request)
{
$dane = $request->validate([
'slug' => ['required', 'string'],
'data' => ['required', 'date', 'after_or_equal:today'],
'notatka' => ['nullable', 'string', 'max:200'],
], [
'data.required' => 'Podaj datę wyjścia.',
'data.after_or_equal' => 'Plan dotyczy dzisiaj albo późniejszego dnia.',
]);
$dozwolone = collect($this->listaSzczytow())->pluck('slug');
if (! $dozwolone->contains($dane['slug'])) {
return back()
->withInput()
->withErrors(['slug' => 'Wybierz szczyt z listy.']);
}
$plany = session('plany', []);
$plany[] = $dane;
session(['plany' => $plany]);
return redirect()
->route('szczyty.index')
->with('status', 'Plan zapisany w sesji.');
}
validate
podajesz własne, kluczem pole.regula. Reszty komunikatów na kursie nie tłumaczysz pakietem językowym — dopisujesz te, które widać przy tym formularzu.
Reguły z tablicy, po kolei:
| Zapis | Przepuszcza | Odrzuca |
|---|---|---|
required | pole, które przyszło i nie jest puste | brak daty |
string | tekst | wartość, która nie jest tekstem |
date | datę, którą da się odczytać | napis w rodzaju „jutro”, gdyby pole nie było type="date" |
after_or_equal:today | dzisiaj albo później | wczorajszą datę |
nullable | puste pole albo tekst | nic — pusta notatka jest w porządku |
max:200 | tekst do 200 znaków | dłuższą notatkę |
validate przy błędzie samo wraca na poprzednią stronę, przenosi komunikaty do @error
i wartości do old(). Metoda się w tym miejscu urywa. Nie wołasz wtedy withInput ręcznie.
Slug z listy nie ma jeszcze reguły „musi być jednym z czterech”. Dlatego po validate jest drugie sprawdzenie.
collect(...)->pluck('slug') bierze z tablicy samą kolumnę slugów.
contains mówi, czy przysłany slug w niej jest. Przy podrzuconej wartości wracasz przez back()
— to skrót przekierowania na poprzedni adres — i sam dokładasz withInput() oraz withErrors,
bo to już nie jest wyjątek z validate.
Gdy oba sprawdzenia przejdą, do sesji trafia jeden plan. Potem przeglądarka dostaje adres listy, nie odpowiedź POST.
4. Redirect i sesja
Nad listą:
@if (session('status'))
<p class="komunikat">{{ session('status') }}</p>
@endif
@if (session('plany'))
<h2>Plany w tej sesji</h2>
<ul>
@foreach (session('plany') as $plan)
<li>
{{ $plan['slug'] }} — {{ $plan['data'] }}
@if (! empty($plan['notatka']))
— {{ $plan['notatka'] }}
@endif
</li>
@endforeach
</ul>
@endif
return redirect()
robi nowe żądanie GET. Odświeżenie F5 nie wysyła POST drugi raz.
Zostawienie użytkownika na odpowiedzi POST kończy się pytaniem „wysłać formularz ponownie?”.
Dwa zapisy w tej metodzie żyją różnie długo:
session(['plany' => $plany])trzyma tablicę do końca sesji. Drugi argumentsession('plany', [])przy odczycie daje pustą tablicę, gdy klucza jeszcze nie ma.->with('status', ...)dodaje komunikat tylko do następnego żądania. Taki jednorazowy komunikat nazywa się flash. Po wyświetleniu Laravel go usuwa.
Sprawdź to na liście. Zapisz plan z dzisiejszą datą: widać zielony pasek i jedną pozycję na liście planów. Naciśnij F5. Pasek znika, pozycja zostaje. Dopisz drugi plan z inną datą — lista ma dwa wiersze. Potem wyślij formularz z pustą datą: zostają wpisane pola, pod datą jest „Podaj datę wyjścia.”, a nowy wiersz nie dochodzi. Na końcu wyślij wczorajszą datę i przeczytaj drugi komunikat.
Plany znikną po wyczyszczeniu ciasteczek albo gdy sesja wygaśnie. W blokach 1–2 sesja leży w plikach
(SESSION_DRIVER=file w .env), w katalogu storage/framework/sessions.
Sesja nie zastępuje bazy. Zapis zdobycia, już przy koncie użytkownika, zrobisz w rozdziale 11.
Gdy formularz odrzuca pustą datę, a po udanym zapisie wraca na listę:
git add .
git status
git commit -m "Formularz planu wyjścia"
Przed commitem git status nie pokazuje .env.