Objects Namespaces And Application Architecture

Abstract Classes And Abstraction

An abstract class is a class that cannot be instantiated directly. It can contain shared behaviour, and it can require child classes to provide specific methods.

Use abstract classes when several related classes share a real base concept and some shared implementation. If you only need a contract, an interface is often better. If you only need shared helper code, composition is often better.

Define Abstract Methods

An abstract method has a signature but no body. Child classes must implement it.

PHP example
<?php

declare(strict_types=1);

abstract class Report
{
    abstract public function title(): string;

    public function filename(): string
    {
        return strtolower(str_replace(' ', '-', $this->title())) . '.txt';
    }
}

class SalesReport extends Report
{
    public function title(): string
    {
        return 'Sales Report';
    }
}

$report = new SalesReport();

echo $report->filename() . PHP_EOL;

// Prints:
// sales-report.txt

The base class owns the shared filename rule. The child class supplies the title.

Abstract Classes Can Have Constructors

Shared state can live in the abstract parent.

PHP example
<?php

declare(strict_types=1);

abstract class ExportFile
{
    public function __construct(protected string $name)
    {
        if (trim($name) === '') {
            throw new InvalidArgumentException('Export name is required.');
        }
    }

    abstract public function extension(): string;

    public function filename(): string
    {
        return strtolower($this->name) . '.' . $this->extension();
    }
}

class CsvExportFile extends ExportFile
{
    public function extension(): string
    {
        return 'csv';
    }
}

echo (new CsvExportFile('Products'))->filename() . PHP_EOL;

// Prints:
// products.csv

The constructor rule applies to every child class.

Template Methods Define A Flow

An abstract class can define a workflow and leave one step to child classes.

PHP example
<?php

declare(strict_types=1);

abstract class Importer
{
    public function import(string $contents): int
    {
        $rows = $this->parseRows($contents);

        return count($rows);
    }

    abstract protected function parseRows(string $contents): array;
}

class LineImporter extends Importer
{
    protected function parseRows(string $contents): array
    {
        return array_filter(explode("\n", trim($contents)));
    }
}

$importer = new LineImporter();

echo $importer->import("first\nsecond\n") . PHP_EOL;

// Prints:
// 2

This pattern is useful when the high-level flow is stable but one step changes by subtype.

Do Not Overuse Abstract Bases

Abstract classes couple child classes to parent implementation details.

PHP example
<?php

declare(strict_types=1);

interface Formatter
{
    public function format(string $value): string;
}

class UppercaseFormatter implements Formatter
{
    public function format(string $value): string
    {
        return strtoupper($value);
    }
}

echo (new UppercaseFormatter())->format('ready') . PHP_EOL;

// Prints:
// READY

If there is no shared implementation, an interface keeps the design lighter.

Protected Members Are Part Of The Child Contract

Abstract classes often use protected methods and properties. Treat those as a contract with child classes, not as private internals.

PHP example
<?php

declare(strict_types=1);

abstract class Notification
{
    protected function prefix(): string
    {
        return '[App]';
    }

    abstract public function message(): string;
}

class WelcomeNotification extends Notification
{
    public function message(): string
    {
        return $this->prefix() . ' Welcome';
    }
}

echo (new WelcomeNotification())->message() . PHP_EOL;

// Prints:
// [App] Welcome

Changing protected behaviour can break child classes, so keep it small and deliberate.

What To Remember

Abstract classes combine shared implementation with required child behaviour. Use them for real base concepts with common code. Prefer interfaces for pure contracts and composition for reusable services.

An Abstract Class Defines A Partial Implementation

An abstract class can combine a contract with shared state and behavior. It is appropriate when implementations have a genuine common lifecycle and the base class can provide stable parts of that lifecycle. It is not merely a way to avoid copying a few lines.

Every concrete child must remain usable wherever the abstract parent is expected. If one child cannot implement a parent operation without throwing an unsupported-operation exception, the hierarchy is describing capabilities too broadly. Split the contract or use composed collaborators.

Template Methods Control A Stable Sequence

A template method places orchestration in the base class and delegates selected steps to protected abstract hooks. This can work for importers that always open a source, parse records, validate them, persist a batch, and report metrics while parsing differs by format.

Keep the sequence small and explicit. Hooks should have precise inputs, outputs, and failure semantics. If children need flags to skip half the algorithm or must know undocumented base fields, the template is no longer stable.

Decide which methods children may override. Mark invariant-preserving orchestration final when changing its order would break the contract. Leave only intentional variation points abstract or protected. A base class where every method is overridable provides little dependable behavior.

Constructor Responsibilities

The abstract parent constructor should initialize state required by its own methods. A child constructor must call it explicitly when needed. Avoid passing a large set of child-specific values through the parent merely to make them available to hooks.

Do not call abstract or overridable methods from the parent constructor. Dynamic dispatch can enter the child before its properties are initialized. Construct the complete object first, then call the template method during normal use.

Shared collaborators such as a logger or clock may belong in the parent when every child uses them under the same semantics. A provider needed by only one child belongs in that child. Otherwise unrelated implementations become coupled to dependencies they cannot meaningfully use.

Protected APIs Need Documentation

Protected methods and properties form an API for descendants. Document whether a hook may return an empty result, whether it may be called more than once, which exceptions it should throw, and whether the base class invokes it before or after state changes.

Prefer private state with protected operations. A protected mutable array invites children to depend on its shape and update order. A protected recordFailure(ImportFailure $failure) method can preserve the base invariant while allowing the storage representation to change.

When subclasses live in other packages, protected changes require the same compatibility care as public changes. Search external extension points, publish deprecations, and use semantic versioning according to the supported contract.

Abstract Class Or Interface

Use an interface when callers need a behavior contract without shared implementation or state. A class can implement several interfaces, so capabilities remain composable. Use an abstract class when concrete types genuinely share lifecycle, state, and invariant-preserving code.

The two can work together. Callers depend on an Importer interface, while related built-in importers extend an internal abstract base. Third-party implementers can satisfy the interface without inheriting internal machinery.

Composition is preferable when variations combine independently. If parsing format, storage destination, retry policy, and validation rules all vary, one hierarchy can grow into many combinations. Inject a parser, validator, and sink into a small orchestrator instead.

Fragile Base-Class Changes

Adding a new abstract method breaks every child immediately. Adding a concrete method can also conflict with a method a child already declared under different assumptions. Changing when a hook runs may compile successfully while altering side effects.

Before evolving a base class, list all children and run a shared contract suite. For a new optional capability, a separate interface is often safer than expanding the parent. For a new orchestration step, provide a compatible default only if doing nothing truly preserves the contract.

Serialized child objects, ORM proxies, and dependency-injection configuration can survive deployments. Renaming the base class, changing constructor parameters, or making a class final may require a migration beyond ordinary call sites.

Error Handling In A Template

The base class should catch only failures it can handle consistently. It may translate parser exceptions into an import result, add shared context, or guarantee cleanup. It should not flatten validation rejection, temporary storage failure, and programming defects into one boolean.

If the operation can partially succeed, define the unit of atomicity. A batch importer may commit each record, each batch, or the entire file. Hooks must not secretly open transactions that conflict with the base orchestration. Put transaction ownership in one layer and test failure at every step.

Cleanup belongs in finally when a source or temporary file must be released. Do not rely on child destructors to complete the template lifecycle.

Shared Contract Tests

Build tests that every child must pass. The suite should verify accepted input, rejected input, state changes, repeated calls, exception translation, and cleanup. Run it against built-in children and provide it to third-party implementers when the extension contract is public.

Then add implementation-specific tests for parsing details or provider behavior. Shared tests prove the parent promise; child tests prove the variation.

Mutation testing can expose weak template tests by removing a hook call or changing sequence. Integration tests should use realistic sources and sinks so transaction and cleanup assumptions are exercised.

A Review Example

Suppose CSV and JSON importers extend one base. Both can parse records, but only CSV needs delimiter configuration and only JSON supports nested structures. Those differences fit child state. If a later XML importer needs streaming while the base always loads the whole source, overriding several protected methods to bypass the sequence is a sign that the parent lifecycle is too narrow.

At that point, extract a streaming record source interface and compose it with validation and persistence. The original abstract class may remain for compatible formats, but new requirements should not force every implementation through a lifecycle it cannot honor.

Review Checklist

Ask whether every child supports every public operation, which sequence the parent guarantees, which hooks are intentionally stable, who owns transactions and cleanup, and whether independent variations would compose better. Check for child type tests, protected mutable state, constructor dispatch, and unsupported-operation exceptions.

Traits Share Code, Not A Behavioral Type

A trait can reuse methods across unrelated classes without claiming that they share one parent type. That can be useful for a small, stateless implementation detail. It does not define a caller-facing contract, and trait methods can still depend on host properties or methods that are difficult to discover.

Keep trait requirements explicit through abstract methods, avoid mutable trait state, and test the behavior through each consuming class. If callers need to depend on the behavior, define an interface as well. If the shared code requires several host hooks, extract a collaborator instead; constructor injection makes its dependencies and lifecycle visible.

Traits also copy implementation into each consuming class, so changing the trait can alter many classes at once. Review name conflicts and method precedence, and do not treat a trait as a way to bypass a hierarchy that is already signaling incompatible responsibilities.

After this lesson, you should be able to choose an abstract class for a genuine shared lifecycle, design narrow protected hooks, preserve parent invariants with final template methods, compare inheritance with interfaces and composition, evolve extension contracts carefully, and test every child against shared behavior.

Practice

Task: Build Export File Types

Create an abstract base class for export files.

Requirements

  • Use declare(strict_types=1);.
  • Create an abstract ExportFile class.
  • Require a non-empty export name in the constructor.
  • Add an abstract method for the file extension.
  • Add a concrete method that returns the filename.
  • Create CsvExportFile and JsonExportFile child classes.
  • Print filenames for both child classes.
  • Show one empty-name case by catching the exception.
  • Include the expected output as comments in the same PHP code block.

The parent class should own the shared filename rule while children provide the extension.

Show solution
PHP example
<?php

declare(strict_types=1);

abstract class ExportFile
{
    public function __construct(protected string $name)
    {
        $this->name = trim($name);

        if ($this->name === '') {
            throw new InvalidArgumentException('Export name is required.');
        }
    }

    abstract protected function extension(): string;

    public function filename(): string
    {
        return strtolower($this->name) . '.' . $this->extension();
    }
}

class CsvExportFile extends ExportFile
{
    protected function extension(): string
    {
        return 'csv';
    }
}

class JsonExportFile extends ExportFile
{
    protected function extension(): string
    {
        return 'json';
    }
}

echo (new CsvExportFile('Products'))->filename() . PHP_EOL;
echo (new JsonExportFile('Orders'))->filename() . PHP_EOL;

try {
    new CsvExportFile('   ');
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

// Prints:
// products.csv
// orders.json
// Export name is required.

The abstract class provides the shared constructor and filename logic. Each child class only supplies the part that differs: the extension.