Start Prinzipien Session 1 Session 2 Session 3 Session 4

Session 3: Interaktion

Ziel dieser Session

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:

Terminal
php artisan make:middleware IsAdmin
app/Http/Middleware/IsAdmin.php
// 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
// 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
// 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
{{-- 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
// 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 }}
{{-- 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
// 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
{{-- 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
// 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
{{-- 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
{{-- 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:

Code
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

  1. Erstelle einen zweiten User-Account (kein Admin)
  2. Prüfe: Kann dieser User /posts/create aufrufen? (Nein → 403)
  3. 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

php
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

php
$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