PHP Interfaces

An interface in PHP is a contract that defines a set of public methods a class must implement, without providing any implementation itself. Interfaces let you guarantee that unrelated classes expose the same API, so code that depends on the interface can work with any class that fulfills it. They are the backbone of loosely coupled, testable PHP applications, and you will find them everywhere in modern frameworks — from Iterator and ArrayAccess in PHP’s SPL to dependency injection containers that type-hint against interfaces instead of concrete classes.

Overview: How Interfaces Work

Think of an interface as a promise about behavior, not a description of implementation. When you write interface Logger { public function log(string $message): void; }, you are telling PHP: “any class that claims to be a Logger must have a public log() method that accepts a string and returns nothing.” The interface itself never defines what log() actually does — that is left entirely to the classes that implement it.

Internally, when the Zend Engine compiles a class declaration that uses implements, it checks the class’s method table against every method signature declared in the interface (and any interfaces that interface extends). If even one required method is missing, or its visibility or signature is incompatible, PHP raises a fatal compile-time error before your script can run. This check happens once, when the class is defined — not on every method call — so implementing an interface has no runtime performance cost.

Interfaces differ from abstract classes in an important way: a class can implement as many interfaces as it wants (separated by commas), but it can only extend one parent class. This is PHP’s answer to the “diamond problem” — because interfaces never carry implementation code, there is no ambiguity about which version of a method “wins” when a class implements several interfaces. Interfaces can also declare constants using const, and starting from PHP 8.0 an interface can require a class to expose a certain static method signature as well. Since PHP 8.3, interfaces are commonly combined with readonly properties, enums, and constructor promotion to build strict, self-documenting contracts.

Syntax

The general shape of declaring and implementing an interface looks like this:

<?php
interface InterfaceName {
    public function methodOne(): string;
    public function methodTwo(int $value): void;
}

class ClassName implements InterfaceName {
    public function methodOne(): string {
        return "value";
    }

    public function methodTwo(int $value): void {
        echo $value;
    }
}
Part Meaning
interface Keyword that starts an interface declaration.
Method signatures Declared with a semicolon instead of a body — no logic allowed inside an interface.
implements Keyword a class uses to declare it fulfills one or more interfaces.
Comma-separated list A class may implement multiple interfaces: class X implements A, B, C.
extends (on an interface) An interface can extend one or more parent interfaces, inheriting their required methods.
Visibility All interface methods are implicitly public; implementing methods must also be declared public.

Examples

Example 1: A simple contract with two implementations

<?php
interface Logger {
    public function log(string $message): void;
}

class FileLogger implements Logger {
    public function log(string $message): void {
        echo "Writing to file: {$message}" . PHP_EOL;
    }
}

class ConsoleLogger implements Logger {
    public function log(string $message): void {
        echo "Console: {$message}" . PHP_EOL;
    }
}

function process(Logger $logger): void {
    $logger->log("Task started");
}

process(new FileLogger());
process(new ConsoleLogger());

Output:

Writing to file: Task started
Console: Task started

The process() function type-hints against the Logger interface, not a concrete class. That means it can accept any object that implements Logger, and you could add a DatabaseLogger or SlackLogger later without changing process() at all.

Example 2: Implementing multiple interfaces

<?php
interface Tallyable {
    public function count(): int;
}

interface Printable {
    public function printItems(): void;
}

class ShoppingCart implements Tallyable, Printable {
    private array $items = [];

    public function addItem(string $item): void {
        $this->items[] = $item;
    }

    public function count(): int {
        return count($this->items);
    }

    public function printItems(): void {
        foreach ($this->items as $index => $item) {
            $number = $index + 1;
            echo "{$number}. {$item}" . PHP_EOL;
        }
    }
}

$cart = new ShoppingCart();
$cart->addItem("Keyboard");
$cart->addItem("Mouse");
$cart->printItems();
echo "Total items: " . $cart->count() . PHP_EOL;

Output:

1. Keyboard
2. Mouse
Total items: 2

ShoppingCart satisfies two separate contracts at once. Each interface stays small and focused, which is exactly the point — a class can mix in as many narrow capabilities as it genuinely needs.

Example 3: Interface constants and combining contracts

<?php
interface HasVersion {
    const VERSION = "1.0";
    public function getVersion(): string;
}

interface Shape {
    public function area(): float;
    public function perimeter(): float;
}

class Rectangle implements Shape, HasVersion {
    public function __construct(
        private readonly float $width,
        private readonly float $height
    ) {}

    public function area(): float {
        return $this->width * $this->height;
    }

    public function perimeter(): float {
        return 2 * ($this->width + $this->height);
    }

    public function getVersion(): string {
        return self::VERSION;
    }
}

$rect = new Rectangle(4.0, 5.0);
echo "Area: " . $rect->area() . PHP_EOL;
echo "Perimeter: " . $rect->perimeter() . PHP_EOL;
echo "Version: " . $rect->getVersion() . PHP_EOL;

Output:

Area: 20
Perimeter: 18
Version: 1.0

Interface constants (HasVersion::VERSION) are inherited automatically by any implementing class and can be referenced with self::VERSION or HasVersion::VERSION. Unlike methods, constants come with a real value baked into the contract itself.

Under the Hood

When PHP parses a class that implements one or more interfaces, the engine builds an internal class entry that records every interface in its ancestry (including interfaces those interfaces extend). Before the class becomes usable, PHP walks that list and verifies that a compatible public method exists for every interface method signature. This is a compile-time structural check, similar to how a puzzle piece is checked against its slot — it either fits or PHP refuses to define the class at all.

This is also why instanceof works against interfaces just like it works against classes: $rect instanceof Shape returns true because the engine’s type table records Shape as one of Rectangle‘s ancestors, even though Rectangle never extends anything. Type-hinting a function parameter with an interface name (as in Example 1) leans on this exact same mechanism — PHP checks the argument’s type table for a matching interface at call time and throws a TypeError if it is missing.

Common Mistakes

Mistake 1: Forgetting to implement every required method

<?php
interface Shape2 {
    public function area(): float;
}

class Circle implements Shape2 {
    public function __construct(private readonly float $radius) {}
}

$circle = new Circle(3.0);
echo $circle->area();

// Fatal error: Class Circle contains 1 abstract method
// and must be declared abstract or implement the remaining methods (Shape2::area)

Implementing an interface is an all-or-nothing commitment. Add the missing method and the class compiles correctly:

<?php
interface Shape2 {
    public function area(): float;
}

class Circle implements Shape2 {
    public function __construct(private readonly float $radius) {}

    public function area(): float {
        return M_PI * $this->radius ** 2;
    }
}

$circle = new Circle(3.0);
echo round($circle->area(), 2) . PHP_EOL;

Output:

28.27

Mistake 2: Narrowing method visibility

<?php
interface Comparable2 {
    public function compareTo(self $other): int;
}

class Money implements Comparable2 {
    protected function compareTo(self $other): int {
        return 0;
    }
}

// Fatal error: Access level to Money::compareTo() must be
// public (as in interface Comparable2)

Every interface method is implicitly public, so implementations can never lower that visibility to protected or private. Fix it by matching the visibility exactly:

<?php
interface Comparable2 {
    public function compareTo(self $other): int;
}

class Money implements Comparable2 {
    public function __construct(private readonly int $cents) {}

    public function compareTo(self $other): int {
        return $this->cents <=> $other->cents;
    }
}

$a = new Money(500);
$b = new Money(750);
echo $a->compareTo($b) . PHP_EOL;

Output:

-1

Mistake 3: Trying to instantiate an interface directly

<?php
interface PaymentGateway {
    public function charge(float $amount): bool;
}

$gateway = new PaymentGateway();

// Fatal error: Cannot instantiate interface PaymentGateway

An interface has no implementation to run, so PHP will never let you create an instance of one with new. You must instantiate a concrete class that implements the interface instead, such as new StripeGateway().

Best Practices

  • Type-hint function parameters, return types, and constructor dependencies against interfaces rather than concrete classes whenever the caller only needs the contract.
  • Keep interfaces small and focused (the Interface Segregation Principle) — several narrow interfaces are easier to implement correctly than one “fat” interface with a dozen methods.
  • Name interfaces to describe a capability, often ending in -able (Comparable, Iterable, Serializable) or starting with Has (HasVersion).
  • Document expected behavior with a docblock on the interface method, since the interface itself carries no code to explain the intent.
  • Reuse PHP’s built-in SPL interfaces (Countable, IteratorAggregate, JsonSerializable, Stringable) instead of inventing your own when the standard library already defines the exact contract you need.
  • Use an abstract class instead of, or alongside, an interface when several classes need to share actual implementation code, not just a method signature.
  • Let one interface extend several others to compose bigger contracts from small, reusable pieces rather than duplicating method signatures.

Practice Exercises

  • Define a Notifiable interface with a single method notify(string $message): void. Create two classes, EmailNotifier and SmsNotifier, that implement it differently, then write a function that accepts any Notifiable and calls notify() on it.
  • Create two interfaces, Flyable (with a fly(): string method) and Swimmable (with a swim(): string method). Build a Duck class that implements both, and print the results of calling both methods.
  • Write an interface Discountable with a constant MAX_DISCOUNT = 50 and a method applyDiscount(float $percent): float. Implement it in a Product class that throws when the requested percent exceeds MAX_DISCOUNT, and returns the discounted price otherwise.

Summary

  • An interface declares public method signatures (and optionally constants) without any implementation — it is a pure contract.
  • A class implements an interface with the implements keyword and can implement multiple interfaces at once, separated by commas.
  • Interface methods must be implemented as public, and every required method must be present or PHP raises a fatal compile-time error.
  • Interfaces can extend one or more other interfaces, letting you compose larger contracts from smaller, reusable ones.
  • instanceof and type-hints work against interfaces exactly as they do against classes, because PHP tracks interfaces in each class’s internal type table.
  • Unlike abstract classes, interfaces support PHP’s version of multiple inheritance of type, without the ambiguity of conflicting implementations.