Laravel 11: Eine vollständige Anwendung von Grund auf erstellen

Umfassender Leitfaden zu Laravel 11: Installation, Eloquent-Modelle, Authentifizierung mit Breeze, Controller, Routen, Autorisierung, REST-API mit Sanctum, Tests mit Pest und Produktions-Deployment.

Laravel 11 Tutorial: Vollständige Anwendung von Grund auf erstellen

Laravel 11 bringt eine schlankere Projektstruktur, native Health-Checks und eine überarbeitete Konfiguration mit. Dieser Leitfaden führt durch den gesamten Prozess: von der Installation bis zum produktionsreifen Deployment einer Task-Management-Anwendung.

Neuerungen in Laravel 11

Laravel 11 entfernt zahlreiche Boilerplate-Dateien (Http-Kernel, Console-Kernel, Middleware-Verzeichnis) und führt eine bootstrap/app.php-basierte Konfiguration ein. Health-Check-Routen, Graceful Encryption Key Rotation und ein neues Starter-Kit-Ökosystem gehören ebenfalls zu den Highlights.

Installation und Projekt-Setup

Der Laravel-Installer erstellt das Projektgerüst in Sekunden. Voraussetzungen: PHP 8.2+ und Composer.

bash
# Install Laravel installer globally
composer global require laravel/installer

# Create a new Laravel 11 project
laravel new taskmanager

# Navigate to the project
cd taskmanager

Die .env-Datei konfiguriert die Datenbankverbindung. Laravel 11 verwendet standardmäßig SQLite, für Produktionsumgebungen empfiehlt sich jedoch PostgreSQL oder MySQL.

env
# .env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=taskmanager
DB_USERNAME=postgres
DB_PASSWORD=secret

Nach der Konfiguration wird die initiale Migration ausgeführt und der Entwicklungsserver gestartet.

bash
# Run initial migrations
php artisan migrate

# Start the development server
php artisan serve

Die Anwendung ist nun unter http://localhost:8000 erreichbar.

Modelle und Migrationen erstellen

Eloquent-Modelle bilden das Herzstück jeder Laravel-Anwendung. Der folgende Befehl erstellt Modell, Migration, Factory und Seeder gleichzeitig.

bash
# Create model with migration, factory, and seeder
php artisan make:model Task -mfs

Die Migration definiert die Tabellenstruktur. Bemerkenswert: softDeletes() ermöglicht das logische Löschen ohne Datenverlust.

database/migrations/xxxx_create_tasks_table.phpphp
public function up(): void
{
    Schema::create('tasks', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->string('title');
        $table->text('description')->nullable();
        $table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
        $table->enum('priority', ['low', 'medium', 'high'])->default('medium');
        $table->date('due_date')->nullable();
        $table->timestamps();
        $table->softDeletes();
    });
}

Das Eloquent-Modell definiert ausfüllbare Felder, Casts und die Beziehung zum User.

app/Models/Task.phpphp
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Task extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'title',
        'description',
        'status',
        'priority',
        'due_date',
        'user_id',
    ];

    protected $casts = [
        'due_date' => 'date',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}
Soft Deletes in der Praxis

Soft Deletes markieren Datensätze als gelöscht, ohne sie physisch aus der Datenbank zu entfernen. Das ist besonders nützlich für Audit-Trails und Datenwiederherstellung. Mit Task::withTrashed() lassen sich auch gelöschte Einträge abfragen.

Authentifizierung mit Breeze einrichten

Laravel Breeze liefert eine schlanke Authentifizierungslösung mit Login, Registrierung, Passwort-Reset und E-Mail-Verifizierung.

bash
# Install Breeze
composer require laravel/breeze --dev

# Scaffold with Blade stack
php artisan breeze:install blade

# Install frontend dependencies and build
npm install && npm run build

# Run migrations for auth tables
php artisan migrate

Das User-Modell wird um die hasMany-Beziehung zu Tasks erweitert.

app/Models/User.phpphp
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

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

Bereit für deine Laravel-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Controller und Routen erstellen

Ein Resourceful Controller implementiert alle CRUD-Operationen. Laravel 11 nutzt die vereinfachte Routendeklaration in routes/web.php.

app/Http/Controllers/TaskController.phpphp
namespace App\Http\Controllers;

use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;

class TaskController extends Controller
{
    public function index(): View
    {
        $tasks = Auth::user()->tasks()
            ->orderBy('due_date')
            ->paginate(10);

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

    public function create(): View
    {
        return view('tasks.create');
    }

    public function store(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'in:pending,in_progress,completed',
            'priority' => 'in:low,medium,high',
            'due_date' => 'nullable|date|after:today',
        ]);

        Auth::user()->tasks()->create($validated);

        return redirect()->route('tasks.index')
            ->with('success', 'Task created successfully.');
    }

    public function edit(Task $task): View
    {
        $this->authorize('update', $task);

        return view('tasks.edit', compact('task'));
    }

    public function update(Request $request, Task $task): RedirectResponse
    {
        $this->authorize('update', $task);

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'in:pending,in_progress,completed',
            'priority' => 'in:low,medium,high',
            'due_date' => 'nullable|date',
        ]);

        $task->update($validated);

        return redirect()->route('tasks.index')
            ->with('success', 'Task updated successfully.');
    }

    public function destroy(Task $task): RedirectResponse
    {
        $this->authorize('delete', $task);

        $task->delete();

        return redirect()->route('tasks.index')
            ->with('success', 'Task deleted successfully.');
    }
}

Die Routen werden als Resource-Route registriert, geschützt durch die auth-Middleware.

routes/web.phpphp
use App\Http\Controllers\TaskController;

Route::middleware(['auth', 'verified'])->group(function () {
    Route::resource('tasks', TaskController::class)
        ->except(['show']);
});

Autorisierung mit Policies

Policies kapseln die Autorisierungslogik und stellen sicher, dass Benutzer nur eigene Tasks bearbeiten können.

bash
# Generate a policy for the Task model
php artisan make:policy TaskPolicy --model=Task

Die Policy prüft, ob der authentifizierte Benutzer der Eigentümer der Task ist.

app/Policies/TaskPolicy.phpphp
namespace App\Policies;

use App\Models\Task;
use App\Models\User;

class TaskPolicy
{
    public function update(User $user, Task $task): bool
    {
        return $user->id === $task->user_id;
    }

    public function delete(User $user, Task $task): bool
    {
        return $user->id === $task->user_id;
    }
}
Sicherheit bei Policies

Policies sollten in jeder Controller-Methode aufgerufen werden, die Daten verändert. Ohne Autorisierungsprüfung könnten Benutzer durch Manipulation der URL fremde Datensätze bearbeiten oder löschen. Die Methode $this->authorize() wirft automatisch eine 403-Exception, wenn die Prüfung fehlschlägt.

REST-API mit Sanctum erstellen

Laravel Sanctum bietet eine leichtgewichtige Authentifizierung für SPAs und mobile Apps. Ab Laravel 11 ist die API-Konfiguration mit einem einzigen Befehl erledigt.

bash
# Install API support (includes Sanctum)
php artisan install:api

Der API-Controller gibt JSON-Responses über API Resources zurück, die die Datenstruktur kontrollieren.

app/Http/Controllers/Api/TaskController.phpphp
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\TaskResource;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class TaskController extends Controller
{
    public function index(Request $request): AnonymousResourceCollection
    {
        $tasks = $request->user()->tasks()
            ->orderBy('created_at', 'desc')
            ->paginate(15);

        return TaskResource::collection($tasks);
    }

    public function store(Request $request): TaskResource
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'in:pending,in_progress,completed',
            'priority' => 'in:low,medium,high',
            'due_date' => 'nullable|date|after:today',
        ]);

        $task = $request->user()->tasks()->create($validated);

        return new TaskResource($task);
    }

    public function update(Request $request, Task $task): TaskResource
    {
        $this->authorize('update', $task);

        $validated = $request->validate([
            'title' => 'sometimes|required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'in:pending,in_progress,completed',
            'priority' => 'in:low,medium,high',
            'due_date' => 'nullable|date',
        ]);

        $task->update($validated);

        return new TaskResource($task);
    }

    public function destroy(Task $task)
    {
        $this->authorize('delete', $task);

        $task->delete();

        return response()->json(['message' => 'Task deleted'], 200);
    }
}

Die API Resource definiert, welche Felder in der JSON-Antwort enthalten sind.

app/Http/Resources/TaskResource.phpphp
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class TaskResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'description' => $this->description,
            'status' => $this->status,
            'priority' => $this->priority,
            'due_date' => $this->due_date?->format('Y-m-d'),
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
        ];
    }
}

Die API-Routen werden durch Sanctums auth:sanctum-Middleware geschützt.

routes/api.phpphp
use App\Http\Controllers\Api\TaskController;

Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('tasks', TaskController::class);
});

Automatisierte Tests mit Pest

Pest bietet eine elegante Syntax für PHP-Tests. Die folgenden Tests decken die CRUD-Operationen und Autorisierung ab.

tests/Feature/TaskTest.phpphp
use App\Models\Task;
use App\Models\User;

beforeEach(function () {
    $this->user = User::factory()->create();
});

test('user can create a task', function () {
    $response = $this->actingAs($this->user)->post('/tasks', [
        'title' => 'Test Task',
        'description' => 'Test description',
        'priority' => 'high',
    ]);

    $response->assertRedirect('/tasks');
    $this->assertDatabaseHas('tasks', [
        'title' => 'Test Task',
        'user_id' => $this->user->id,
    ]);
});

test('user can update own task', function () {
    $task = Task::factory()->create(['user_id' => $this->user->id]);

    $response = $this->actingAs($this->user)->put("/tasks/{$task->id}", [
        'title' => 'Updated Task',
        'status' => 'completed',
        'priority' => 'low',
    ]);

    $response->assertRedirect('/tasks');
    expect($task->fresh()->title)->toBe('Updated Task');
});

test('user cannot update another users task', function () {
    $otherUser = User::factory()->create();
    $task = Task::factory()->create(['user_id' => $otherUser->id]);

    $response = $this->actingAs($this->user)->put("/tasks/{$task->id}", [
        'title' => 'Hacked Task',
    ]);

    $response->assertForbidden();
});

test('user can delete own task', function () {
    $task = Task::factory()->create(['user_id' => $this->user->id]);

    $response = $this->actingAs($this->user)->delete("/tasks/{$task->id}");

    $response->assertRedirect('/tasks');
    $this->assertSoftDeleted('tasks', ['id' => $task->id]);
});

test('api returns paginated tasks', function () {
    Task::factory(20)->create(['user_id' => $this->user->id]);

    $response = $this->actingAs($this->user)->getJson('/api/tasks');

    $response->assertOk()
        ->assertJsonCount(15, 'data')
        ->assertJsonStructure([
            'data' => [['id', 'title', 'status', 'priority']],
            'meta' => ['current_page', 'last_page'],
        ]);
});

Die Tests werden mit einem einzigen Befehl ausgeführt.

bash
# Run all tests
php artisan test

# Run with coverage
php artisan test --coverage

Produktionsoptimierungen

Vor dem Deployment optimieren die folgenden Cache-Befehle die Performance erheblich.

bash
# Cache configuration, routes, and views
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Optimize autoloader
composer install --optimize-autoloader --no-dev

Die .env.production enthält produktionsspezifische Einstellungen.

env
# .env.production
APP_ENV=production
APP_DEBUG=false
APP_URL=https://taskmanager.example.com

LOG_CHANNEL=stack
LOG_LEVEL=error

CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
Sensible Umgebungsvariablen

Die .env-Datei darf niemals in die Versionskontrolle eingecheckt werden. APP_KEY, Datenbankpasswörter und API-Schlüssel sollten über sichere Umgebungsvariablen des Hosting-Providers konfiguriert werden. Der Befehl php artisan env:encrypt verschlüsselt die Datei für sichere Deployments.

Fazit

Diese Anleitung hat die wesentlichen Bausteine einer Laravel-11-Anwendung behandelt:

  • Projekt-Setup: Laravel-Installer, Umgebungskonfiguration und initiale Migrationen
  • Eloquent-Modelle: Relationen, Casts, Soft Deletes und Factories für strukturierte Datenhaltung
  • Authentifizierung: Breeze liefert Login, Registrierung und Passwort-Reset in wenigen Minuten
  • Controller und Routen: Resource-Controller und Middleware-geschützte Route-Gruppen
  • Autorisierung: Policies stellen sicher, dass Benutzer nur eigene Ressourcen verwalten
  • REST-API: Sanctum-Tokens und API Resources für mobile und SPA-Clients
  • Tests: Pest-Tests decken CRUD-Operationen und Autorisierungsregeln ab
  • Production: Caching, Autoloader-Optimierung und sichere Umgebungskonfiguration

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#laravel
#php
#rest api
#eloquent
#tutorial

Teilen

Verwandte Artikel