Session 4: Kommentare und Benutzer
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.
php artisan make:model Comment -m
Migration definieren
// 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 Spaltepost_idals Fremdschlüsselconstrained()— sagt: Diese ID muss in derposts-Tabelle existierenonDelete('cascade')— wenn ein Post gelöscht wird, werden seine Kommentare automatisch mitgelöscht
php artisan migrate
Comment-Model
// 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
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:
$post = Post::find(1);
$comments = $post->comments; // Alle Kommentare
$count = $post->comments->count(); // Anzahl
Und umgekehrt:
$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
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 --}}
<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?')">
×
</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:
@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 --}}
@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
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
php artisan make:controller CommentController
// 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).
php artisan make:policy CommentPolicy --model=Comment
// 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:
- Bist du der Autor? Dann darfst du löschen.
- Bist du Admin? Dann darfst du auch löschen.
- 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:
- Erstelle zwei Accounts (einen Admin, einen normalen User)
- Schreibe als normaler User einen Kommentar
- Logge dich als der andere normale User ein — du siehst keinen Lösch-Button beim fremden Kommentar
- 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
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 --}}
<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:
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)stattget() - File Uploads: Bilder zu Posts hinzufügen
- Rollen-System:
spatie/laravel-permissionfür komplexere Rechte - API: JSON-Endpunkte mit
Route::apiResource()
Laravel-Dokumentation: https://laravel.com/docs
Deine Aufgabe
- 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
- 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
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
auth()->check() // Eingeloggt?
auth()->user() // User-Objekt
auth()->id() // User-ID
auth()->user()->isAdmin() // Admin?
$this->authorize('delete', $model); // Policy prüfen
Blade Auth
@auth ... @endauth // Nur für eingeloggte User
@guest ... @endguest // Nur für Gäste
{{-- Rollencheck --}}
@if(auth()->user()?->isAdmin())
{{-- Admin-Inhalt --}}
@endif