← Back to Blog

Design Patterns in Practice: Template Method in Laravel

laravel design-patterns php

Design patterns are among the tools that many developers know in theory but rarely apply consciously in everyday work. Most of us learned them at some point — somewhere between university, bootcamps, and the "Clean Code" phase. But as soon as things need to move fast in practice, we often overlook where a pattern actually makes sense.

That's why today we're looking at a design pattern that is rarely consciously recognized but is extremely useful in everyday Laravel requirements:

How the Template Method pattern can be used to elegantly structure simple or complex export processes.

What Is the Template Method Design Pattern?

In short:

The Template Method pattern defines an algorithm in a base class and leaves certain steps to subclasses for individual implementation.

Or as refactoring.guru describes it:

A base method defines the workflow. Individual steps are protected / abstract and must be overridden in subclasses — the main method itself is never overridden.

In PHP, this typically looks like:

  • Abstract main class contains the complete workflow (handle(), execute(), export(), ...)
  • Varying steps are defined as protected abstract
  • Subclasses implement these steps — without changing the workflow

This lets you model reusable processes that have slightly different implementations depending on the use case.

Template Method Pattern
Template Method Pattern

A Practical Example: Structuring Export Functions Cleanly

A classic from real projects:

"We need an Excel export. And later maybe a second one. And a variant for HR. And one for Accounting..."

This quickly leads to copy-paste export classes that only differ in details. A perfect use case for the Template Method pattern.

The Abstract Base Class: The Export Process

The export process is defined once:

<?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;
}

What's happening here:

  • export() is the algorithm that defines the workflow
  • The variable parts (Filename, Header, Employees, Row) are abstract
  • Subclasses fill in these parts — without changing the workflow

That's exactly what Template Method is.

Export Variant 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 Variant 2 — Custom Extension

In practice, the differences are often:

  • Additional columns
  • Different sorting
  • Filters for specific roles/statuses
  • Alternative filenames
  • Different sources (e.g., 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 ?? '',
            // ...
        ];
    }
}

How the Export Is Called

As an example in a Livewire component:

<?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);
    }
}

What stands out:

  • The components don't need to know how the export works
  • Only the variant is selected
  • Each variant stays lean and focused

Conclusion

Why the Template Method Pattern Is a Perfect Fit Here

The Template Method pattern solves multiple problems at once:

Clean, defined workflow — The process lives once centrally in the base class.

Clear extensibility — New variants only need to override the methods that differ.

No copy-paste — The logic for the actual export never needs to be duplicated.

Interchangeability — Subclasses can easily be instantiated or registered via DI/macros/service container.

Perfect for Laravel — Laravel encourages structured, extensible architectures — Template Method fits right in.

Room to Grow

The current solution with a match block works well as a starting point. But the more export variants are added, the less appealing it becomes to maintain this selection manually. In the long run, you'll want to decouple the component and reduce the coupling.

There are several ways to make this more elegant and automated later — without having to modify the export classes themselves:

  • Strategy Pattern, to move the variant selection into its own well-testable structure.
  • Automatic DI configuration, where a service provider registers all available variants centrally.
  • Auto-discovery via PHP Attributes, so new export classes are found automatically as soon as they exist in the code.

These approaches make it possible to manage many different variants without additional maintenance effort. The entry point stays simple — and the architecture can grow with the project.