Start Prinzipien Session 1 Session 2 Session 3 Session 4

Session 2: Echte Daten

Ziel dieser Session

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 HomeController mit index() und about()
  • 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

Terminal
php artisan make:model Post -m

Das -m Flag sagt: "Erstelle gleich eine Migration dazu." Der Befehl erzeugt zwei Dateien:

  1. app/Models/Post.php — das Model
  2. database/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
// 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
// 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

Terminal
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:

Terminal
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
// 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:

Terminal
php artisan tinker

Darin:

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

Terminal
php artisan make:seeder PostSeeder
database/seeders/PostSeeder.php
// 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:

Terminal
php artisan db:seed --class=PostSeeder

05Schritt 5: Controller umbauen

Jetzt ersetzen wir das hardcoded Array durch eine Datenbankabfrage:

app/Http/Controllers/HomeController.php
// 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
{{-- 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

Terminal
php artisan make:controller PostController
app/Http/Controllers/PostController.php
// 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
// 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
{{-- 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
{{-- 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

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

Terminal
php artisan make:migration add_is_admin_to_users_table
database/migrations/xxxx_add_is_admin_to_users_table.php
// 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');
        });
    }
};
Terminal
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
// app/Models/User.php — innerhalb der Klasse ergänzen

public function isAdmin(): bool
{
    return $this->is_admin === true;
}

Einen Admin-User anlegen

In Tinker:

Terminal
php artisan tinker
php
use App\Models\User;

$user = User::where('email', 'deine@email.de')->first();
$user->is_admin = true;
$user->save();

Oder direkt beim Registrieren über Tinker:

php
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
// 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.

09Schritt 9: Navigation erweitern

Füge den Posts-Link zum Layout hinzu. Das bestehende Layout liegt in resources/views/layouts/navigation.blade.php:

In der Navigation ergänzen
{{-- In der Navigation ergänzen --}}

<a class="nav-link" href="/posts">Posts</a>

Die genaue Stelle hängt davon ab, wie Breeze die Navigation aufgebaut hat. Suche nach den bestehenden Links und füge deinen daneben ein.

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:

Code
Browser → Route → Middleware → Controller → Model (DB) → View → Browser

Deine Aufgabe

Füge ein Feld excerpt (Kurzbeschreibung) zur Posts-Tabelle hinzu:

  1. Erstelle eine neue Migration: php artisan make:migration add_excerpt_to_posts_table
  2. Definiere das Feld: $table->string('excerpt')->nullable()->after('title');
  3. Führe die Migration aus: php artisan migrate
  4. Aktualisiere $fillable im Model: ['title', 'excerpt', 'content']
  5. Nutze $post->excerpt in der View statt Str::limit()

Cheatsheet

Artisan

bash
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

php
auth()->check()        // Eingeloggt?
auth()->user()         // User-Objekt
auth()->id()           // User-ID
auth()->user()->isAdmin()  // Ist Admin?