Skip to content

Template Method

The Template Method Pattern is a behavioral design pattern where a base class (Abstract Class) defines the skeleton of an algorithm, while allowing subclasses (Concrete Classes) to override specific steps without changing the overall structure of the algorithm.


Structure

Role Example Responsibility
Abstract Class DataProcessor Defines the template method and declares abstract steps
Concrete Class CSVDataProcessor, JSONDataProcessor, XMLDataProcessor Implements specific behavior for individual steps

Steps

  1. Create an Abstract Class
  2. Define the template method that describes the algorithm
  3. Declare abstract steps that subclasses must implement
  4. Optionally define hook methods that subclasses may override
  5. Create Concrete Classes that implement these steps
classDiagram
    class DataProcessor {
        +process()
        #openFile()
        #parseData()*
        #validateData()*
        #transformData()
        #saveResults()
        #closeFile()
    }

    class CSVDataProcessor {
        #parseData()
        #validateData()
        #transformData()
    }

    class JSONDataProcessor {
        #parseData()
        #validateData()
        #transformData()
    }

    class XMLDataProcessor {
        #parseData()
        #validateData()
        #transformData()
    }

    CSVDataProcessor --|> DataProcessor
    JSONDataProcessor --|> DataProcessor
    XMLDataProcessor --|> DataProcessor

The process() method is declared final, ensuring that the algorithm always follows the same sequence of steps while allowing subclasses to customize individual parts of the workflow.


Example: Data Processor

A DataProcessor defines a fixed pipeline:

open → parse → validate → transform → save → close

Each subclass implements the steps required to handle a specific file format. A DataRecord Value Object is passed between steps, decoupling them from raw array structures.


DataProcessor.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\TemplateMethod\DataProcessor;

abstract class DataProcessor
{
    protected string $filePath;
    /** @var resource */
    protected $fileHandle;

    public function __construct(string $filePath)
    {
        $this->filePath = $filePath;
    }

    /**
     * Template method defining the data processing algorithm.
     */
    final public function process(): void
    {
        $this->openFile();
        try {
            $records = $this->parseData();
            $this->validateData($records);
            $records = $this->transformData($records);
            $this->saveResults($records);
        } finally {
            $this->closeFile();
        }
    }

    protected function openFile(): void
    {
        echo "Opening file: {$this->filePath}\n";
        $file = fopen($this->filePath, 'r');
        if ($file === false) {
            throw new \RuntimeException('Unable to open file');
        }

        $this->fileHandle = $file;
    }

    /**
     * @return DataRecord[]
     */
    abstract protected function parseData(): array;

    /**
     * @param DataRecord[] $records
     * @return void
     */
    abstract protected function validateData(array $records): void;

    /**
     * @param DataRecord[] $records
     * @return DataRecord[]
     */
    protected function transformData(array $records): array
    {
        return $records;
    }

    /**
     * @param DataRecord[] $records
     * @return void
     */
    protected function saveResults(array $records): void
    {
        echo "Saving " . count($records) . " records\n";
    }

    protected function closeFile(): void
    {
        fclose($this->fileHandle);
        echo "File closed\n";
    }
}
DataRecord.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\TemplateMethod\DataProcessor;

/**
 * Value Object representing a single parsed data record.
 *
 * Immutable container for field data passed through the processing
 */
class DataRecord
{
    /**
     * @param list<string|null>|array<string, string> $fields
     */
    public function __construct(
        public readonly array $fields
    ) {
    }
}
CSVDataProcessor.php
    <?php

    declare(strict_types=1);

    namespace DesignPattern\Behavioural\TemplateMethod\DataProcessor;

    class CSVDataProcessor extends DataProcessor
    {
        /**
         * @return DataRecord[]
         */
        protected function parseData(): array
        {
            echo "Parsing CSV data\n";

            $rows = [];
            // empty escape: required by PHPStan and avoids PHP 8.4 deprecation
            while (($row = fgetcsv($this->fileHandle, 1000, ',', '"', '')) !== false) {
                $rows[] = new DataRecord($row);
            }

            return $rows;
        }

        /**
         * @param DataRecord[] $records
         * @return void
         * @throws \UnexpectedValueException
         */
        protected function validateData(array $records): void
        {
            echo "Validating CSV structure\n";

            foreach ($records as $record) {
                if (count($record->fields) < 2) {
                    throw new \UnexpectedValueException("Invalid CSV row");
                }
            }
        }

        /**
         * @param DataRecord[] $records
         * @return DataRecord[]
         */
        protected function transformData(array $records): array
        {
            echo "Transforming CSV data\n";

            $transformed = [];
            foreach ($records as $record) {
                $fields = [];
                foreach ($record->fields as $value) {
                    $fields[] = trim((string) $value);
                }
                $transformed[] = new DataRecord($fields);
            }

            return $transformed;
        }
    }
JSONDataProcessor.php
    <?php

    declare(strict_types=1);

    namespace DesignPattern\Behavioural\TemplateMethod\DataProcessor;

    class JSONDataProcessor extends DataProcessor
    {
        /**
         * @return DataRecord[]
         * @throws \JsonException
         * @throws \UnexpectedValueException
         */
        protected function parseData(): array
        {
            echo "Parsing JSON data\n";

            $content = stream_get_contents($this->fileHandle);

            /** @var array{records: array<int, array<string, string>>} $decoded */
            $decoded = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

            $result = [];
            foreach ($decoded['records'] as $item) {
                $result[] = new DataRecord($item);
            }

            return $result;
        }

        /**
         * @param DataRecord[] $records
         * @return void
         * @throws \UnexpectedValueException
         */
        protected function validateData(array $records): void
        {
            echo "Validating JSON structure\n";

            if (empty($records)) {
                throw new \UnexpectedValueException("Missing records key in JSON");
            }
        }

        /**
         * @param DataRecord[] $records
         * @return DataRecord[]
         */
        protected function transformData(array $records): array
        {
            echo "Transforming JSON data\n";

            $transformed = [];
            foreach ($records as $record) {
                $fields = [];
                foreach ($record->fields as $key => $value) {
                    $fields[$key] = trim((string) $value);
                }
                $transformed[] = new DataRecord($fields);
            }

            return $transformed;
        }
    }
XMLDataProcessor.php
    <?php

    declare(strict_types=1);

    namespace DesignPattern\Behavioural\TemplateMethod\DataProcessor;

    class XMLDataProcessor extends DataProcessor
    {
        /**
         * @return DataRecord[]
         * @throws \JsonException
         */
        protected function parseData(): array
        {
            echo "Parsing XML data\n";

            $content = stream_get_contents($this->fileHandle);
            /** @var \SimpleXMLElement $xml */
            $xml = simplexml_load_string($content);

            $records = [];
            foreach ($xml->record as $record) {
                $fields = [];
                foreach ($record as $key => $value) {
                    $fields[$key] = (string) $value;
                }
                $records[] = new DataRecord($fields);
            }

            return $records;
        }

        /**
         * @param DataRecord[] $records
         * @return void
         * @throws \UnexpectedValueException
         */
        protected function validateData(array $records): void
        {
            echo "Validating XML structure\n";

            if (empty($records)) {
                throw new \UnexpectedValueException("XML file contains no data");
            }
        }

        /**
         * @param DataRecord[] $records
         * @return DataRecord[]
         */
        protected function transformData(array $records): array
        {
            echo "Transforming XML data\n";

            $transformed = [];
            foreach ($records as $record) {
                $fields = [];
                foreach ($record->fields as $key => $value) {
                    $fields[$key] = trim((string) $value);
                }
                $transformed[] = new DataRecord($fields);
            }

            return $transformed;
        }
    }

Tests

DataProcessorTest.php
<?php

declare(strict_types=1);

namespace Tests\Behavioural\TemplateMethod\DataProcessor;

use DesignPattern\Behavioural\TemplateMethod\DataProcessor\CSVDataProcessor;
use DesignPattern\Behavioural\TemplateMethod\DataProcessor\DataProcessor;
use DesignPattern\Behavioural\TemplateMethod\DataProcessor\JSONDataProcessor;
use DesignPattern\Behavioural\TemplateMethod\DataProcessor\XMLDataProcessor;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use SimpleXMLElement;

#[CoversClass(DataProcessor::class)]
class DataProcessorTest extends TestCase
{
    /**
     * @param string $file
     * @param array<int, array{name: string, age: int}> $records
     * @return void
     */
    private function createCsv(string $file, array $records): void
    {
        $fp = fopen($file, 'w');

        if ($fp === false) {
            throw new \RuntimeException("Unable to open file: $file");
        }

        fputcsv($fp, array_keys($records[0]), ',', '"', '');

        foreach ($records as $record) {
            fputcsv($fp, $record, ',', '"', '');
        }

        fclose($fp);
    }

    /**
     * @param string $file
     * @param array<int, array{name: string, age: int}> $records
     * @return void
     */
    private function createXml(string $file, array $records): void
    {
        $xml = new SimpleXMLElement('<records/>');

        foreach ($records as $recordData) {
            $record = $xml->addChild('record');

            foreach ($recordData as $key => $value) {
                $record->addChild($key, (string) $value);
            }
        }

        $xml->asXML($file);
    }

    /**
     * @param string $file
     * @param array<int, array{name: string, age: int}> $records
     * @return void
     */
    private function createJson(string $file, array $records): void
    {
        file_put_contents(
            $file,
            json_encode(['records' => $records], JSON_PRETTY_PRINT)
        );
    }

    /**
     * @return array<string, array{
     *     class-string,
     *     string,
     *     string,
     *     array<int, array{name: string, age: int}>,
     *     int
     * }>
     */
    public static function processorProvider(): array
    {
        return [
            'csv::2' => [CSVDataProcessor::class, 'csv', 'CSV', [
                ['name' => 'John', 'age' => 30],
                ['name' => 'Jane', 'age' => 25],
            ],
                3
            ],
            'csv::3' => [CSVDataProcessor::class, 'csv', 'CSV', [
                ['name' => 'John', 'age' => 30],
                ['name' => 'Jane', 'age' => 25],
                ['name' => 'Doe', 'age' => 35],
            ],
                4
            ],
            'json::2' => [JSONDataProcessor::class, 'json', 'JSON', [
                ['name' => 'John', 'age' => 30],
                ['name' => 'Jane', 'age' => 25],
            ],
                2
            ],
            'json::3' => [JSONDataProcessor::class, 'json', 'JSON', [
                ['name' => 'John', 'age' => 30],
                ['name' => 'Jane', 'age' => 25],
                ['name' => 'Doe', 'age' => 35],
            ],
                3
            ],
            'xml::2' => [XMLDataProcessor::class, 'xml', 'XML', [
                ['name' => 'John', 'age' => 30],
                ['name' => 'Jane', 'age' => 25],
            ],
                2
            ],
            'xml::3' => [XMLDataProcessor::class, 'xml', 'XML', [
                ['name' => 'John', 'age' => 30],
                ['name' => 'Jane', 'age' => 25],
                ['name' => 'Doe', 'age' => 35],
            ],
                3
            ],
        ];
    }

    /**
     * @param string $processorClass
     * @param string $extension
     * @param string $label
     * @param array<int, array{name: string, age: int}> $records
     * @param int $expectedRecords
     * @return void
     */
    #[DataProvider('processorProvider')]
    public function testProcessing(
        string $processorClass,
        string $extension,
        string $label,
        array $records,
        int $expectedRecords
    ): void {
        $file = sys_get_temp_dir() . "/test.$extension";

        match ($extension) {
            'csv'  => $this->createCsv($file, $records),
            'json' => $this->createJson($file, $records),
            'xml'  => $this->createXml($file, $records),
            default => throw new \RuntimeException("Unsupported extension: $extension"),
        };

        $processor = new $processorClass($file);

        $this->expectOutputString(
            "Opening file: {$file}\n" .
            "Parsing {$label} data\n" .
            "Validating {$label} structure\n" .
            "Transforming {$label} data\n" .
            "Saving {$expectedRecords} records\n" .
            "File closed\n"
        );

        /** @var DataProcessor $processor */
        $processor->process();

        unlink($file);
    }
}

Example 2: Pizza Preparation

The Pizza class defines the preparation process:

prepare dough → add sauce → add toppings → bake

The base class controls the preparation order while subclasses customize the topping step.


Abstract Class

Pizza.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\TemplateMethod\Pizza;

abstract class Pizza
{
    /**
     * @var array<int, string>
     */
    private array $recipeSteps = [];

    protected function prepareDoug(): string
    {
        return "prepare Dough";
    }

    protected function addTomatoSauce(): string
    {
        return "add Sauce";
    }

    protected function addCheese(): string
    {
        return "add Cheese";
    }

    abstract protected function addTopping(): string;

    protected function bake(): string
    {
        return "bake Pizza";
    }

    protected function cut(): string
    {
        return "cut Pizza";
    }

    /**
     * Template method defining the pizza-making algorithm.
     */
    final public function makePizza(): void
    {
        foreach ($this->getRecipeSteps() as $step) {
            $this->recipeSteps[] = $step;
        }
    }

    /**
     * @return array<int, string>
     */
    final public function getSteps(): array
    {
        return $this->recipeSteps;
    }

    /**
     * @return array<int, string>
     */
    protected function getRecipeSteps(): array
    {
        $steps = [
            $this->prepareDoug(),
            $this->addTomatoSauce(),
            $this->addCheese(),
            $this->addTopping(),
            $this->bake(),
            $this->cut(),
        ];

        return array_filter($steps);
    }
}

Concrete Pizzas

CheesyStuffedPizza.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\TemplateMethod\Pizza;

class CheesyStuffedPizza extends Pizza
{
    protected function prepareDoug(): string
    {
        return "prepare stuffed dough";
    }

    protected function addCheese(): string
    {
        return "add extra cheese";
    }

    protected function addTopping(): string
    {
        return "add Pepperoni";
    }

    protected function bake(): string
    {
        return "bake Pizza slowly for extra cheesiness";
    }
}
SalamiPizza.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\TemplateMethod\Pizza;

class SalamiPizza extends Pizza
{
    protected function addTopping(): string
    {
        return "add Salami";
    }
}

Tests

PizzaTest.php
<?php

declare(strict_types=1);

namespace Tests\Behavioural\TemplateMethod\Pizza;

use DesignPattern\Behavioural\TemplateMethod\Pizza\CheesyStuffedPizza;
use DesignPattern\Behavioural\TemplateMethod\Pizza\SalamiPizza;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(SalamiPizza::class)]
class PizzaTest extends TestCase
{
    public function testPizzaSalami(): void
    {
        $salamiPizza = new SalamiPizza();
        $salamiPizza->makePizza();

        self::assertSame(
            [
                'prepare Dough',
                'add Sauce',
                'add Cheese',
                'add Salami',
                'bake Pizza',
                'cut Pizza'
            ],
            $salamiPizza->getSteps()
        );
    }

    public function testPizzaCheesyStuffed(): void
    {
        $cheesyPizza = new CheesyStuffedPizza();
        $cheesyPizza->makePizza();

        self::assertSame(
            [
                'prepare stuffed dough',
                'add Sauce',
                'add extra cheese',
                'add Pepperoni',
                'bake Pizza slowly for extra cheesiness',
                'cut Pizza'
            ],
            $cheesyPizza->getSteps()
        );
    }
}