Session 3: Interaktion
Formulare, Validierung und der komplette CRUD-Zyklus.
Was wir heute bauen
- Ein Formular zum Erstellen neuer Posts
- Serverseitige Validierung mit Fehlermeldungen
- Bearbeiten und Löschen von Posts
- Admin-Prüfung: Nur Admins dürfen Posts verwalten
- Feedback über Flash-Messages
Rückblick: Wo stehen wir?
Wir haben: Posts in der DB, Benutzer mit is_admin, Auth-Middleware, Detailseite und Übersicht. Was fehlt: Formulare, CRUD, und die Admin-Beschränkung.
01Schritt 1: HTTP-Verben verstehen
Dein Browser kommuniziert mit dem Server über Verben:
| Verb | Bedeutung | Beispiel |
|---|---|---|
| GET | Daten abrufen | Seite anzeigen |
| POST | Neue Daten senden | Formular abschicken |
| PUT | Bestehende Daten aktualisieren | Post bearbeiten |
| DELETE | Daten löschen | Post entfernen |
Stell dir das wie Bestellungen im Restaurant vor: GET = "Die Karte bitte", POST = "Ich bestelle das Steak", PUT = "Doch lieber medium", DELETE = "Stornieren".
Browser können nur GET und POST. Für PUT und DELETE nutzt Laravel einen Trick — dazu gleich mehr.
02Schritt 2: Admin-Middleware erstellen
Bevor wir CRUD bauen, sichern wir die Routen ab. Wir erstellen eine eigene Middleware, die prüft ob der User ein Admin ist:
php artisan make:middleware IsAdmin
// app/Http/Middleware/IsAdmin.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->isAdmin()) {
abort(403, 'Nur Admins haben Zugriff.');
}
return $next($request);
}
}
Die Middleware prüft: Ist der aktuelle User ein Admin? Falls nicht, wird ein 403-Fehler (Forbidden) geworfen. Das ?-> ist der Nullsafe-Operator — falls kein User eingeloggt ist, wird nicht isAdmin() aufgerufen sondern direkt null zurückgegeben.
Middleware registrieren
In Laravel 13 registrierst du die Middleware in bootstrap/app.php:
// bootstrap/app.php — im withMiddleware-Block ergänzen
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'admin' => \App\Http\Middleware\IsAdmin::class,
]);
})
Jetzt können wir middleware('admin') in unseren Routen verwenden.
03Schritt 3: CRUD-Routen
// routes/web.php
use App\Http\Controllers\HomeController;
use App\Http\Controllers\PostController;
// Öffentlich
Route::get('/', [HomeController::class, 'index']);
Route::get('/about', [HomeController::class, 'about']);
Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{post}', [PostController::class, 'show']);
// Nur für Admins
Route::middleware(['auth', 'admin'])->group(function () {
Route::get('/posts/create', [PostController::class, 'create']);
Route::post('/posts', [PostController::class, 'store']);
Route::get('/posts/{post}/edit', [PostController::class, 'edit']);
Route::put('/posts/{post}', [PostController::class, 'update']);
Route::delete('/posts/{post}', [PostController::class, 'destroy']);
});
Beachte: middleware(['auth', 'admin']) — zwei Middlewares in Reihe. Zuerst wird geprüft ob der User eingeloggt ist (auth), dann ob er Admin ist (admin). Wie zwei Türsteher hintereinander.
Wichtig: /posts/create muss vor /posts/{post} stehen, sonst interpretiert Laravel "create" als Post-ID.
04Schritt 4: Das Formular bauen
{{-- resources/views/posts/create.blade.php --}}
<x-app-layout>
<div class="container py-4">
<h1>Neuen Post erstellen</h1>
<form action="/posts" method="POST" class="mt-4">
@csrf
<div class="mb-3">
<label for="title" class="form-label">Titel</label>
<input
type="text"
class="form-control @error('title') is-invalid @enderror"
id="title"
name="title"
value="{{ old('title') }}"
>
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="content" class="form-label">Inhalt</label>
<textarea
class="form-control @error('content') is-invalid @enderror"
id="content"
name="content"
rows="6"
>{{ old('content') }}</textarea>
@error('content')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Post erstellen</button>
<a href="/posts" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
</div>
</x-app-layout>
Drei wichtige Konzepte:
@csrf — Cross-Site Request Forgery Protection. Laravel generiert ein verstecktes Token. Ohne dieses Token lehnt der Server das Formular ab. Vergisst du @csrf, bekommst du einen 419-Fehler.
@error('title') — Zeigt die Fehlermeldung für das Feld an, falls die Validierung fehlschlägt.
old('title') — Befüllt das Feld mit dem vorher eingegebenen Wert. Wenn die Validierung fehlschlägt, muss der User nicht alles neu tippen.
05Schritt 5: Speichern mit Validierung
// app/Http/Controllers/PostController.php
use Illuminate\Http\Request;
// ... bestehende Methoden ...
public function create()
{
return view('posts.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|min:5|max:255',
'content' => 'required|min:10',
]);
$post = Post::create($validated);
return redirect("/posts/{$post->id}")
->with('success', 'Post erfolgreich erstellt.');
}
$request->validate() ist der Türsteher. Er prüft die Daten nach deinen Regeln:
| Regel | Bedeutung |
|---|---|
required |
Darf nicht leer sein |
min:5 |
Mindestens 5 Zeichen |
max:255 |
Höchstens 255 Zeichen |
nullable |
Darf leer sein |
Schlägt die Validierung fehl, leitet Laravel automatisch zurück zum Formular und stellt die Fehler über @error bereit. Besteht sie, enthält $validated nur die geprüften Felder.
Flash-Messages anzeigen
->with('success', '...') speichert eine einmalige Nachricht. Um sie anzuzeigen, ergänze das Layout. Suche in resources/views/layouts/app.blade.php die Stelle nach {{ $slot }} oder erstelle eine Blade-Component:
{{-- In resources/views/layouts/app.blade.php, vor {{ $slot }} --}}
@if(session('success'))
<div class="container mt-3">
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
</div>
@endif
Probier es aus: Logge dich als Admin ein, öffne /posts/create, füll das Formular aus und sende es ab. Du landest auf der Detailseite mit einer Erfolgsmeldung. Versuche es mit einem leeren Titel — du siehst die Fehlermeldung. Logge dich als normaler User ein und versuche /posts/create zu öffnen — du bekommst einen 403-Fehler.
06Schritt 6: Posts bearbeiten
// app/Http/Controllers/PostController.php
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => 'required|min:5|max:255',
'content' => 'required|min:10',
]);
$post->update($validated);
return redirect("/posts/{$post->id}")
->with('success', 'Post aktualisiert.');
}
{{-- resources/views/posts/edit.blade.php --}}
<x-app-layout>
<div class="container py-4">
<h1>Post bearbeiten</h1>
<form action="/posts/{{ $post->id }}" method="POST" class="mt-4">
@csrf
@method('PUT')
<div class="mb-3">
<label for="title" class="form-label">Titel</label>
<input
type="text"
class="form-control @error('title') is-invalid @enderror"
id="title"
name="title"
value="{{ old('title', $post->title) }}"
>
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="content" class="form-label">Inhalt</label>
<textarea
class="form-control @error('content') is-invalid @enderror"
id="content"
name="content"
rows="6"
>{{ old('content', $post->content) }}</textarea>
@error('content')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Speichern</button>
<a href="/posts/{{ $post->id }}" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
</div>
</x-app-layout>
@method('PUT') erzeugt ein verstecktes Feld _method=PUT. Das Formular nutzt method="POST" (Browser können nur das), aber Laravel erkennt das versteckte Feld und behandelt die Anfrage als PUT.
old('title', $post->title) — der zweite Parameter ist der Fallback. Beim ersten Laden zeigt das Feld den aktuellen Wert. Bei einem Validierungsfehler die letzte Eingabe.
07Schritt 7: Posts löschen
// app/Http/Controllers/PostController.php
public function destroy(Post $post)
{
$post->delete();
return redirect('/posts')
->with('success', 'Post gelöscht.');
}
Aktions-Buttons in der Detailseite (nur für Admins)
Erweitere die Show-View:
{{-- resources/views/posts/show.blade.php — unterhalb des Inhalts --}}
<div class="mt-4 d-flex gap-2">
<a href="/posts" class="btn btn-secondary">Zurück</a>
@if(auth()->user()?->isAdmin())
<a href="/posts/{{ $post->id }}/edit" class="btn btn-outline-primary">Bearbeiten</a>
<form action="/posts/{{ $post->id }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-outline-danger"
onclick="return confirm('Wirklich löschen?')">
Löschen
</button>
</form>
@endif
</div>
auth()->user()?->isAdmin() prüft: Ist der User eingeloggt UND Admin? Nur dann werden die Bearbeiten/Löschen-Buttons angezeigt. Normale User sehen nur den Zurück-Button.
Probier es aus: Als Admin siehst du alle Buttons. Als normaler User nur "Zurück". Nicht eingeloggt? Nur "Zurück".
08Schritt 8: "Neuer Post"-Link nur für Admins
In der Posts-Übersicht:
{{-- resources/views/posts/index.blade.php — im Header-Bereich --}}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Alle Posts</h1>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-primary">{{ $posts->count() }} Posts</span>
@if(auth()->user()?->isAdmin())
<a href="/posts/create" class="btn btn-primary btn-sm">Neuer Post</a>
@endif
</div>
</div>
Zusammenfassung
| Konzept | Was es macht | Analogie |
|---|---|---|
| @csrf | Schutz vor Cross-Site-Angriffen | Eintrittskarte |
| Validierung | Prüft Eingaben serverseitig | Türsteher |
| Flash-Messages | Einmalige Benachrichtigung | Quittung an der Kasse |
| @method('PUT/DELETE') | Simuliert HTTP-Verben | Sonderbestellung |
| Custom Middleware | Eigene Zugriffsprüfung (isAdmin) | VIP-Kontrolle |
Der CRUD-Überblick:
Create: GET /posts/create → Formular [nur Admin]
POST /posts → Speichern [nur Admin]
Read: GET /posts → Liste [alle]
GET /posts/{id} → Detail [alle]
Update: GET /posts/{id}/edit → Formular [nur Admin]
PUT /posts/{id} → Speichern [nur Admin]
Delete: DELETE /posts/{id} → Löschen [nur Admin]
Deine Aufgabe
- Erstelle einen zweiten User-Account (kein Admin)
- Prüfe: Kann dieser User
/posts/createaufrufen? (Nein → 403) - Erweitere die Validierung mit eigenen Fehlermeldungen:
``php $validated = $request->validate([ 'title' => 'required|min:5', 'content' => 'required|min:10', ], [ 'title.required' => 'Bitte gib einen Titel ein.', 'title.min' => 'Der Titel muss mindestens :min Zeichen haben.', ]); ``
Cheatsheet
Routen
Route::get('/url', [Controller::class, 'methode']);
Route::post('/url', [Controller::class, 'methode']);
Route::put('/url/{model}', [Controller::class, 'methode']);
Route::delete('/url/{model}', [Controller::class, 'methode']);
Route::middleware(['auth', 'admin'])->group(function () {
// Geschützte Routen
});
Validierung
$validated = $request->validate([
'feld' => 'required|min:5|max:255',
]);
Blade Formulare
| Direktive | Bedeutung |
|---|---|
@csrf |
CSRF-Schutz (Pflicht) |
@method('PUT') |
HTTP-Verb simulieren |
@error('feld') |
Fehler anzeigen |
old('feld') |
Vorherige Eingabe |
old('feld', $fallback) |
Mit Fallback-Wert |
auth()->user()?->isAdmin() |
Admin-Check in Views |