iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Template Method

Template Method defines an algorithms skeleton in a base class and lets subclasses override specific steps. The shape of the algorithm stays fixed; the per-case details vary. Classic uses: importers, exporters, parsers, lifecycle managers — anything where the steps are the same but the substance changes per type.

Importer skeleton with per-format overrides

EXAMPLE
<?php
// ============================================================
// Abstract base — defines the import algorithm; SUBCLASSES fill in the holes.
// ============================================================
abstract class Importer {
    public final function import(string $source): ImportResult {
        $raw = $this->read($source);
        $rows = $this->parse($raw);
        $rows = array_map([$this, 'normalise'], $rows);
        $valid = $this->validate($rows);
        $count = $this->persist($valid);
        $this->postImport($count);
        return new ImportResult($count);
    }

    // 'Primitive' operations — subclasses MUST implement
    abstract protected function read(string $source): string;
    abstract protected function parse(string $raw): array;

    // 'Hook' operations — subclasses MAY override; sensible defaults provided
    protected function normalise(array $row): array     { return array_map('trim', $row); }
    protected function validate(array $rows): array     { return $rows; }
    protected function persist(array $rows): int        {
        foreach ($rows as $r) DB::table('orders')->insert($r);
        return count($rows);
    }
    protected function postImport(int $count): void     { /* no-op */ }
}

// ============================================================
// Concrete: CSV
// ============================================================
class CsvImporter extends Importer {
    protected function read(string $source): string {
        return file_get_contents($source);
    }
    protected function parse(string $raw): array {
        $lines = preg_split('/\\r?\\n/', trim($raw));
        $header = str_getcsv(array_shift($lines));
        return array_map(fn($line) => array_combine($header, str_getcsv($line)), $lines);
    }
    protected function validate(array $rows): array {
        return array_values(array_filter($rows, fn($r) => isset($r['order_id'], $r['email'])));
    }
}

// ============================================================
// Concrete: JSON
// ============================================================
class JsonImporter extends Importer {
    protected function read(string $source): string {
        return file_get_contents($source);
    }
    protected function parse(string $raw): array {
        $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
        return $data['orders'] ?? [];
    }
    protected function postImport(int $count): void {
        Log::channel('import')->info('json import done', ['count' => $count]);
    }
}

// ============================================================
// Concrete: XML (different read source — an HTTP URL)
// ============================================================
class XmlImporter extends Importer {
    public function __construct(private HttpClient $http) {}
    protected function read(string $source): string {
        return $this->http->get($source)->body();
    }
    protected function parse(string $raw): array {
        $xml = new SimpleXMLElement($raw);
        return array_map(fn($o) => json_decode(json_encode($o), true), iterator_to_array($xml->order));
    }
}

// ============================================================
// Callers always go through the same shape — the algorithm is the contract
// ============================================================
(new CsvImporter())->import('/tmp/orders.csv');
(new JsonImporter())->import('/tmp/orders.json');
(new XmlImporter(new HttpClient()))->import('https://supplier.example/api/orders.xml');

Why it matters

The trap to avoid: a fat base class. Each new requirement tempts you to add a hook or another protected method, and the algorithm drifts from skeleton to god-class. When you reach that point, swap to Strategy — extract the varying pieces into composed objects rather than overridden methods.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
class Pipeline {
    run(input) {
        const parsed = this.parse(input);
        const transformed = this.transform(parsed);
        return this.serialize(transformed);
    }
    // subclasses override parse / transform / serialize
}
Try it Yourself »

Discussion

Loading…