Template Method defines the skeleton of an algorithm in a base class and lets subclasses fill in specific steps. It is the backbone of inheritance-based extensibility – and one of the most widely used patterns in frameworks like Magento 2, Symfony and Laravel. I show the classic GoF implementation, the difference between abstract steps and hooks, and a comparison with Strategy.
Classic Template Method
<?php
declare(strict_types=1);
// Base class defines the algorithm skeleton
abstract class DataImporter
{
// Template method - defines the steps, cannot be overridden
final public function import(string $source): array
{
$rawData = $this->readData($source); // step 1: read
$validated = $this->validateData($rawData); // step 2: validate
$transformed = $this->transformData($validated); // step 3: transform
$this->saveData($transformed); // step 4: save
$this->cleanup(); // step 5: cleanup (hook)
return $transformed;
}
// Abstract step - MUST be implemented by subclasses
abstract protected function readData(string $source): array;
abstract protected function validateData(array $data): array;
abstract protected function transformData(array $data): array;
abstract protected function saveData(array $data): void;
// Hook - MAY be overridden, has a default implementation
protected function cleanup(): void
{
// Default: do nothing
}
}
// Concrete implementation for CSV
class CsvProductImporter extends DataImporter
{
protected function readData(string $source): array
{
$rows = [];
$file = fopen($source, 'r');
while (($row = fgetcsv($file)) !== false) {
$rows[] = $row;
}
fclose($file);
return array_slice($rows, 1); // skip header
}
protected function validateData(array $data): array
{
return array_filter($data, fn($row) =>
!empty($row[0]) && is_numeric($row[2]) // sku and price required
);
}
protected function transformData(array $data): array
{
return array_map(fn($row) => [
'sku' => trim($row[0]),
'name' => trim($row[1]),
'price' => (float) $row[2],
'active' => (bool) ($row[3] ?? 1),
], $data);
}
protected function saveData(array $data): void
{
foreach ($data as $item) {
// save to Magento via ProductRepository
}
}
// Override the hook to clean up temp files
protected function cleanup(): void
{
unlink($this->tempFile);
}
}
// JSON importer only needs to implement the read step differently
class JsonProductImporter extends DataImporter
{
protected function readData(string $source): array
{
return json_decode(file_get_contents($source), true, 512, JSON_THROW_ON_ERROR);
}
protected function validateData(array $data): array
{
return array_filter($data, fn($item) => isset($item['sku'], $item['price']));
}
protected function transformData(array $data): array { return $data; }
protected function saveData(array $data): void { /* ... */ }
}
// Usage - same call regardless of format
$csvImporter = new CsvProductImporter();
$jsonImporter = new JsonProductImporter();
$csvImporter->import('/tmp/products.csv');
$jsonImporter->import('/api/products.json');
Abstract steps vs Hooks
| Type | In code | Required? | Default? | Use when |
|---|---|---|---|---|
| Abstract step | abstract protected function |
Yes | No | Step is always needed but implementation varies |
| Hook | protected function with empty or default body |
No | Yes | Optional step that some subclasses may need |
Template Method in Magento 2
<?php
// AbstractModel defines _beforeSave and _afterSave hooks
class MyModel extends \Magento\Framework\Model\AbstractModel
{
protected function _construct(): void
{
$this->_init(\Vendor\Module\Model\ResourceModel\MyModel::class);
}
// Override hook - runs before every save
protected function _beforeSave(): static
{
if (empty($this->getCreatedAt())) {
$this->setCreatedAt(date('Y-m-d H:i:s'));
}
$this->setUpdatedAt(date('Y-m-d H:i:s'));
return parent::_beforeSave();
}
// Override hook - runs after every save
protected function _afterSave(): static
{
// Clear related cache after save
return parent::_afterSave();
}
}
// AbstractBlock defines _toHtml() template method
class MyBlock extends \Magento\Framework\View\Element\Template
{
// Hook: override to prepare data before template rendering
protected function _prepareLayout(): static
{
$this->setData('products', $this->loadProducts());
return parent::_prepareLayout();
}
}
Template Method vs Strategy
| Aspect | Template Method | Strategy |
|---|---|---|
| Mechanism | Inheritance | Composition |
| Algorithm steps | Fixed skeleton, variable steps | Entire algorithm interchangeable |
| Runtime swap? | No – type fixed at object creation | Yes – strategy can be changed |
| Coupling | Tight (subclass knows parent) | Loose (context knows interface) |
| When to use | Algorithm steps vary but structure is stable | Algorithms are completely independent |
Summary
Template Method is one of the simplest and most broadly useful patterns. It is the right choice when you have a stable algorithm structure with variable steps – data importers, report generators, export pipelines, model lifecycle hooks. Prefer Strategy when the entire algorithm varies or when you need runtime swapping. In Magento 2, AbstractModel, AbstractBlock and AbstractCarrier all use Template Method – knowing the pattern explains exactly how and where to extend them.
