Programming to a contract
A checkout needs to take payment. Today you use one payment provider; next year you may add another, and in tests you want a fake one that never charges anybody. If the checkout code calls one provider's class directly, every change means rewriting the checkout.
An interface solves this by describing what a payment gateway can do without saying how. The checkout depends only on that description, and any class that fits it can be plugged in.
Defining and implementing an interface
An interface lists method signatures, with no bodies. A class that implements it must provide every one of those methods:
<?php
interface PaymentGateway
{
public function charge(int $paise, string $reference): string;
}
class FakeGateway implements PaymentGateway
{
public function charge(int $paise, string $reference): string
{
return "fake-txn-$reference";
}
}
class LoggingGateway implements PaymentGateway
{
public function __construct(private PaymentGateway $inner) {}
public function charge(int $paise, string $reference): string
{
echo "[log] charging " . number_format($paise / 100, 2) . " for $reference\n";
return $this->inner->charge($paise, $reference);
}
}
function checkout(PaymentGateway $gateway, int $paise, string $orderId): void
{
$transaction = $gateway->charge($paise, $orderId);
echo "Order $orderId paid, transaction $transaction\n";
}
checkout(new FakeGateway(), 12700, 'ORD-1042');
checkout(new LoggingGateway(new FakeGateway()), 8950, 'ORD-1043');Order ORD-1042 paid, transaction fake-txn-ORD-1042
[log] charging 89.50 for ORD-1043
Order ORD-1043 paid, transaction fake-txn-ORD-1043checkout() asks for a PaymentGateway, so it accepts any class that implements it: the fake one here, a real provider's class in production, or a wrapper that adds logging. The real gateway class would call the provider's API inside charge(), and nothing in checkout() would change.
That idea, depending on an interface rather than a specific class, is at the heart of how frameworks such as Laravel are built.
The contract is enforced
If a class says it implements an interface but leaves a method out, or gives it an incompatible signature, PHP refuses to load the class:
<?php
interface Notifier
{
public function send(string $to, string $message): bool;
}
class SmsNotifier implements Notifier
{
public function sendText(string $to, string $message): bool
{
return true;
}
}Fatal error: Class SmsNotifier contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Notifier::send) in /path/to/script.php on line 7The message even tells you which method is missing. Interfaces can also contain constants, and one interface can extend others.
Several interfaces at once
A class can extend only one parent, but it can implement any number of interfaces, each describing one ability. PHP itself ships many, such as Countable (works with count()), Stringable (has __toString()) and JsonSerializable (controls json_encode()):
<?php
class Cart implements Countable, JsonSerializable
{
private array $items = [];
public function add(string $product, int $qty): void
{
$this->items[$product] = ($this->items[$product] ?? 0) + $qty;
}
public function count(): int
{
return array_sum($this->items);
}
public function jsonSerialize(): array
{
return ['items' => $this->items, 'count' => $this->count()];
}
}
$cart = new Cart();
$cart->add('Mug', 2);
$cart->add('Keyboard', 1);
echo count($cart) . "\n";
echo json_encode($cart) . "\n";
var_dump($cart instanceof Countable);3
{"items":{"Mug":2,"Keyboard":1},"count":3}
bool(true)Abstract classes
An abstract class sits between an interface and a normal class. It can contain real properties and methods that its children share, and it can declare abstract methods, with no body, that every child must write. It cannot be created with new itself:
<?php
abstract class Report
{
public function __construct(protected array $rows) {}
abstract protected function title(): string;
abstract protected function formatRow(array $row): string;
public function render(): string
{
$out = $this->title() . "\n" . str_repeat('=', strlen($this->title())) . "\n";
foreach ($this->rows as $row) {
$out .= $this->formatRow($row) . "\n";
}
return $out;
}
}
class StockReport extends Report
{
protected function title(): string
{
return 'Stock levels';
}
protected function formatRow(array $row): string
{
return sprintf('%-20s %4d', $row['name'], $row['stock']);
}
}
$rows = [['name' => 'Mechanical Keyboard', 'stock' => 60], ['name' => 'Headphones', 'stock' => 25]];
echo (new StockReport($rows))->render();Stock levels
============
Mechanical Keyboard 60
Headphones 25render() is written once, in the abstract class, and defines the overall steps; each report only fills in its own title and row format. This is called the template method pattern. Trying new Report($rows) would throw an Error: "Cannot instantiate abstract class Report".
Interface or abstract class?
| Interface | Abstract class | |
|---|---|---|
| Contains code | No (only signatures and constants) | Yes, shared methods and properties |
| A class can use | Many | Only one (it is a parent class) |
| Best for | A contract that unrelated classes can fulfil | A family of closely related classes sharing behaviour |
They also combine well: an interface for the contract, and an optional abstract base class that implements part of it for convenience. Type-hint against the interface.
Common mistakes
- Type-hinting a specific class when an interface would let you swap implementations.
- Putting code in an interface, which is not allowed (use an abstract class or a trait).
- Trying to instantiate an abstract class.
- Creating interfaces with one implementation "just in case". Add one when you have, or clearly expect, a second implementation or need a fake for tests.
Exercise
Write an interface Shape with area(): float and name(): string, and two classes Rectangle and Circle that implement it. Write a function largest(Shape ...$shapes): Shape and use it to print the name and area (rounded to 2 decimals) of the largest of a 3 by 4 rectangle and a circle of radius 2.
Show one possible answer
<?php
interface Shape
{
public function area(): float;
public function name(): string;
}
class Rectangle implements Shape
{
public function __construct(private float $width, private float $height) {}
public function area(): float { return $this->width * $this->height; }
public function name(): string { return "rectangle"; }
}
class Circle implements Shape
{
public function __construct(private float $radius) {}
public function area(): float { return M_PI * $this->radius ** 2; }
public function name(): string { return "circle"; }
}
function largest(Shape ...$shapes): Shape
{
usort($shapes, fn(Shape $a, Shape $b) => $b->area() <=> $a->area());
return $shapes[0];
}
$winner = largest(new Rectangle(3, 4), new Circle(2));
echo $winner->name() . ": " . round($winner->area(), 2) . "\n";circle: 12.57Key points
- An interface lists methods a class promises to have, with no code. A class can implement many interfaces.
- Type-hinting against an interface lets a function accept any implementation, so you can swap one for another.
- An abstract class can hold shared code and declare abstract methods that children must write; it cannot be instantiated.
- Choose an interface for a contract and an abstract class for shared behaviour within one family.