PHP Abstract Classes
What Is an Abstract Class?
In simple terms: An abstract class is an incomplete class — a template that defines what its child classes have in common and leaves some parts for them to fill in. You cannot create an object from it directly, just as you cannot buy a generic "vehicle"; you buy a car or a motorcycle. The abstract class describes what all vehicles share; the concrete classes complete the details.
Technically: A class declared with abstract cannot be instantiated. It may contain normal (concrete) methods and properties, plus abstract methods — method signatures without a body. Every non-abstract child class must implement all inherited abstract methods with compatible signatures; otherwise PHP raises a fatal error.
Why Abstract Classes Exist
When several classes share code and structure but differ in specific steps, an abstract class:
- holds the shared code once (properties, constructor, common methods),
- forces each child to implement the parts that differ,
- prevents creating "half-finished" objects of the base class.
Syntax
abstract class BaseClass
{
// normal properties and methods
abstract protected function step(string $input): string; // no body
}
class ConcreteClass extends BaseClass
{
protected function step(string $input): string
{
// implementation
}
}
A First Example: Shapes
<?php
abstract class Shape
{
public function __construct(protected string $name) {}
abstract public function area(): float;
public function describe(): string
{
return sprintf('%s with area %.2f', $this->name, $this->area());
}
}
class Circle extends Shape
{
public function __construct(private float $radius)
{
parent::__construct('Circle');
}
public function area(): float
{
return M_PI * $this->radius ** 2;
}
}
class Rectangle extends Shape
{
public function __construct(private float $width, private float $height)
{
parent::__construct('Rectangle');
}
public function area(): float
{
return $this->width * $this->height;
}
}
foreach ([new Circle(1.5), new Rectangle(4, 2.5)] as $shape) {
echo $shape->describe() . "\n";
}
try {
$shape = new Shape('Generic');
} catch (Error $e) {
echo $e->getMessage() . "\n";
}
Output
Circle with area 7.07
Rectangle with area 10.00
Cannot instantiate abstract class Shape
How It Works
Shapedeclaresarea()as abstract: every shape has an area, but there is no general formula.describe()is a concrete method that already usesarea(), even thoughShapedoes not implement it. At runtime, the child's implementation is called.CircleandRectangleprovide the formulas and reuse the shared constructor logic anddescribe().new Shape(...)fails: an abstract class is not a complete type and cannot be instantiated.
Rules for Abstract Classes and Methods
| Rule | Error If Violated |
|---|---|
| Abstract classes cannot be instantiated | Cannot instantiate abstract class Shape |
| Abstract methods have no body | Abstract function Shape::area() cannot contain body |
| A class with an abstract method must itself be abstract | Class Shape contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Shape::area) |
| Concrete child classes must implement every abstract method | Same error, naming the missing methods |
| Implementations must have compatible signatures and equal or wider visibility | Declaration of ... must be compatible with ... |
Abstract methods cannot be private | Child classes could not implement them |
Forgetting an Implementation
class Square extends Shape
{
// area() is missing
}
PHP Fatal error: Class Square contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Shape::area)
The error appears when the class is declared — not when area() is first called — so missing implementations are caught early.
An abstract class does not need to have abstract methods. Declaring a class abstract without them simply prevents direct instantiation of a base class that is only meant to be extended.
The Template Method Pattern
The most common use of abstract classes is the Template Method pattern: the abstract class implements an algorithm with a fixed sequence of steps, and child classes implement individual steps.
<?php
abstract class Exporter
{
final public function export(array $rows): string
{
if ($rows === []) {
throw new InvalidArgumentException('Nothing to export.');
}
$output = $this->header(array_keys($rows[0]));
foreach ($rows as $row) {
$output .= $this->row(array_values($row));
}
return $output . $this->footer(count($rows));
}
abstract protected function header(array $columns): string;
abstract protected function row(array $values): string;
protected function footer(int $rowCount): string
{
return ''; // optional step: no footer by default
}
}
final class CsvExporter extends Exporter
{
protected function header(array $columns): string
{
return implode(',', $columns) . "\n";
}
protected function row(array $values): string
{
return implode(',', $values) . "\n";
}
}
final class MarkdownExporter extends Exporter
{
protected function header(array $columns): string
{
return '| ' . implode(' | ', $columns) . " |\n"
. '|' . str_repeat(' --- |', count($columns)) . "\n";
}
protected function row(array $values): string
{
return '| ' . implode(' | ', $values) . " |\n";
}
protected function footer(int $rowCount): string
{
return "\n_$rowCount rows_\n";
}
}
$rows = [
['sku' => 'PEN-01', 'price' => 20],
['sku' => 'NB-A5', 'price' => 60],
];
echo (new CsvExporter())->export($rows);
echo "\n";
echo (new MarkdownExporter())->export($rows);
Output
sku,price
PEN-01,20
NB-A5,60
| sku | price |
| --- | --- |
| PEN-01 | 20 |
| NB-A5 | 60 |
_2 rows_
Step-by-Step Explanation
export()is the template method: it validates the input and defines the order header → rows → footer. It isfinal, so no exporter can skip the validation or change the sequence.header()androw()are abstract: every format must define them.footer()is a concrete method with an empty default — an optional hook that child classes may override, asMarkdownExporterdoes.- Adding a new format (HTML, TSV) requires only a new subclass with two or three small methods.
Abstract Classes vs Interfaces
| Abstract Class | Interface | |
|---|---|---|
| Contains implemented methods | Yes | No (only signatures, plus constants and — in PHP 8.4 — property declarations) |
| Properties and constructor | Yes | No regular properties; no constructor logic |
| A class can extend / implement | Only one abstract class | Many interfaces |
| Purpose | Share code and structure among closely related classes | Define a contract that unrelated classes can fulfill |
They are often combined: an interface defines the contract (Exporter), and an optional abstract base class implementing it provides shared code (AbstractExporter). Code that uses exporters depends on the interface, so it also accepts implementations that do not extend the base class (see PHP Interfaces).
When to Choose an Abstract Class
- Several classes share substantial implementation (not just method names).
- The shared code needs state (properties) or a common constructor.
- You want to enforce a workflow (template method) while letting subclasses fill in steps.
Common Mistakes
Trying to Instantiate the Abstract Class
Effect: Cannot instantiate abstract class.
Fix: Instantiate a concrete subclass.
Leaving Abstract Methods Unimplemented
Effect: Fatal error when the child class is declared.
Fix: Implement all abstract methods, or declare the child abstract too.
Using Abstract Classes Only to Define a Contract
Effect: Implementations are forced into a single inheritance chain.
Fix: Use an interface when no shared code exists.
Large "God" Base Classes
Effect: Child classes inherit many unrelated methods and become tightly coupled to the base.
Fix: Keep abstract classes focused; move unrelated helpers into separate collaborating classes.
Best Practices
- Use abstract classes to share code among closely related classes and to enforce required methods.
- Make workflow methods
finaland abstract stepsprotected(Template Method pattern). - Provide optional hooks as concrete methods with sensible defaults.
- Pair abstract base classes with interfaces so code depends on the contract, not the base class.
- Keep inheritance hierarchies shallow.
Official reference: https://www.php.net/manual/en/language.oop5.abstract.php