PHP Abstract Classes
An abstract class in PHP is a class that cannot be instantiated on its own — it exists purely to be extended. It lets you define a common structure and shared behavior for a family of related classes while forcing every subclass to implement certain methods in its own way. Abstract classes are one of PHP’s core tools for building flexible, maintainable object-oriented systems, especially when you want to guarantee that related classes share a consistent contract without duplicating code.
Overview: How Abstract Classes Work
You declare an abstract class with the abstract keyword before class. Inside it, you can mix two kinds of methods: abstract methods, which declare a signature but have no body (they end with a semicolon instead of { }), and concrete methods, which have a full implementation just like in any normal class. An abstract class can also have properties, constants, a constructor, and static members — it behaves like a regular class in every way except one: PHP will not let you create an instance of it directly with new.
The rule that ties it all together is this: if a class contains even one abstract method, the class itself must be declared abstract. Any non-abstract ("concrete") subclass that extends it is then required to implement every abstract method it inherited, using a compatible signature — the same or a covariant return type, and the same or a contravariant parameter type (PHP enforces the Liskov substitution principle here). If a subclass fails to implement all of them, that subclass must also be declared abstract, or PHP raises a fatal error when the class is compiled.
What happens internally
When the Zend Engine compiles a class declaration, it stores metadata about the class in an internal class entry, including a flag that marks it as abstract (ZEND_ACC_EXPLICIT_ABSTRACT_CLASS) and, per method, a flag marking that specific method as abstract. During class linking — the step where PHP resolves a class’s full inheritance chain — the engine walks every abstract method inherited from parent classes and interfaces and checks whether the current class provides a matching implementation. If any are missing and the class itself isn’t marked abstract, compilation fails immediately with a fatal error, before your script even reaches the line that would try to use the class. Separately, when you attempt new AbstractClassName(), the object-creation handler checks the abstract flag on the class entry and throws an Error ("Cannot instantiate abstract class") rather than allocating an object. Both checks exist so that an incomplete class can never produce a usable object at runtime.
Abstract classes vs. interfaces
Abstract classes and interfaces are often confused because both can force a class to implement certain methods. The difference is that an abstract class can hold real, shared implementation, properties, and a constructor, and a class can extend only one abstract class (single inheritance). An interface is a pure contract — no implementation, no state — and a class can implement any number of interfaces. Use an abstract class when subclasses share meaningful code and state; use an interface when you only need to guarantee a set of method signatures across otherwise unrelated classes. The two are frequently combined: an abstract class can itself implement one or more interfaces.
Syntax
abstract class ClassName
{
abstract public function methodName(Type $param): ReturnType;
public function concreteMethod(): void
{
// full implementation allowed here
}
}
class ConcreteClass extends ClassName
{
public function methodName(Type $param): ReturnType
{
// required implementation
}
}
| Part | Meaning |
|---|---|
abstract class |
Declares a class that cannot be instantiated directly. |
abstract public function ...; |
A method with no body — only a signature. Every non-abstract subclass must implement it. |
| Visibility | Abstract methods can be public or protected, but never private — a private method can’t be overridden, which would defeat the point. |
extends |
A class extends at most one abstract (or normal) parent class. |
| Concrete method | A fully implemented method in the abstract class; inherited as-is unless overridden. |
Examples
Example 1: A basic abstract class
<?php
abstract class Shape
{
abstract public function area(): float;
public function describe(): string
{
return sprintf('%s has an area of %.2f', static::class, $this->area());
}
}
class Circle extends Shape
{
public function __construct(private float $radius) {}
public function area(): float
{
return M_PI * $this->radius ** 2;
}
}
class Rectangle extends Shape
{
public function __construct(private float $width, private float $height) {}
public function area(): float
{
return $this->width * $this->height;
}
}
$shapes = [new Circle(3), new Rectangle(4, 5)];
foreach ($shapes as $shape) {
echo $shape->describe() . PHP_EOL;
}
Output:
Circle has an area of 28.27
Rectangle has an area of 20.00
Shape defines one abstract method, area(), and one concrete method, describe(), that every subclass inherits for free. Circle and Rectangle each supply their own area() logic, but they don’t need to rewrite describe() — that’s the whole point of putting shared behavior in the abstract class instead of an interface.
Example 2: A more realistic use case — payment gateways
<?php
abstract class PaymentGateway
{
protected array $log = [];
public function __construct(protected string $merchantId) {}
abstract public function charge(float $amount): string;
protected function logTransaction(string $message): void
{
$this->log[] = $message;
}
public function getLog(): array
{
return $this->log;
}
}
class StripeGateway extends PaymentGateway
{
public function charge(float $amount): string
{
$reference = 'stripe_' . number_format($amount, 2);
$this->logTransaction("Charged $amount via Stripe for merchant {$this->merchantId}");
return $reference;
}
}
class PaypalGateway extends PaymentGateway
{
public function charge(float $amount): string
{
$reference = 'paypal_' . number_format($amount, 2);
$this->logTransaction("Charged $amount via PayPal for merchant {$this->merchantId}");
return $reference;
}
}
$gateways = [
new StripeGateway('merchant_1'),
new PaypalGateway('merchant_2'),
];
foreach ($gateways as $gateway) {
echo $gateway->charge(49.99) . PHP_EOL;
print_r($gateway->getLog());
}
Output:
stripe_49.99
Array
(
[0] => Charged 49.99 via Stripe for merchant merchant_1
)
paypal_49.99
Array
(
[0] => Charged 49.99 via PayPal for merchant merchant_2
)
Here the abstract class carries real shared state ($merchantId, $log) and a protected helper method, while forcing every gateway to define its own charge() logic. Notice that logTransaction() is protected, not abstract — subclasses use it, they don’t override it.
Example 3: The template method pattern
<?php
abstract class ReportGenerator
{
final public function generate(): string
{
$data = $this->fetchData();
$formatted = $this->formatData($data);
return "Report: {$formatted}";
}
abstract protected function fetchData(): array;
abstract protected function formatData(array $data): string;
}
class SalesReportGenerator extends ReportGenerator
{
protected function fetchData(): array
{
return ['Jan' => 1200, 'Feb' => 1500];
}
protected function formatData(array $data): string
{
$parts = [];
foreach ($data as $month => $total) {
$parts[] = "{$month}: \${$total}";
}
return implode(', ', $parts);
}
}
$report = new SalesReportGenerator();
echo $report->generate() . PHP_EOL;
Output:
Report: Jan: $1200, Feb: $1500
This is the template method pattern: the abstract class defines the fixed algorithm (generate(), marked final so subclasses can’t change the overall sequence), while each subclass only fills in the variable steps (fetchData() and formatData()). This is one of the most common and powerful uses of abstract classes in real applications.
Under the Hood: What PHP Does, Step by Step
- When PHP compiles
abstract class Shape { abstract public function area(): float; ... }, it marks the class entry as abstract and marksareaas an abstract method internally. - When it compiles
class Circle extends Shape, the engine linksCircleto its parent and checks every abstract methodShapedeclares. - If
Circleprovides a compatiblearea(), the check passes andCirclebecomes a normal, instantiable class. - If
Circledoes not provide it, PHP fatally errors out right there — the script never gets to run any of your logic. - If you call
new Shape()directly, the object-creation handler sees the abstract flag onShape‘s class entry and throws anErrorinstead of allocating memory for an instance.
Common Mistakes
Mistake 1: Trying to instantiate the abstract class directly
<?php
abstract class Shape
{
abstract public function area(): float;
}
$shape = new Shape();
echo $shape->area();
Output:
Fatal error: Uncaught Error: Cannot instantiate abstract class Shape
Beginners sometimes forget that an abstract class is a blueprint, not a usable object. The fix is to instantiate a concrete subclass instead:
<?php
abstract class Shape
{
abstract public function area(): float;
}
class Circle extends Shape
{
public function __construct(private float $radius) {}
public function area(): float
{
return M_PI * $this->radius ** 2;
}
}
$shape = new Circle(3);
echo $shape->area();
Output:
28.274333882308
Mistake 2: Forgetting to implement an abstract method
<?php
abstract class Shape
{
abstract public function area(): float;
}
class Circle extends Shape
{
public function __construct(private float $radius) {}
}
$circle = new Circle(2);
Output:
Fatal error: Class Circle contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Shape::area)
This happens the moment PHP tries to use the Circle class definition — before your own code even runs. The fix is simply to implement the missing method:
<?php
abstract class Shape
{
abstract public function area(): float;
}
class Circle extends Shape
{
public function __construct(private float $radius) {}
public function area(): float
{
return M_PI * $this->radius ** 2;
}
}
$circle = new Circle(2);
echo $circle->area();
Output:
12.566370614359
Best Practices
- Use an abstract class when subclasses share real state or behavior; use an interface when you only need to guarantee method signatures across otherwise unrelated classes.
- Keep abstract methods narrowly scoped — don’t force every subclass to implement something that’s irrelevant to most of them.
- Use the template method pattern (a
finalconcrete method that calls abstract steps) when you want to fix an algorithm’s shape while letting subclasses customize specific parts. - Document the expected behavior of each abstract method in a comment, since there’s no method body to show intent.
- Always declare return types and parameter types on abstract methods so PHP enforces a consistent contract across all subclasses.
- Avoid deep abstract class hierarchies (abstract extends abstract extends abstract); prefer composition or interfaces once a hierarchy grows past two or three levels.
- Combine an abstract class with one or more interfaces when a family of classes needs both shared implementation and multiple independent contracts.
Practice Exercises
- Create an abstract class
Employeewith a constructor that stores aname, an abstract methodcalculateSalary(): float, and a concrete methodprintPaycheck()that echoes the name and salary. Write two subclasses,HourlyEmployee(salary = hours worked × hourly rate) andSalariedEmployee(a fixed monthly salary), and print a paycheck for one of each. - Create an abstract class
Vehiclewith a class constant, an abstract methodmaxSpeed(): int, and a concrete methoddescribe()that usesstatic::classand callsmaxSpeed(). ImplementCarandMotorcyclesubclasses and confirm each prints its own top speed. - Take two unrelated classes in a small project that duplicate the same logging or validation code, and refactor the shared logic into a new abstract parent class both can extend. Confirm both classes still behave the same after the refactor.
Summary
- An abstract class is declared with
abstract classand cannot be instantiated directly — attemptingnewon it throws a fatalError. - It can mix abstract methods (no body, signature only) with fully implemented concrete methods, properties, constants, and a constructor.
- Any class with at least one abstract method must itself be abstract; concrete subclasses must implement every inherited abstract method or PHP fatally errors at compile time.
- Abstract methods can be
publicorprotected, neverprivate. - Abstract classes differ from interfaces by allowing shared implementation and state, at the cost of single inheritance.
- The template method pattern — a
finalmethod orchestrating abstract steps — is one of the most practical uses of abstract classes.
