PHP Interfaces
What Is an Interface?
In simple terms: An interface is a contract. It lists the methods a class promises to provide, without saying how they work. A charging port standard is a good comparison: any phone with that port works with any charger with that plug, regardless of the manufacturer. In code, any class that implements a Notifier interface can be used wherever a notifier is needed — email, SMS, WhatsApp, or a test double.
Technically: An interface is declared with the interface keyword and contains public method signatures (and constants; since PHP 8.4 also property declarations). A class uses implements to promise that it provides all of these methods with compatible signatures. Interfaces are types: they can be used in parameter, property, and return type declarations and with instanceof. A class can implement any number of interfaces.
Why Interfaces Matter
| Benefit | Explanation |
|---|---|
| Interchangeability | Code depends on what an object can do, not which class it is |
| Decoupling | A service using a PaymentGateway interface does not change when you switch providers |
| Testability | Tests can supply simple fake implementations |
| Multiple roles | A class can be Countable, JsonSerializable, and IteratorAggregate at once |
| Clear contracts | The interface documents exactly what implementations must provide |
Declaring and Implementing an Interface
Syntax
interface InterfaceName
{
public function methodName(Type $param): ReturnType;
}
class ClassName implements InterfaceName
{
public function methodName(Type $param): ReturnType
{
// implementation
}
}
Example: Interchangeable Notifiers
<?php
interface Notifier
{
public function send(string $recipient, string $message): void;
}
final class EmailNotifier implements Notifier
{
public function send(string $recipient, string $message): void
{
echo "Email to $recipient: $message\n";
}
}
final class SmsNotifier implements Notifier
{
public function send(string $recipient, string $message): void
{
echo "SMS to $recipient: " . mb_substr($message, 0, 27) . "\n";
}
}
final class OrderService
{
public function __construct(private Notifier $notifier) {}
public function ship(int $orderId, string $recipient): void
{
// ... update the order ...
$this->notifier->send($recipient, "Order #$orderId has been shipped and is on its way.");
}
}
(new OrderService(new EmailNotifier()))->ship(501, 'asha@example.com');
(new OrderService(new SmsNotifier()))->ship(502, '9876543210');
Output
Email to asha@example.com: Order #501 has been shipped and is on its way.
SMS to 9876543210: Order #502 has been shipped
How It Works
Notifierdefines the contract: asend()method with this exact signature.EmailNotifierandSmsNotifierimplement it in completely different ways (the SMS version shortens the message to 27 characters).OrderServicedeclares its dependency asNotifier— the interface, not a concrete class. It works with any implementation and never needs to change when a new channel is added.- Passing an object that does not implement
Notifierwould cause aTypeError.
Testing With Fake Implementations
Interfaces make it easy to replace real services in tests:
<?php
interface Notifier
{
public function send(string $recipient, string $message): void;
}
final class InMemoryNotifier implements Notifier
{
public array $sent = [];
public function send(string $recipient, string $message): void
{
$this->sent[] = [$recipient, $message];
}
}
final class OrderService
{
public function __construct(private Notifier $notifier) {}
public function ship(int $orderId, string $recipient): void
{
$this->notifier->send($recipient, "Order #$orderId has been shipped.");
}
}
$fake = new InMemoryNotifier();
(new OrderService($fake))->ship(501, 'asha@example.com');
var_dump(count($fake->sent) === 1);
echo $fake->sent[0][1] . "\n";
Output
bool(true)
Order #501 has been shipped.
The test checks what the service sent without sending a real email or SMS.
Rules for Interfaces
| Rule | Detail |
|---|---|
All interface methods are public | Implementations must be public too |
| Methods have no body | Interfaces describe what, not how |
| Implementations need compatible signatures | Same rules as overriding: wider parameter types, narrower return types |
| All methods must be implemented | Otherwise the class must be declared abstract |
| Interfaces cannot be instantiated | new Notifier() is an error |
| Constants are allowed | Notifier::MAX_LENGTH; overridable by implementers since PHP 8.1 unless final |
Forgetting a Method
final class WhatsAppNotifier implements Notifier
{
// send() is missing
}
PHP Fatal error: Class WhatsAppNotifier contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Notifier::send)
Implementing Several Interfaces
A class can fulfill several contracts at once. PHP's built-in interfaces let your objects work with core language features:
<?php
final class Cart implements Countable, IteratorAggregate, JsonSerializable
{
private array $items = [];
public function add(string $sku, int $quantity): void
{
$this->items[$sku] = ($this->items[$sku] ?? 0) + $quantity;
}
public function count(): int
{
return array_sum($this->items);
}
public function getIterator(): Iterator
{
return new ArrayIterator($this->items);
}
public function jsonSerialize(): array
{
return ['items' => $this->items, 'count' => $this->count()];
}
}
$cart = new Cart();
$cart->add('PEN-01', 3);
$cart->add('NB-A5', 2);
echo count($cart) . " items\n";
foreach ($cart as $sku => $quantity) {
echo "$sku × $quantity\n";
}
echo json_encode($cart) . "\n";
Output
5 items
PEN-01 × 3
NB-A5 × 2
{"items":{"PEN-01":3,"NB-A5":2},"count":5}
| Interface | Enables |
|---|---|
Countable | count($object) |
IteratorAggregate / Iterator | foreach ($object as ...) (see Iterators) |
JsonSerializable | Custom output for json_encode() |
ArrayAccess | $object['key'] syntax |
Stringable | Objects with __toString() (implemented automatically since PHP 8.0) |
Interface Inheritance
Interfaces can extend one or more other interfaces, combining their contracts:
<?php
interface HasWeight
{
public function weightKg(): float;
}
interface HasDimensions
{
public function volumeCm3(): float;
}
interface Shippable extends HasWeight, HasDimensions
{
public const MAX_WEIGHT_KG = 30;
}
final class Parcel implements Shippable
{
public function __construct(private float $kg, private float $l, private float $w, private float $h) {}
public function weightKg(): float
{
return $this->kg;
}
public function volumeCm3(): float
{
return $this->l * $this->w * $this->h;
}
}
$parcel = new Parcel(2.4, 30, 20, 10);
var_dump($parcel instanceof HasWeight);
echo $parcel->volumeCm3() . " cm³, max " . Shippable::MAX_WEIGHT_KG . " kg\n";
Output
bool(true)
6000 cm³, max 30 kg
A Parcel is a Shippable, a HasWeight, and a HasDimensions. Small, focused interfaces combined like this follow the Interface Segregation Principle: classes should not be forced to implement methods they do not need.
Interfaces vs Abstract Classes
| Interface | Abstract Class | |
|---|---|---|
| Purpose | Define a contract | Share implementation among related classes |
| Code | No method bodies | Can contain implemented methods |
| State | Constants only (property declarations in PHP 8.4, with hooks) | Properties and constructors |
| How many per class | Many | One |
A common design uses both: an interface as the type everyone depends on, and an optional abstract base class that implements the interface and provides shared code for implementations that want it.
Naming
PHP's own interfaces use adjectives (Countable, Stringable) or nouns (Iterator). In applications, names describing the role are common: Notifier, PaymentGateway, ProductRepository. Some projects add a suffix (NotifierInterface); choose one convention and use it consistently.
Common Mistakes
Depending on Concrete Classes
Effect: OrderService that requires EmailNotifier cannot use SMS or a test fake.
Fix: Type-hint the interface.
Huge Interfaces
Effect: Implementations must provide many methods they do not need.
Fix: Split interfaces by role and combine them where needed.
Non-Public Implementations
Effect: Fatal error; interface methods must be public.
Fix: Declare implemented methods public.
Interfaces With Only One Implementation "Just in Case"
Effect: Extra files without benefit.
Fix: Introduce an interface when there are multiple implementations, a test double is needed, or a library boundary requires it.
Best Practices
- Define interfaces for roles that can have several implementations (gateways, notifiers, storage, repositories).
- Type-hint interfaces in constructors and method parameters.
- Keep interfaces small and focused; combine them through interface inheritance.
- Implement built-in interfaces (
Countable,IteratorAggregate,JsonSerializable) to integrate objects with the language. - Use fake implementations of interfaces in tests.
Official reference: https://www.php.net/manual/en/language.oop5.interfaces.php