Session 2: Echte Daten
Posts kommen aus der Datenbank statt aus dem Code.
Was wir heute bauen
Am Ende dieser Session:
- Liegt eine
posts-Tabelle in der Datenbank - Werden Posts aus der DB geladen statt aus einem Array
- Gibt es eine Posts-Übersicht und eine Detailseite
- Gibt es Benutzer mit einer Unterscheidung zwischen Admin und normalem User
- Verstehst du Migrations, Models, Eloquent und Authentifizierung
Unser Ablauf erweitert sich:
Route → Controller → Model → View
Das Model ist die Küche in unserem Restaurant. Der Controller (Kellner) gibt die Bestellung weiter, das Model (Küche) holt die Zutaten (Daten) und bereitet sie zu. Die View (Teller) zeigt das Ergebnis.
Rückblick: Wo stehen wir?
Wir haben:
- Routen in
web.php - Einen
HomeControllermitindex()undabout() - Views mit Blade Components und ein Layout
- Posts als hardcoded Array
Was fehlt: Eine Datenbank, ein Post-Model, und Benutzer.
01Schritt 1: Was ist eine Migration?
Bevor du Daten speichern kannst, brauchst du eine Tabelle. Und bevor du eine Tabelle baust, brauchst du einen Bauplan. In Laravel heißt dieser Bauplan Migration.
Eine Migration ist eine PHP-Datei, die beschreibt, wie eine Tabelle aussehen soll: Welche Spalten hat sie? Welche Datentypen? Das ist wie ein Architekturplan — du zeichnest erst, baust dann.
Der Vorteil: Migrationen sind versioniert. Jeder im Team führt denselben Befehl aus und hat dieselbe Datenbankstruktur. Kein "bei mir funktioniert es aber".
Model und Migration gleichzeitig erstellen
php artisan make:model Post -m
Das -m Flag sagt: "Erstelle gleich eine Migration dazu." Der Befehl erzeugt zwei Dateien:
app/Models/Post.php— das Modeldatabase/migrations/xxxx_create_posts_table.php— die Migration
02Schritt 2: Die Migration definieren
Öffne die Migration (der Dateiname beginnt mit einem Datum). Du siehst:
// database/migrations/xxxx_create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
Das ist der Bauplan. id() erzeugt eine automatisch hochzählende ID. timestamps() fügt created_at und updated_at hinzu. Aber es fehlen die eigentlichen Felder. Ergänze title und content:
// database/migrations/xxxx_create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
Was bedeuten die Typen?
| Methode | SQL-Typ | Wofür |
|---|---|---|
id() |
BIGINT, auto-increment | Eindeutige ID |
string('title') |
VARCHAR(255) | Kurzer Text (Titel) |
text('content') |
TEXT | Langer Text (Inhalt) |
timestamps() |
TIMESTAMP | Erstellungs- und Änderungszeitpunkt |
Migration ausführen
php artisan migrate
Laravel liest alle Migrationen und erstellt die Tabellen in der Datenbank. Unsere Datenbank ist SQLite — eine einfache Datei unter database/database.sqlite. Kein externer Server nötig.
Probier es aus: Führe den Befehl aus. Du solltest eine Ausgabe sehen, die create_posts_table enthält. Prüfe mit:
php artisan migrate:status
Die Posts-Migration sollte als "Ran" markiert sein.
03Schritt 3: Das Model verstehen
Das Model ist deine Schnittstelle zur Datenbank. Statt SQL zu schreiben, arbeitest du mit PHP-Objekten. Laravel nennt dieses System Eloquent — ein ORM (Object-Relational Mapper).
Stell dir Eloquent als Dolmetscher vor: Du sprichst PHP, die Datenbank spricht SQL. Eloquent übersetzt in beide Richtungen.
Öffne app/Models/Post.php:
// app/Models/Post.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'content'];
}
Laravel leitet aus dem Klassennamen Post den Tabellennamen posts ab (Plural, kleingeschrieben). $fillable definiert, welche Felder per Massenzuweisung befüllt werden dürfen. Das ist eine Sicherheitsmaßnahme: Ohne diese Liste könnte ein Angreifer beliebige Felder setzen. Laravel blockt alles, was nicht in $fillable steht.
04Schritt 4: Testdaten einfügen
Eine leere Datenbank hilft uns nicht. Wir brauchen Posts. Dafür gibt es zwei Wege:
Weg 1: Tinker (schnell, zum Ausprobieren)
Tinker ist eine interaktive PHP-Konsole:
php artisan tinker
Darin:
use App\Models\Post;
Post::create(['title' => 'Mein erster Post', 'content' => 'Dieser Post kommt aus der Datenbank.']);
Post::create(['title' => 'Laravel ist elegant', 'content' => 'Eloquent macht Datenbankabfragen lesbar und sicher.']);
Post::create(['title' => 'Bootstrap fürs Styling', 'content' => 'Unser Blog nutzt Bootstrap 5 für ein sauberes Layout.']);
Tippe exit, um Tinker zu verlassen.
Weg 2: Seeder (wiederholbar, für Teams)
php artisan make:seeder PostSeeder
// database/seeders/PostSeeder.php
<?php
namespace Database\Seeders;
use App\Models\Post;
use Illuminate\Database\Seeder;
class PostSeeder extends Seeder
{
public function run(): void
{
Post::create([
'title' => 'Willkommen auf dem Blog',
'content' => 'Das ist der erste Post. Er wurde automatisch vom Seeder erstellt.',
]);
Post::create([
'title' => 'Wie Eloquent funktioniert',
'content' => 'Eloquent ist Laravels ORM. Es übersetzt zwischen PHP-Objekten und Datenbanktabellen.',
]);
Post::create([
'title' => 'Migrationen erklärt',
'content' => 'Eine Migration ist ein versionierter Bauplan für deine Datenbank.',
]);
}
}
Ausführen:
php artisan db:seed --class=PostSeeder
05Schritt 5: Controller umbauen
Jetzt ersetzen wir das hardcoded Array durch eine Datenbankabfrage:
// app/Http/Controllers/HomeController.php
<?php
namespace App\Http\Controllers;
use App\Models\Post;
class HomeController extends Controller
{
public function index()
{
$posts = Post::all();
return view('home', compact('posts'));
}
public function about()
{
return view('about');
}
}
View anpassen
Da wir jetzt mit Objekten statt Arrays arbeiten, ändert sich der Zugriff:
{{-- resources/views/home.blade.php --}}
<x-app-layout>
<div class="container py-4">
<h1>Willkommen auf meinem Blog</h1>
<p class="lead">Aktuelle Beiträge:</p>
<div class="row mt-4">
@foreach($posts as $post)
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ $post->title }}</h5>
<p class="card-text">{{ Str::limit($post->content, 100) }}</p>
<a href="/posts/{{ $post->id }}" class="btn btn-primary btn-sm">Weiterlesen</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
</x-app-layout>
$post->title statt $post['title'] — Objektzugriff statt Array. Str::limit() kürzt den Text.
Probier es aus: Lade die Startseite neu. Die Posts kommen jetzt aus der Datenbank. Füge in Tinker einen vierten Post hinzu und lade nochmal — er erscheint sofort.
06Schritt 6: Detailseite mit Route Model Binding
PostController erstellen
php artisan make:controller PostController
// app/Http/Controllers/PostController.php
<?php
namespace App\Http\Controllers;
use App\Models\Post;
class PostController extends Controller
{
public function show(Post $post)
{
return view('posts.show', compact('post'));
}
public function index()
{
$posts = Post::latest()->get();
return view('posts.index', compact('posts'));
}
}
Der Parameter Post $post in show() ist Route Model Binding. Laravel nimmt die ID aus der URL, sucht den Post in der Datenbank und übergibt ihn als Objekt. Existiert der Post nicht, zeigt Laravel automatisch eine 404-Seite.
Routen
// routes/web.php
use App\Http\Controllers\PostController;
Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{post}', [PostController::class, 'show']);
Detail-View
{{-- resources/views/posts/show.blade.php --}}
<x-app-layout>
<div class="container py-4">
<article>
<h1>{{ $post->title }}</h1>
<p class="text-muted">
Erstellt am {{ $post->created_at->format('d.m.Y H:i') }}
</p>
<div class="mt-4">
<p>{{ $post->content }}</p>
</div>
</article>
<a href="/posts" class="btn btn-secondary mt-4">Zurück zur Übersicht</a>
</div>
</x-app-layout>
Posts-Index-View
{{-- resources/views/posts/index.blade.php --}}
<x-app-layout>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Alle Posts</h1>
<span class="badge bg-primary">{{ $posts->count() }} Posts</span>
</div>
@forelse($posts as $post)
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ $post->title }}</h5>
<p class="card-text">{{ Str::limit($post->content, 150) }}</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">{{ $post->created_at->diffForHumans() }}</small>
<a href="/posts/{{ $post->id }}" class="btn btn-outline-primary btn-sm">Weiterlesen</a>
</div>
</div>
</div>
@empty
<div class="alert alert-info">Noch keine Posts vorhanden.</div>
@endforelse
</div>
</x-app-layout>
Probier es aus: Öffne /posts und klicke auf "Weiterlesen". Die Detailseite zeigt den vollständigen Post.
07Schritt 7: Benutzer und Authentifizierung
Bisher kann jeder alles. Das ändern wir jetzt. Laravel Breeze ist bereits installiert und bringt Login, Registrierung und ein Dashboard mit.
Probier es aus: Öffne /register und erstelle einen Account. Logge dich ein. Du siehst ein Dashboard.
Auth im Code
auth()->check() // Ist jemand eingeloggt?
auth()->user() // Der aktuelle User
auth()->id() // Die User-ID
is_admin: Einfaches Rechtemanagement
Wir unterscheiden zwei Rollen: Admin (darf Posts verwalten) und normaler User (darf kommentieren). Dafür reicht ein einzelnes Feld in der users-Tabelle.
Erstelle eine Migration:
php artisan make:migration add_is_admin_to_users_table
// database/migrations/xxxx_add_is_admin_to_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false)->after('email');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
};
php artisan migrate
User-Model erweitern
Das User-Model existiert bereits unter app/Models/User.php. Füge eine Helper-Methode hinzu:
// app/Models/User.php — innerhalb der Klasse ergänzen
public function isAdmin(): bool
{
return $this->is_admin === true;
}
Einen Admin-User anlegen
In Tinker:
php artisan tinker
use App\Models\User;
$user = User::where('email', 'deine@email.de')->first();
$user->is_admin = true;
$user->save();
Oder direkt beim Registrieren über Tinker:
User::create([
'name' => 'Admin',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
'is_admin' => true,
]);
Probier es aus: Öffne Tinker und setze deinen User auf Admin. Prüfe mit $user->isAdmin() — es sollte true zurückkommen.
08Schritt 8: Routen schützen
Jetzt nutzen wir die Auth-Middleware, um Routen zu schützen:
// routes/web.php
use App\Http\Controllers\HomeController;
use App\Http\Controllers\PostController;
// Öffentlich — jeder darf lesen
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 eingeloggte User
Route::middleware('auth')->group(function () {
// Post-Verwaltung kommt in Session 3
});
middleware('auth') ist eine Sicherheitsschleuse. Nicht eingeloggte Besucher werden automatisch zur Login-Seite weitergeleitet.
In Session 3 füllen wir die geschützte Gruppe mit den CRUD-Routen für Posts — und prüfen dort zusätzlich, ob der User ein Admin ist.
Zusammenfassung
| Konzept | Was es macht | Analogie |
|---|---|---|
| Migration | Definiert die Tabellenstruktur | Bauplan eines Gebäudes |
| Model | PHP-Schnittstelle zur Datenbank | Die Küche |
| Eloquent | ORM — übersetzt PHP zu SQL | Dolmetscher |
| $fillable | Erlaubte Felder für Massenzuweisung | Sicherheitsliste am Eingang |
| Route Model Binding | Automatisches Laden per URL-ID | Der Kellner weiß welcher Tisch bestellt hat |
| is_admin | Einfaches Rechtemanagement | VIP-Ausweis |
| Middleware | Prüft Anfragen vor der Verarbeitung | Sicherheitsschleuse |
Der erweiterte Flow:
Browser → Route → Middleware → Controller → Model (DB) → View → Browser
Deine Aufgabe
Füge ein Feld excerpt (Kurzbeschreibung) zur Posts-Tabelle hinzu:
- Erstelle eine neue Migration:
php artisan make:migration add_excerpt_to_posts_table - Definiere das Feld:
$table->string('excerpt')->nullable()->after('title'); - Führe die Migration aus:
php artisan migrate - Aktualisiere
$fillableim Model:['title', 'excerpt', 'content'] - Nutze
$post->excerptin der View stattStr::limit()
Cheatsheet
Artisan
php artisan make:model Post -m # Model + Migration
php artisan migrate # Migrationen ausführen
php artisan migrate:status # Status anzeigen
php artisan make:migration add_feld_to_table # Einzelne Migration
php artisan make:seeder PostSeeder # Seeder erstellen
php artisan db:seed --class=PostSeeder # Seeder ausführen
php artisan tinker # Interaktive Konsole
Eloquent
| Methode | Bedeutung |
|---|---|
Post::all() |
Alle Posts holen |
Post::find(1) |
Post mit ID 1 |
Post::latest()->get() |
Neueste zuerst |
Post::create([...]) |
Neuen Post erstellen |
Auth
auth()->check() // Eingeloggt?
auth()->user() // User-Objekt
auth()->id() // User-ID
auth()->user()->isAdmin() // Ist Admin?