← Zurück zum Blog

Design Patterns in Practice: Template Method in Laravel

laravel design-patterns php

Design Patterns gehören zu den Werkzeugen, die viele Entwickler theoretisch kennen, aber im Alltag selten bewusst anwenden. Die meisten von uns haben sie irgendwann einmal gelernt – irgendwo zwischen Uni, Bootcamp und "Clean Code"-Phase. Doch sobald es in der Praxis schnell gehen muss, übersieht man oft, wo ein Pattern tatsächlich Sinn ergibt.

Deshalb schauen wir uns heute ein Design Pattern an, das zwar selten bewusst wahrgenommen wird, aber in alltäglichen Laravel-Anforderungen extrem nützlich ist:

Wie das Template Method Pattern genutzt werden kann, um einfache oder komplexe Export-Prozesse elegant zu strukturieren.

Was ist das Template Method Design Pattern?

Kurz gesagt:

Das Template Method Pattern definiert einen Algorithmus in einer Basisklasse und überlässt Subklassen bestimmte Schritte zur individuellen Ausgestaltung.

Oder wie es refactoring.guru beschreibt:

Eine Basismethode legt den Ablauf fest. Einzelne Schritte sind protected / abstract und müssen in Subklassen überschrieben werden – die Hauptmethode selbst wird dagegen nie überschrieben.

In PHP sieht das typischerweise so aus:

  • Abstrakte Hauptklasse enthält den kompletten Ablauf (handle(), execute(), export(), ...)
  • Variierende Schritte sind protected abstract definiert
  • Subklassen implementieren diese Schritte – ohne den Ablauf zu verändern

Damit kann man wiederverwendbare Prozesse modellieren, die je nach Umsetzung leicht unterschiedliche Ausprägungen haben.

Template Method Pattern
Template Method Pattern

Ein Praxisbeispiel: Export-Funktionen sauber strukturieren

Ein Klassiker aus echten Projekten:

"Wir brauchen einen Excel-Export. Und später vielleicht noch einen zweiten. Und eine Variante für HR. Und eine für Accounting..."

Das führt schnell zu Copy-Paste-Exportklassen, die sich nur in Details unterscheiden. Perfekter Einsatzzweck für das Template Method Pattern.

Die abstrakte Basisklasse: Der Export-Prozess

Der Export-Prozess wird einmal definiert:

<?php

namespace App\Exports\Employees;

use App\Models\Employee;
use Illuminate\Database\Eloquent\Collection;
use OpenSpout\Common\Entity\Style\Style;
use Spatie\SimpleExcel\SimpleExcelWriter;
use Symfony\Component\HttpFoundation\StreamedResponse;

abstract class BaseEmployeeExport
{
    public function export(array $ids): StreamedResponse
    {
        $filename = $this->getFilename();

        $writer = SimpleExcelWriter::streamDownload($filename)
            ->addHeader($this->getHeader());

        $this->getEmployees($ids)->each(fn(Employee $employee) =>
            $writer->addRow($this->getRow($employee))
        );

        return response()->streamDownload(function() use ($writer) {
            $writer->close();
        }, $filename);
    }

    abstract protected function getFilename(): string;

    abstract protected function getHeader(): array;

    abstract protected function getEmployees(array $ids): Collection;

    abstract protected function getRow(Employee $employee): array;
}

Was hier passiert:

  • export() ist der Algorithmus, der den Ablauf vorgibt
  • Die variablen Teile (Filename, Header, Employees, Row) sind abstrakt
  • Subklassen füllen diese Stellen aus – ohne den Ablauf zu verändern

Genau das ist Template Method.

Export-Variante 1 – Standard-Export

<?php

namespace App\Exports\Employees;

use App\Models\Employee;
use Illuminate\Database\Eloquent\Collection;

class StandardEmployeeExport extends BaseEmployeeExport
{
    protected function getFilename(): string
    {
        return 'Employee_Export_'.date('Y-m-d_H-i-s').'.xlsx';
    }

    protected function getEmployees(array $ids): Collection
    {
        return Employee::whereIn('id', $ids)
            ->orderBy('firstname')
            ->get();
    }

    protected function getHeader(): array
    {
        return [
            trans('labels.firstname'),
            //...
        ];
    }

    protected function getRow(Employee $employee): array
    {
        return [
            $employee->firstname,
            //...
        ];
    }
}

Export-Variante 2 – beliebige Erweiterung

In der Praxis unterscheidet sich hier oft:

  • zusätzliche Spalten
  • andere Sortierung
  • Filter auf bestimmte Rollen/Status
  • alternative Dateinamen
  • andere Quellen (z. B. HR-Export vs. Finance-Export)
<?php

namespace App\Exports\Employees;

use App\Models\Employee;
use Illuminate\Database\Eloquent\Collection;

class ExtendedEmployeeExport extends BaseEmployeeExport
{
    protected function getFilename(): string
    {
        return 'Employee_Export_Extended_'.date('Y-m-d_H-i-s').'.xlsx';
    }

    protected function getEmployees(array $ids): Collection
    {
        return Employee::whereIn('id', $ids)
            ->with('department')
            ->orderBy('lastname')
            ->get();
    }

    protected function getHeader(): array
    {
        return [
            trans('labels.firstname'),
            trans('labels.lastname'),
            trans('labels.department'),
            // ...
        ];
    }

    protected function getRow(Employee $employee): array
    {
        return [
            $employee->firstname,
            $employee->lastname,
            $employee->department->name ?? '',
            // ...
        ];
    }
}

Wie der Export aufgerufen wird

Beispielhaft in einer Livewire-Komponente:

<?php

class EmployeeExport extends Component
{
    public function export(string $variant): StreamedResponse
    {
        $export = match($variant) {
            "extended" => new ExtendedEmployeeExport(),
            "..." => new ....EmployeeExport(),
            default => new StandardEmployeeExport()
        };

        return $export->export($this->selectedIds);
    }
}

Was hier auffällt:

  • Die Komponenten müssen nicht wissen, wie der Export funktioniert
  • Nur die Variante wird ausgewählt
  • Jede Variante bleibt schlank und fokussiert

Fazit

Warum das Template Method Pattern hier perfekt passt

Das Template Method Pattern löst mehrere Probleme gleichzeitig:

Sauber definierter Ablauf — Der Prozess liegt einmal zentral in der Basisklasse.

Klare Erweiterbarkeit — Neue Varianten brauchen nur die Methoden zu überschreiben, die sich unterscheiden.

Kein Copy-Paste — Die Logik für den eigentlichen Export muss nie dupliziert werden.

Austauschbarkeit — Subklassen lassen sich leicht instanziieren oder über DI/Macros/Service Container registrieren.

Perfekt für Laravel — Laravel fördert strukturierte, ausbaufähige Architekturen – Template Method fügt sich nahtlos ein.

Potenzial zum Erweitern

Die bisherige Lösung mit einem match-Block funktioniert gut für den Einstieg. Doch je mehr Export-Varianten hinzukommen, desto unattraktiver wird es, diese Auswahl manuell zu pflegen. Langfristig möchte man die Komponente entlasten und die Kopplung reduzieren.

Es gibt mehrere Wege, wie man das später eleganter und deutlich automatischer gestalten kann – ohne die Exportklassen selbst ändern zu müssen:

  • Strategy Pattern, um die Variantenauswahl in eine eigene, gut testbare Struktur auszulagern.
  • Automatische DI-Konfiguration, bei der ein Service Provider alle verfügbaren Varianten zentral registriert.
  • Auto-Discovery über PHP Attributes, sodass neue Exportklassen automatisch gefunden werden, sobald sie im Code existieren.

Diese Ansätze ermöglichen es, viele unterschiedliche Varianten ohne zusätzlichen Pflegeaufwand zu verwalten. Der Einstieg bleibt einfach – und die Architektur kann mit dem Projekt wachsen.