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…