Start Prinzipien Session 1 Session 2 Session 3 Session 4

Session 4: Kommentare und Benutzer

Ziel dieser Session

Relationen, Authentifizierung und Autorisierung.

Was wir heute bauen

  • Kommentare unter Posts
  • Relationen zwischen Models (Post hat viele Kommentare, Kommentar gehört zu User)
  • Jeder eingeloggte User darf kommentieren
  • User dürfen eigene Kommentare löschen
  • Admins dürfen alle Kommentare löschen
  • Autorisierung über Policies

Rückblick: Wo stehen wir?

Wir haben: Posts mit CRUD (nur Admins), Benutzer mit is_admin, Auth-Middleware, Validierung, Flash-Messages. Was fehlt: Relationen zwischen Daten und ein Kommentar-Feature für alle User.

01Schritt 1: Comment-Model und Migration

Kommentare gehören zu Posts und zu Usern. Ein Post hat viele Kommentare. Ein Kommentar gehört zu genau einem Post und einem User. Das sind Relationen — der wichtigste Punkt dieser Session.

Die Analogie: Ein Autor hat viele Bücher. Jedes Buch gehört zu einem Autor. In Laravel drücken wir das mit hasMany und belongsTo aus.

Terminal
php artisan make:model Comment -m

Migration definieren

database/migrations/xxxx_create_comments_table.php
// database/migrations/xxxx_create_comments_table.php

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained()->onDelete('cascade');
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->text('content');
    $table->timestamps();
});

Drei neue Dinge:

  • foreignId('post_id') — erstellt eine Spalte post_id als Fremdschlüssel
  • constrained() — sagt: Diese ID muss in der posts-Tabelle existieren
  • onDelete('cascade') — wenn ein Post gelöscht wird, werden seine Kommentare automatisch mitgelöscht
Terminal
php artisan migrate

Comment-Model

app/Models/Comment.php
// app/Models/Comment.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
    protected $fillable = ['post_id', 'user_id', 'content'];

    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

02Schritt 2: Relationen definieren

Post hat viele Kommentare

app/Models/Post.php — in der Klasse ergänzen
// app/Models/Post.php — in der Klasse ergänzen

use Illuminate\Database\Eloquent\Relations\HasMany;

public function comments(): HasMany
{
    return $this->hasMany(Comment::class);
}

Jetzt kannst du elegant auf die Kommentare eines Posts zugreifen:

php
$post = Post::find(1);
$comments = $post->comments;          // Alle Kommentare
$count = $post->comments->count();     // Anzahl

Und umgekehrt:

php
$comment = Comment::find(1);
$post = $comment->post;               // Der zugehörige Post
$author = $comment->user->name;       // Name des Autors

Kein SQL, keine JOINs. Eloquent erledigt das.

03Schritt 3: Eager Loading — Performance

Stell dir vor, ein Post hat 50 Kommentare. Für jeden Kommentar fragt Laravel einzeln den User aus der Datenbank ab. Das sind 1 Abfrage für die Kommentare + 50 für die User = 51 Abfragen. Das ist wie 50 Mal zum Kühlschrank laufen statt einmal alles rauszuholen.

Die Lösung: Eager Loading mit with() oder load().

app/Http/Controllers/PostController.php — show-Methode anpassen
// app/Http/Controllers/PostController.php — show-Methode anpassen

public function show(Post $post)
{
    $post->load('comments.user');

    return view('posts.show', compact('post'));
}

load('comments.user') sagt: "Lade alle Kommentare und für jeden Kommentar gleich den User mit." Das sind 3 Abfragen statt 51.

04Schritt 4: Kommentare anzeigen

Erweitere die Post-Detailseite:

resources/views/posts/show.blade.php — nach dem Post-Inhalt, vor den Buttons
{{-- resources/views/posts/show.blade.php — nach dem Post-Inhalt, vor den Buttons --}}

<section class="mt-5">
    <h3>Kommentare ({{ $post->comments->count() }})</h3>

    @forelse($post->comments as $comment)
        <div class="card mb-2">
            <div class="card-body">
                <div class="d-flex justify-content-between align-items-start">
                    <div>
                        <strong>{{ $comment->user->name }}</strong>
                        <small class="text-muted ms-2">{{ $comment->created_at->diffForHumans() }}</small>
                    </div>

                    @if(auth()->id() === $comment->user_id || auth()->user()?->isAdmin())
                        <form action="/comments/{{ $comment->id }}" method="POST">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="btn btn-sm btn-outline-danger"
                                    onclick="return confirm('Kommentar löschen?')">
                                &times;
                            </button>
                        </form>
                    @endif
                </div>
                <p class="mb-0 mt-2">{{ $comment->content }}</p>
            </div>
        </div>
    @empty
        <p class="text-muted">Noch keine Kommentare. Schreibe den ersten.</p>
    @endforelse
</section>

Beachte die Lösch-Logik im Template:

blade
@if(auth()->id() === $comment->user_id || auth()->user()?->isAdmin())

Der Lösch-Button erscheint wenn:

  • Der eingeloggte User der Autor des Kommentars ist, ODER
  • Der eingeloggte User ein Admin ist

Normale User sehen nur den Button bei ihren eigenen Kommentaren. Admins sehen ihn bei allen.

05Schritt 5: Kommentar-Formular

Nur eingeloggte User sollen kommentieren können:

resources/views/posts/show.blade.php — nach der Kommentar-Liste
{{-- resources/views/posts/show.blade.php — nach der Kommentar-Liste --}}

@auth
    <div class="card mt-4">
        <div class="card-body">
            <h5>Kommentar schreiben</h5>
            <form action="/posts/{{ $post->id }}/comments" method="POST">
                @csrf
                <div class="mb-3">
                    <textarea
                        name="content"
                        class="form-control @error('content') is-invalid @enderror"
                        rows="3"
                        placeholder="Dein Kommentar..."
                    >{{ old('content') }}</textarea>
                    @error('content')
                        <div class="invalid-feedback">{{ $message }}</div>
                    @enderror
                </div>
                <button type="submit" class="btn btn-primary">Kommentar speichern</button>
            </form>
        </div>
    </div>
@else
    <div class="alert alert-info mt-4">
        <a href="/login">Melde dich an</a>, um Kommentare zu schreiben.
    </div>
@endauth

@auth ... @else ... @endauth — zeigt den Inhalt nur für eingeloggte User. Gäste sehen den Login-Hinweis.

06Schritt 6: Kommentar speichern

Route

routes/web.php — innerhalb einer auth-Middleware-Gruppe
// routes/web.php — innerhalb einer auth-Middleware-Gruppe

Route::middleware('auth')->group(function () {
    Route::post('/posts/{post}/comments', [CommentController::class, 'store']);
    Route::delete('/comments/{comment}', [CommentController::class, 'destroy']);
});

Vergiss nicht den Import: use App\Http\Controllers\CommentController;

Controller erstellen

Terminal
php artisan make:controller CommentController
app/Http/Controllers/CommentController.php
// app/Http/Controllers/CommentController.php

<?php

namespace App\Http\Controllers;

use App\Models\Comment;
use App\Models\Post;
use Illuminate\Http\Request;

class CommentController extends Controller
{
    public function store(Request $request, Post $post)
    {
        $validated = $request->validate([
            'content' => 'required|min:3',
        ]);

        $post->comments()->create([
            'user_id' => auth()->id(),
            'content' => $validated['content'],
        ]);

        return redirect("/posts/{$post->id}")
            ->with('success', 'Kommentar gespeichert.');
    }

    public function destroy(Comment $comment)
    {
        $this->authorize('delete', $comment);

        $postId = $comment->post_id;
        $comment->delete();

        return redirect("/posts/{$postId}")
            ->with('success', 'Kommentar gelöscht.');
    }
}

$post->comments()->create(...) erstellt einen neuen Kommentar und setzt post_id automatisch. $this->authorize('delete', $comment) prüft die Berechtigung über eine Policy — die bauen wir jetzt.

Probier es aus: Logge dich ein, öffne einen Post und schreibe einen Kommentar. Er erscheint sofort mit deinem Namen.

07Schritt 7: Policy — Wer darf was?

Eine Policy definiert Berechtigungen für ein Model. Das ist wie Hausrecht: Du darfst dein Geschirr abräumen, aber nicht das der Nachbarn — es sei denn, du bist der Wirt (Admin).

Terminal
php artisan make:policy CommentPolicy --model=Comment
app/Policies/CommentPolicy.php
// app/Policies/CommentPolicy.php

<?php

namespace App\Policies;

use App\Models\Comment;
use App\Models\User;

class CommentPolicy
{
    public function delete(User $user, Comment $comment): bool
    {
        // Eigene Kommentare darf jeder löschen
        if ($user->id === $comment->user_id) {
            return true;
        }

        // Admins dürfen alle Kommentare löschen
        return $user->isAdmin();
    }
}

Die Logik ist explizit und lesbar:

  1. Bist du der Autor? Dann darfst du löschen.
  2. Bist du Admin? Dann darfst du auch löschen.
  3. Sonst: Nein.

Im Controller haben wir $this->authorize('delete', $comment) — das ruft die delete-Methode der Policy auf. Gibt sie false zurück, wirft Laravel einen 403-Fehler.

Probier es aus:

  1. Erstelle zwei Accounts (einen Admin, einen normalen User)
  2. Schreibe als normaler User einen Kommentar
  3. Logge dich als der andere normale User ein — du siehst keinen Lösch-Button beim fremden Kommentar
  4. Logge dich als Admin ein — du siehst den Lösch-Button bei allen Kommentaren

08Schritt 8: Kommentaranzahl in der Übersicht

Zeige in der Posts-Übersicht, wie viele Kommentare ein Post hat:

app/Http/Controllers/PostController.php — index-Methode anpassen
// app/Http/Controllers/PostController.php — index-Methode anpassen

public function index()
{
    $posts = Post::withCount('comments')->latest()->get();

    return view('posts.index', compact('posts'));
}

withCount('comments') fügt jedem Post ein comments_count-Attribut hinzu — ohne alle Kommentare tatsächlich zu laden.

In der Posts-Übersicht, beim einzelnen Post
{{-- In der Posts-Übersicht, beim einzelnen Post --}}

<small class="text-muted">
    {{ $post->created_at->diffForHumans() }} · {{ $post->comments_count }} Kommentare
</small>

Zusammenfassung: Rechte-Überblick

Aktion Admin Normaler User Gast
Posts lesen Ja Ja Ja
Posts erstellen Ja Nein Nein
Posts bearbeiten Ja Nein Nein
Posts löschen Ja Nein Nein
Kommentieren Ja Ja Nein
Eigene Kommentare löschen Ja Ja
Fremde Kommentare löschen Ja Nein
Konzept Was es macht Analogie
hasMany Ein Model hat viele andere Autor hat viele Bücher
belongsTo Ein Model gehört zu einem anderen Buch gehört einem Autor
Eager Loading Lädt verknüpfte Daten in einer Abfrage Einmal zum Kühlschrank
Policy Definiert Berechtigungen pro Model Hausrecht
@auth/@guest Blade-Check für Login-Status VIP-Bereich
withCount Zählt Relationen ohne sie zu laden Strichliste

Was wir in 4 Sessions gebaut haben

Session Thema Ergebnis
1 Routes, Controller, Views Startseite und About-Seite
2 Datenbank, Models, Auth, is_admin Posts aus der DB, User mit Rechten
3 Formulare, Validierung, CRUD Posts verwalten (nur Admins)
4 Relationen, Kommentare, Policies Kommentare mit Rechte-System

Der vollständige Flow:

Code
Browser → Route → Middleware (auth/admin) → Controller → Model (DB) → View → Browser

Wie es weitergeht

  • Testing: Schreibe Pest-Tests für deine Policies und CRUD-Operationen
  • Pagination: Post::latest()->paginate(10) statt get()
  • File Uploads: Bilder zu Posts hinzufügen
  • Rollen-System: spatie/laravel-permission für komplexere Rechte
  • API: JSON-Endpunkte mit Route::apiResource()

Laravel-Dokumentation: https://laravel.com/docs

Deine Aufgabe

  1. Teste das komplette Rechte-System:
  • Admin erstellt Post → funktioniert
  • Normaler User versucht Post zu erstellen → 403
  • Normaler User kommentiert → funktioniert
  • Normaler User löscht eigenen Kommentar → funktioniert
  • Normaler User löscht fremden Kommentar → 403
  • Admin löscht fremden Kommentar → funktioniert
  1. Bonus: Erweitere die Policy, damit der Admin auch Posts anderer Admins bearbeiten kann (geht schon, weil alle Admin-Routen die admin-Middleware nutzen — aber versuche es mit einer Post-Policy umzusetzen).

Cheatsheet

Relationen

Im Model definieren
// Im Model definieren
public function comments(): HasMany {
    return $this->hasMany(Comment::class);
}

public function post(): BelongsTo {
    return $this->belongsTo(Post::class);
}

// Nutzen
$post->comments;                        // Alle Kommentare
$comment->user->name;                   // Name des Autors
$post->comments()->create([...]);       // Kommentar erstellen
Post::withCount('comments')->get();     // Mit Anzahl laden

Auth & Rechte

php
auth()->check()                   // Eingeloggt?
auth()->user()                    // User-Objekt
auth()->id()                      // User-ID
auth()->user()->isAdmin()         // Admin?
$this->authorize('delete', $model); // Policy prüfen

Blade Auth

blade
@auth ... @endauth        // Nur für eingeloggte User
@guest ... @endguest      // Nur für Gäste

{{-- Rollencheck --}}
@if(auth()->user()?->isAdmin())
    {{-- Admin-Inhalt --}}
@endif