Abstract classes vs interfaces — when to use which
Concept
Abstract classes and interfaces both define contracts but serve different purposes. Choosing wrong leads to brittle code or pointless boilerplate.
Interface: A pure contract — method signatures only, no implementation. A class can implement multiple interfaces. All methods are implicitly public. PHP 8+ interfaces can have constants. Interfaces define the public API contract that consumers depend on. They enable the Dependency Inversion Principle: consumers depend on abstractions (interfaces), not concrete classes.
Abstract class: A partial implementation with abstract method declarations. Can have: constructors, concrete methods (with bodies), properties, protected/private members, and constants. A class can extend only ONE abstract class. Use when you have shared behavior to implement once (Template Method pattern), or when you need to share state (properties) with subclasses.
Decision rule:
- "What can it do?" → interface (behavior contract)
- "What is it?" + shared code → abstract class
- Use interface unless you have concrete implementation to share — default interfaces are more flexible
PHP's built-in interfaces: Iterator, Countable, ArrayAccess, Stringable, JsonSerializable — implementing these hooks into PHP core behavior. Always check if an existing interface fits before defining a custom one.
Code Example
<?php
declare(strict_types=1);
// Interface — pure contract
interface Notifiable
{
public function notify(string $message): void;
public function getContactInfo(): string;
}
interface Loggable
{
public function getLogContext(): array;
}
// A class can implement multiple interfaces
class User implements Notifiable, Loggable
{
public function __construct(
private string $email,
private string $name,
) {}
public function notify(string $message): void
{
mail($this->email, 'Notification', $message);
}
public function getContactInfo(): string { return $this->email; }
public function getLogContext(): array { return ['user' => $this->name]; }
}
// Abstract class — shared behavior + forced overrides
abstract class PaymentGateway
{
private array $log = [];
// Concrete shared behavior
public function charge(float $amount, string $currency): bool
{
$this->log[] = "Charging $amount $currency";
return $this->processCharge($amount, $currency); // calls abstract
}
// Abstract — subclass MUST implement
abstract protected function processCharge(float $amount, string $currency): bool;
// Hook method — subclass MAY override
protected function onSuccess(float $amount): void {}
}
class StripeGateway extends PaymentGateway
{
protected function processCharge(float $amount, string $currency): bool
{
// Stripe-specific API call
return true;
}
}
// Cannot instantiate abstract class
// $gw = new PaymentGateway(); // Fatal Error: Cannot instantiate abstract class
// Interface + abstract hybrid pattern
interface Repository
{
public function findById(int $id): ?object;
public function save(object $entity): void;
}
abstract class BaseRepository implements Repository
{
abstract protected function getTable(): string; // subclass provides table name
public function findById(int $id): ?object
{
// Concrete implementation using getTable()
return null;
}
}