Blok 4 · rozdział 11
Zdobycia i postęp Korony
Plan trzymany w sesji nie jest już potrzebny. Zdobycie to wiersz: użytkownik, szczyt, data i notatka. Jedna osoba zapisuje dany szczyt raz. Strona „Moja Korona” liczy postęp do 28.
Po tym rozdziale powinieneś umieć
- dodać tabelę
zdobyciaz unikalną parą użytkownik–szczyt; - dopisać relacje do modelu
Userbez podmieniania klasy z Breeze; - zapisać zdobycie tylko dla zalogowanego i odrzucić duplikat;
- pokazać licznik oraz listę brakujących szczytów;
- zablokować usunięcie cudzego wiersza kodem 403.
1. Tabela i model
php artisan make:migration create_zdobycia_table
php artisan make:model Zdobycie
Schema::create('zdobycia', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('szczyt_id')->constrained('szczyty')->cascadeOnDelete();
$table->date('data_zdobycia');
$table->string('notatka')->nullable();
$table->timestamps();
$table->unique(['user_id', 'szczyt_id']);
});
php artisan migrate
Model Zdobycie:
protected $table = 'zdobycia';
protected $fillable = ['user_id', 'szczyt_id', 'data_zdobycia', 'notatka'];
protected function casts(): array
{
return [
'data_zdobycia' => 'date',
];
}
W Laravelu 13 casty zapisuje się metodą casts(). Gdy generator zostawił właściwość $casts, możesz zostać przy niej — efekt dla daty jest ten sam.
after_or_equal:today). Zdobycie dotyczy dnia, który już był albo jest dzisiaj: before_or_equal:today.
2. Relacje z kontem
W Zdobycie:
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function szczyt(): BelongsTo
{
return $this->belongsTo(Szczyt::class);
}
W klasie User (tej z Breeze — dopisujesz metodę, nie tworzysz drugiej klasy User):
public function zdobycia(): HasMany
{
return $this->hasMany(Zdobycie::class);
}
W Szczyt ta sama metoda zdobycia(): HasMany. Pamiętaj o use na górze plików.
users
Jedno konto ma wiele zdobyć.
N : M
przez zdobycia
szczyty
Jeden szczyt ma wielu zdobywców. Data leży w tabeli środkowej.
3. Zapis na karcie szczytu
php artisan make:controller ZdobycieController
Route::post('/szczyty/{szczyt}/zdobycia', [ZdobycieController::class, 'store'])
->middleware('auth')
->name('zdobycia.store');
public function store(Request $request, Szczyt $szczyt)
{
$dane = $request->validate([
'data_zdobycia' => ['required', 'date', 'before_or_equal:today'],
'notatka' => ['nullable', 'string', 'max:200'],
], [
'data_zdobycia.before_or_equal' => 'Data nie może być z przyszłości.',
]);
$juzJest = $request->user()
->zdobycia()
->where('szczyt_id', $szczyt->id)
->exists();
if ($juzJest) {
return back()->withErrors([
'data_zdobycia' => 'Ten szczyt jest już w Twojej koronie.',
]);
}
$request->user()->zdobycia()->create([
'szczyt_id' => $szczyt->id,
'data_zdobycia' => $dane['data_zdobycia'],
'notatka' => $dane['notatka'] ?? null,
]);
return redirect()
->route('szczyty.show', $szczyt)
->with('status', 'Zapisano zdobycie.');
}
create na relacji $request->user()->zdobycia() sam uzupełnia user_id.
Nie bierz identyfikatora użytkownika z ukrytego pola formularza — przeglądarka dałaby się podmienić.
Na karcie szczytu:
@auth
<form class="formularz" method="post" action="{{ route('zdobycia.store', $szczyt) }}">
@csrf
<label>Data wejścia
<input type="date" name="data_zdobycia" value="{{ old('data_zdobycia') }}">
</label>
@error('data_zdobycia')
<p class="blad">{{ $message }}</p>
@enderror
<label>Notatka
<input type="text" name="notatka" value="{{ old('notatka') }}">
</label>
<button type="submit">Zapisz zdobycie</button>
</form>
@else
<p><a href="{{ route('login') }}">Zaloguj się</a>, żeby zapisać ten szczyt.</p>
@endauth
@auth w HTML tego nie załatwia — chowa przycisk, nie zamyka trasy.
4. Moja Korona
Route::get('/moja-korona', [ZdobycieController::class, 'moja'])
->middleware('auth')
->name('korona.moja');
public function moja()
{
$user = auth()->user();
$zdobyte = $user->zdobycia()->count();
$wszystkie = Szczyt::query()->count();
$brakujace = Szczyt::query()
->with('pasmo')
->whereDoesntHave('zdobycia', function ($zapytanie) use ($user) {
$zapytanie->where('user_id', $user->id);
})
->orderByDesc('wysokosc_m')
->get();
return view('korona.moja', [
'zdobyte' => $zdobyte,
'wszystkie' => $wszystkie,
'brakujace' => $brakujace,
]);
}
Widok resources/views/korona/moja.blade.php rozszerza układ korona. Pasek:
<p>{{ $zdobyte }} / {{ $wszystkie }}</p>
<div class="postep">
<span style="width: {{ $wszystkie ? round($zdobyte / $wszystkie * 100) : 0 }}%"></span>
</div>
Pod spodem @forelse ($brakujace as $szczyt) i komponent karty. W menu dodaj link route('korona.moja') wewnątrz @auth.
5. Cudze zdobycie
Route::delete('/zdobycia/{zdobycie}', [ZdobycieController::class, 'destroy'])
->middleware('auth')
->name('zdobycia.destroy');
public function destroy(Zdobycie $zdobycie)
{
abort_unless($zdobycie->user_id === auth()->id(), 403);
$zdobycie->delete();
return back()->with('status', 'Usunięto zdobycie.');
}
Formularz usuwania przy własnym szczycie używa @method('DELETE') i @csrf. HTML zna tylko GET i POST, więc Laravel czyta ukryte pole metody.
Gdy zalogowany zapisuje zdobycie, a drugie konto nie widzi cudzych w swoim liczniku:
git add .
git status
git commit -m "Zapis zdobyć"
Przed commitem git status nie pokazuje .env.