Laravel Entwicklungsmuster
Produktionsreife Laravel Architekturmuster für skalierbare, wartbare Anwendungen.
Wann verwenden
- Erstellen von Laravel Webanwendungen oder APIs
- Strukturieren von Controllern, Services und Domänenlogik
- Arbeiten mit Eloquent Modellen und Beziehungen
- Entwerfen von APIs mit Ressourcen und Paginierung
- Hinzufügen von Warteschlangen, Events, Caching und Hintergrundjobs
Wie es funktioniert
- Strukturieren Sie die App um klare Grenzen (Controller -> Services/Actions -> Modelle).
- Verwenden Sie explizite Bindungen und Scoped-Bindungen, um das Routing vorhersehbar zu halten; erzwingen Sie dennoch Autorisierung für die Zugriffskontrolle.
- Bevorzugen Sie typisierte Modelle, Casts und Scopes, um die Domänenlogik konsistent zu halten.
- Verlagern Sie IO-intensive Arbeiten in Warteschlangen und cachen Sie teure Lesevorgänge.
- Zentralisieren Sie die Konfiguration in
config/*und halten Sie Umgebungen explizit.
Beispiele
Projektstruktur
Verwenden Sie ein konventionelles Laravel-Layout mit klaren Schichtgrenzen (HTTP, Services/Actions, Modelle).
Empfohlenes Layout
app/
├── Actions/ # Einzweck-Anwendungsfälle
├── Console/
├── Events/
├── Exceptions/
├── Http/
│ ├── Controllers/
│ ├── Middleware/
│ ├── Requests/ # Form-Request-Validierung
│ └── Resources/ # API-Ressourcen
├── Jobs/
├── Models/
├── Policies/
├── Providers/
├── Services/ # Koordinierende Domänendienste
└── Support/
config/
database/
├── factories/
├── migrations/
└── seeders/
resources/
├── views/
└── lang/
routes/
├── api.php
├── web.php
└── console.php
Controller -> Services -> Actions
Halten Sie Controller schlank. Verlagern Sie Orchestrierung in Services und Einzwecklogik in Actions.
final class CreateOrderAction
{
public function __construct(private OrderRepository $orders) {}
public function handle(CreateOrderData $data): Order
{
return $this->orders->create($data);
}
}
final class OrdersController extends Controller
{
public function __construct(private CreateOrderAction $createOrder) {}
public function store(StoreOrderRequest $request): JsonResponse
{
$order = $this->createOrder->handle($request->toDto());
return response()->json([
'success' => true,
'data' => OrderResource::make($order),
'error' => null,
'meta' => null,
], 201);
}
}
Routing und Controller
Bevorzugen Sie Route-Model-Binding und Ressourcen-Controller für Klarheit.
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('projects', ProjectController::class);
});
Route Model Binding (Scoped)
Verwenden Sie Scoped-Bindungen, um mandantenübergreifenden Zugriff zu verhindern.
Route::scopeBindings()->group(function () {
Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
});
Verschachtelte Routen und Binding-Namen
- Halten Sie Präfixe und Pfade konsistent, um doppelte Verschachtelung zu vermeiden (z.B.
conversationvs.conversations). - Verwenden Sie einen einzelnen Parameternamen, der dem gebundenen Modell entspricht (z.B.
{conversation}fürConversation). - Bevorzugen Sie Scoped-Bindungen bei Verschachtelung, um Eltern-Kind-Beziehungen zu erzwingen.
use App\Http\Controllers\Api\ConversationController;
use App\Http\Controllers\Api\MessageController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->prefix('conversations')->group(function () {
Route::post('/', [ConversationController::class, 'store'])->name('conversations.store');
Route::scopeBindings()->group(function () {
Route::get('/{conversation}', [ConversationController::class, 'show'])
->name('conversations.show');
Route::post('/{conversation}/messages', [MessageController::class, 'store'])
->name('conversation-messages.store');
Route::get('/{conversation}/messages/{message}', [MessageController::class, 'show'])
->name('conversation-messages.show');
});
});
Wenn Sie möchten, dass ein Parameter zu einer anderen Modellklasse aufgelöst wird, definieren Sie eine explizite Bindung. Für benutzerdefinierte Bindungslogik verwenden Sie Route::bind() oder implementieren Sie resolveRouteBinding() im Modell.
use App\Models\AiConversation;
use Illuminate\Support\Facades\Route;
Route::model('conversation', AiConversation::class);
Service Container Bindungen
Binden Sie Schnittstellen an Implementierungen in einem Service-Provider für klare Abhängigkeitsverkabelung.
use App\Repositories\EloquentOrderRepository;
use App\Repositories\OrderRepository;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(OrderRepository::class, EloquentOrderRepository::class);
}
}
Eloquent Modell-Muster
Modellkonfiguration
final class Project extends Model
{
use HasFactory;
protected $fillable = ['name', 'owner_id', 'status'];
protected $casts = [
'status' => ProjectStatus::class,
'archived_at' => 'datetime',
];
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_id');
}
public function scopeActive(Builder $query): Builder
{
return $query->whereNull('archived_at');
}
}
Benutzerdefinierte Casts und Value Objects
Verwenden Sie Enums oder Value Objects für strenge Typisierung.
use Illuminate\Database\Eloquent\Casts\Attribute;
protected $casts = [
'status' => ProjectStatus::class,
];
protected function budgetCents(): Attribute
{
return Attribute::make(
get: fn (int $value) => Money::fromCents($value),
set: fn (Money $money) => $money->toCents(),
);
}
Eager Loading zur Vermeidung von N+1
$orders = Order::query()
->with(['customer', 'items.product'])
->latest()
->paginate(25);
Query-Objekte für komplexe Filter
final class ProjectQuery
{
public function __construct(private Builder $query) {}
public function ownedBy(int $userId): self
{
$query = clone $this->query;
return new self($query->where('owner_id', $userId));
}
public function active(): self
{
$query = clone $this->query;
return new self($query->whereNull('archived_at'));
}
public function builder(): Builder
{
return $this->query;
}
}
Global Scopes und Soft Deletes
Verwenden Sie Global Scopes für Standard-Filterung und SoftDeletes für wiederherstellbare Datensätze.
Verwenden Sie entweder einen Global Scope oder einen benannten Scope für denselben Filter, nicht beide, es sei denn, Sie beabsichtigen ein geschichtetes Verhalten.
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
use SoftDeletes;
protected static function booted(): void
{
static::addGlobalScope('active', function (Builder $builder): void {
$builder->whereNull('archived_at');
});
}
}
Query Scopes für wiederverwendbare Filter
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
public function scopeOwnedBy(Builder $query, int $userId): Builder
{
return $query->where('owner_id', $userId);
}
}
// In Service, Repository usw.
$projects = Project::ownedBy($user->id)->get();
Transaktionen für mehrschrittige Aktualisierungen
use Illuminate\Support\Facades\DB;
DB::transaction(function (): void {
$order->update(['status' => 'paid']);
$order->items()->update(['paid_at' => now()]);
});
Migrationen
Benennungskonvention
- Dateinamen verwenden Zeitstempel:
YYYY_MM_DD_HHMMSS_create_users_table.php - Migrationen verwenden anonyme Klassen (keine benannte Klasse); der Dateiname kommuniziert die Absicht
- Tabellennamen sind
snake_caseund standardmäßig plural
Beispiel-Migration
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::create('orders', function (Blueprint $table): void {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->string('status', 32)->index();
$table->unsignedInteger('total_cents');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
Form Requests und Validierung
Halten Sie die Validierung in Form Requests und transformieren Sie Eingaben in DTOs.
use App\Models\Order;
final class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can('create', Order::class) ?? false;
}
public function rules(): array
{
return [
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'items' => ['required', 'array', 'min:1'],
'items.*.sku' => ['required', 'string'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
];
}
public function toDto(): CreateOrderData
{
return new CreateOrderData(
customerId: (int) $this->validated('customer_id'),
items: $this->validated('items'),
);
}
}
API-Ressourcen
Halten Sie API-Antworten mit Ressourcen und Paginierung konsistent.
$projects = Project::query()->active()->paginate(25);
return response()->json([
'success' => true,
'data' => ProjectResource::collection($projects->items()),
'error' => null,
'meta' => [
'page' => $projects->currentPage(),
'per_page' => $projects->perPage(),
'total' => $projects->total(),
],
]);
Events, Jobs und Warteschlangen
- Emittieren Sie Domänen-Events für Nebeneffekte (E-Mails, Analysen)
- Verwenden Sie queued Jobs für langsame Arbeiten (Berichte, Exporte, Webhooks)
- Bevorzugen Sie idempotente Handler mit Wiederholungen und Backoff
Caching
- Cachen Sie leseintensive Endpunkte und teure Abfragen
- Invalidieren Sie Caches bei Modell-Events (created/updated/deleted)
- Verwenden Sie Tags beim Cachen verwandter Daten für einfache Invalidierung
Konfiguration und Umgebungen
- Halten Sie Geheimnisse in
.envund Konfiguration inconfig/*.php - Verwenden Sie umgebungsspezifische Konfigurationsüberschreibungen und
config:cachein der Produktion