0

Template Method — skeleton algorithm in a base class

Intermediate5 min read·eng-04-005
compare

Concept

Template Method pattern defines the skeleton of an algorithm in a base class, deferring specific steps to subclasses. The base class calls abstract methods at the right points; subclasses implement those methods to fill in the algorithm.

Structure:

  • Abstract Class (Template): The template() method calls the algorithm steps in order. Some steps are concrete (implemented in the abstract class). Some are abstract (subclasses must implement). Some are "hooks" (optional overrides with default behavior).
  • Concrete Subclass: Implements the abstract steps without changing the algorithm's structure.

Algorithm skeleton example:

text
buildReport() {      ← template method (not overridable, or overridable with care)
    $data = this.fetchData()      ← abstract: subclass provides
    $processed = this.process($data)  ← concrete: base class does it
    return this.format($processed)    ← abstract: subclass provides
}

Hooks: Optional methods that do nothing by default. Subclasses can override for extra behavior. protected function beforeFormat(): void {} — call it in the template, override if needed.

Template Method vs Strategy: Template Method uses inheritance to vary behavior. Strategy uses composition. Template Method is fine when the relationship really is "is-a" and you control the class hierarchy. Strategy is more flexible — swap algorithms without subclassing.

PHP examples: Abstract controller base classes, base migration class, testing base class with setup/teardown hooks, data exporter with fetchData/format steps.

Code Example

php
<?php
// Abstract Class with Template Method
abstract class DataExporter
{
    // Template method — defines the algorithm skeleton
    final public function export(): string
    {
        $this->beforeExport();                // hook
        $data      = $this->fetchData();      // abstract
        $processed = $this->processData($data); // concrete
        $output    = $this->format($processed); // abstract
        $this->afterExport($output);          // hook
        return $output;
    }

    // Abstract steps — MUST be implemented by subclasses
    abstract protected function fetchData(): array;
    abstract protected function format(array $data): string;

    // Concrete step — same logic for all subclasses
    protected function processData(array $data): array
    {
        return array_filter($data, fn($row) => !empty($row));
    }

    // Hooks — optional overrides with default (empty) behavior
    protected function beforeExport(): void {}
    protected function afterExport(string $output): void {}
}

// Concrete Subclasses
class CsvExporter extends DataExporter
{
    public function __construct(private readonly string $table) {}

    protected function fetchData(): array
    {
        return \DB::table($this->table)->get()->toArray();
    }

    protected function format(array $data): string
    {
        if (empty($data)) return '';
        $headers = implode(',', array_keys((array) $data[0]));
        $rows    = array_map(fn($row) => implode(',', (array) $row), $data);
        return $headers . "\n" . implode("\n", $rows);
    }

    protected function beforeExport(): void
    {
        \Log::info("Starting CSV export for table: {$this->table}");
    }
}

class JsonExporter extends DataExporter
{
    public function __construct(private readonly string $table) {}

    protected function fetchData(): array
    {
        return \DB::table($this->table)->get()->toArray();
    }

    protected function format(array $data): string
    {
        return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    }
}

class XmlExporter extends DataExporter
{
    public function __construct(private readonly \Illuminate\Database\Query\Builder $query) {}

    protected function fetchData(): array
    {
        return $this->query->get()->toArray();
    }

    protected function format(array $data): string
    {
        $xml = new \SimpleXMLElement('<data/>');
        foreach ($data as $row) {
            $item = $xml->addChild('item');
            foreach ((array) $row as $key => $value) {
                $item->addChild($key, htmlspecialchars((string) $value));
            }
        }
        return $xml->asXML();
    }
}

// Usage — same export() call regardless of format
$csvOutput  = (new CsvExporter('users'))->export();
$jsonOutput = (new JsonExporter('products'))->export();