After the Page Builder in ICanUp gained Hero, CTA, Highlight, Cards, Steps, and FAQ, another kind of duplication became visible.
Those blocks have different content, configuration fields, and variants. But their overall structure is the same: build default data, validate content and configuration separately, require a variant, and produce correctly prefixed validation attributes.
Copying that skeleton into every definition would create several almost identical implementations that could slowly drift apart.
That is where Template Method appeared naturally in the code.
The Repetition Was in the Algorithm, Not the Data Shape
Shared Part | What Changes |
|---|---|
| Fields inside content and configuration. |
Default data is assembled the same way. | Concrete default values. |
Validation follows the same skeleton. | Block-specific validation rules. |
| The available variants. |
Attributes need content/configuration prefixes. | Concrete localized attribute labels. |
This is an important sign of Template Method. I did not try to make Hero and Cards identical. They remain different.
What is shared is the algorithm used to build the contract. The base class fixes that order while subclasses provide the variable parts.
The Base Class Fixes the Template With Final Methods
abstract class AbstractPresentationBlockDefinition implements PresentationBlockDefinitionInterface{ final public function defaultData(): array { return [ 'content' => $this->defaultContent(), 'configuration' => $this->defaultConfiguration(), ]; } final public function rules(): array {These three public methods are the template methods.
defaultData() always returns the same top-level shape.
rules() always adds structural rules, prefixes concrete rules, and validates the variant.
attributes() always builds labels using the same process.
A concrete block cannot accidentally replace that skeleton because those methods are final.
Subclasses Fill the Hooks Inside the Algorithm
abstract protected function defaultContent(): array; abstract protected function defaultConfiguration(): array; abstract protected function contentAttributes(): array; abstract protected function configurationAttributes(): array; // Provided by concrete presentation definitions:public function contentRules(): array; public function configurationRules(): array; public function variants(): array;The base class knows when defaults, rules, variants, and attributes are needed, but it does not know what exactly they are.
That is the inversion inside Template Method: the algorithm belongs to the parent class while selected steps are supplied by concrete implementations.
defaultData Always Has One Structural Contract
final public function defaultData(): array{ return [ 'content' => $this->defaultContent(), 'configuration' => $this->defaultConfiguration(), ];}Hero can contain a title, text, and buttons. Steps contains a title, introduction, and items. Cards has an even more complex structure.
But the editor and backend always receive the same top-level contract: content plus configuration.
A concrete definition does not need to remember that wrapper. It returns only its own pieces.
Hero Defines Only Its Domain-Specific Part
final class HeroBlockDefinition extends AbstractPresentationBlockDefinition{ public function type(): ContentBlockTypeEnum { return ContentBlockTypeEnum::HERO; } public function contentRules(): array { return [ 'title' => [ 'required', 'string', 'max:255', ],Hero does not implement defaultData(). It does not build prefixes and does not add the common variant validation.
It knows only its own part: which fields belong to Hero, what their defaults are, and which rules they require.
When the parent calls those hooks, the concrete values automatically become part of the shared algorithm.
rules Shows Template Method Most Clearly
final public function rules(): array{ return [ 'content' => ['required', 'array'], 'configuration' => ['required', 'array'], ...$this->prefixKeys( 'content', $this->contentRules(), ), ...$this->prefixKeys( 'configuration',The subclass does not control the order of this method.
The parent always requires two arrays, always prefixes content rules, always prefixes configuration rules, and always validates the variant against variants().
The subclass controls only the values placed into those slots.
Even Technical Prefixing Stays in the Base Class
private function prefixKeys( string $prefix, array $values,): array { $prefixed = []; foreach ( $values as $key => $value ) { $prefixed[ "{$prefix}.{$key}" ] = $value; } return $prefixed;Small details like this often create divergence after copy-paste.
One definition might return alignment, another might return configuration.alignment, while a third prefixes rules and attributes differently.
The base class owns structural knowledge so concrete definitions can work with local field names.
Why final Matters More Than abstract Here
abstract tells a subclass: you must provide this step.
final tells a subclass: you must not replace this algorithm.
Without final, one presentation block could override rules() and forget the common configuration.variant, while another could return defaults in a different structure.
Template Method is useful when part of the flow needs to remain stable.
Template Method fixes what must remain consistent and leaves hooks only where variation is actually needed.
Steps Uses the Same Algorithm With Completely Different Data
final class StepsBlockDefinition extends AbstractPresentationBlockDefinition{ public function contentRules(): array { return [ 'title' => [ 'nullable', 'string', 'max:255', ], 'introduction' => [ 'nullable', 'string', ],Steps does not look like Hero in terms of content shape. It has an items array and completely different validation rules.
But the presentation contract is the same, so the parent algorithm remains useful.
This is a useful criterion for inheritance: subclasses do not need identical data, but they should genuinely follow the same algorithm skeleton.
Not Every Content Block Should Extend the Base Class
This is one of the most important parts of the case.
Regular Code, Image, Notice, and other blocks can implement ContentBlockDefinitionInterface directly. Even FormBlockDefinition implements the presentation interface without extending AbstractPresentationBlockDefinition because its data contract is different.
I do not force every definition through one parent. Template Method is useful only for a family of classes that genuinely shares one flow.
Template Method and Strategy Solve Similar Problems Differently
Template Method | Strategy |
|---|---|
Variation through inheritance. | Variation through composition. |
The base class owns the algorithm skeleton. | The caller works with an interchangeable object. |
Hooks fill individual steps. | A Strategy implements behavior as a separate object. |
Fits a stable family structure. | Fits runtime-interchangeable algorithms. |
AbstractPresentationBlockDefinition. | SearchProviderInterface from the previous article. |
What a New Presentation Block Needs
- Create a concrete definition extending
AbstractPresentationBlockDefinitionwhen the block fits this contract. - Provide its own
contentRules()andconfigurationRules(). - Define
variants()and editor options. - Provide
defaultContent()anddefaultConfiguration(). - Provide localized content/configuration attributes.
- Add its DTO through
makeData(). - Register the definition in the Content Block registry.
There is no need to reimplement the defaults wrapper, validation-rule prefixing, or the common variant rule.
What This Template Method Actually Gave the Project
Before the Abstraction | After the Abstraction |
|---|---|
Every block could assemble defaults differently. | The parent defines the top-level default shape. |
Common validation could be forgotten. | Structural validation lives in a final method. |
Prefixes would be duplicated across definitions. | Prefix logic lives in one private method. |
Variant validation could drift. | The parent always builds Rule::in from variants. |
A contract change required many edits. | The shared contract changes in one base class. |
The Cost of Template Method
Template Method couples subclasses to an inheritance hierarchy. That is a stronger relationship than an interface with composition.
Changing the final algorithm affects every subclass, so the base class should stay small and stable.
It is also a bad sign when the base class accumulates dozens of protected hooks because one block needs an exception. Composition is often cleaner in that situation.
Conclusion
In ICanUp, AbstractPresentationBlockDefinition does not try to understand what Hero, Cards, or FAQ mean.
It knows only the stable algorithm: how defaults are assembled, how validation rules are built, and how attributes are produced.
Concrete definitions provide the variable steps. final methods prevent them from accidentally breaking the structural contract.
That is a practical Template Method to me: inheritance is not used to reuse arbitrary code, but to fix one algorithm skeleton with explicit extension points.



