Interfaces #

Interfaces are one of the most important tools in PHP software design — not because of their complex syntax, but because of what they represent: a contract. When a function accepts a parameter typed as an interface, it doesn’t care about the specific implementation. A Logger could write to a file, a database, Slack, or nowhere at all — as long as it implements the same interface, the calling function doesn’t need to change. This is the heart of the Dependency Inversion Principle, one of the most influential design principles in software engineering. This article covers PHP interfaces thoroughly: their syntax and rules, commonly used built-in PHP interfaces, interface inheritance, the difference from abstract classes, and how interfaces make code easier to test and change.

What an Interface Is and Why It Matters #

An interface defines what an object must be able to do, without defining how it does it. Think of an electrical outlet — its interface is two or three holes with a specific voltage specification. Whether the electricity comes from the grid, solar panels, or a diesel generator is irrelevant to the device plugged into it.

flowchart LR
    A[Calling Function / Class] -- "type: LoggerInterface" --> B[LoggerInterface]
    B --> C[FileLogger\nimplements LoggerInterface]
    B --> D[DatabaseLogger\nimplements LoggerInterface]
    B --> E[SlackLogger\nimplements LoggerInterface]
    B --> F[NullLogger\nimplements LoggerInterface]

    style B fill:#fef9c3,stroke:#ca8a04
    style A fill:#dbeafe

The caller depends on the interface (abstraction), not on concrete implementations. This lets you swap implementations without changing a single line of caller code.


Defining an Interface #

An interface is defined with the interface keyword. All methods inside it are automatically public and abstract — no need (and no permission) to write that explicitly. Interfaces can’t have method implementations, instance properties, or constructors.

<?php
interface LoggerInterface
{
    // All methods in an interface are automatically public abstract
    public function log(string $level, string $message, array $context = []): void;
    public function info(string $message, array $context = []): void;
    public function warning(string $message, array $context = []): void;
    public function error(string $message, array $context = []): void;
}

interface StorageInterface
{
    public function save(string $key, mixed $value, int $ttl = 0): bool;
    public function get(string $key): mixed;
    public function delete(string $key): bool;
    public function has(string $key): bool;
}

interface SerializableInterface
{
    public function toArray(): array;
    public function toJson(): string;
}

Interface Rules #

<?php
interface ExampleInterface
{
    // ✓ Constants — allowed
    const VERSION = '1.0';

    // ✓ Method signatures — must be public
    public function publicMethod(): void;

    // ✗ Private/protected methods — not allowed
    // private function privateMethod(): void;
    // protected function protectedMethod(): void;

    // ✗ Instance properties — not allowed
    // public string $name;

    // ✗ Constructors — not allowed
    // public function __construct();

    // ✗ Method implementations — not allowed
    // public function methodWithBody(): void { echo "this is not valid"; }
}

Implementing an Interface #

A class uses the implements keyword to declare that it fulfills an interface’s contract. The class must implement every method the interface defines, otherwise PHP throws a fatal error.

<?php
interface LoggerInterface
{
    public function log(string $level, string $message, array $context = []): void;
    public function info(string $message, array $context = []): void;
    public function warning(string $message, array $context = []): void;
    public function error(string $message, array $context = []): void;
}

// File implementation
class FileLogger implements LoggerInterface
{
    public function __construct(
        private string $path,
        private string $format = '[{time}][{level}] {message}',
    ) {}

    public function log(string $level, string $message, array $context = []): void
    {
        $line = strtr($this->format, [
            '{time}'    => date('Y-m-d H:i:s'),
            '{level}'   => strtoupper($level),
            '{message}' => $message,
        ]) . "\n";

        file_put_contents($this->path, $line, FILE_APPEND | LOCK_EX);
    }

    public function info(string $message, array $context = []): void
    {
        $this->log('info', $message, $context);
    }

    public function warning(string $message, array $context = []): void
    {
        $this->log('warning', $message, $context);
    }

    public function error(string $message, array $context = []): void
    {
        $this->log('error', $message, $context);
    }
}

// Database implementation
class DatabaseLogger implements LoggerInterface
{
    public function __construct(private \PDO $db) {}

    public function log(string $level, string $message, array $context = []): void
    {
        $stmt = $this->db->prepare(
            "INSERT INTO logs (level, message, context, created_at)
             VALUES (?, ?, ?, NOW())"
        );
        $stmt->execute([$level, $message, json_encode($context)]);
    }

    public function info(string $message, array $context = []): void
    {
        $this->log('info', $message, $context);
    }

    public function warning(string $message, array $context = []): void
    {
        $this->log('warning', $message, $context);
    }

    public function error(string $message, array $context = []): void
    {
        $this->log('error', $message, $context);
    }
}

// Empty implementation — for testing (does nothing)
class NullLogger implements LoggerInterface
{
    public function log(string $level, string $message, array $context = []): void {}
    public function info(string $message, array $context = []): void {}
    public function warning(string $message, array $context = []): void {}
    public function error(string $message, array $context = []): void {}
}

Interfaces as Type Hints #

The real power of interfaces appears when they’re used as type hints. A function accepting a LoggerInterface works with all its implementations:

<?php
class OrderService
{
    // Depends on the abstraction (interface), not a concrete implementation
    public function __construct(
        private LoggerInterface $logger,
        private \PDO            $db,
    ) {}

    public function createOrder(array $data): int
    {
        $this->logger->info("Creating a new order", ['data' => $data]);

        try {
            $stmt = $this->db->prepare(
                "INSERT INTO orders (user_id, total) VALUES (?, ?)"
            );
            $stmt->execute([$data['user_id'], $data['total']]);
            $orderId = (int) $this->db->lastInsertId();

            $this->logger->info("Order created successfully", ['id' => $orderId]);
            return $orderId;

        } catch (\Exception $e) {
            $this->logger->error("Failed to create order", ['error' => $e->getMessage()]);
            throw $e;
        }
    }
}

// In production — use FileLogger
$service = new OrderService(
    logger: new FileLogger('/var/log/orders.log'),
    db:     $pdo,
);

// In testing — use NullLogger or a mock
$serviceTest = new OrderService(
    logger: new NullLogger(),
    db:     $mockPdo,
);

// Switch the logger implementation to a database one without changing OrderService
$serviceProd = new OrderService(
    logger: new DatabaseLogger($pdo),
    db:     $pdo,
);

Implementing Multiple Interfaces #

A class can implement more than one interface at once — this is the advantage of interfaces over abstract classes for defining capabilities that are orthogonal to each other.

<?php
interface Cacheable
{
    public function getCacheKey(): string;
    public function getCacheTtl(): int;
}

interface Serializable
{
    public function toArray(): array;
    public function toJson(): string;
}

interface Validatable
{
    public function isValid(): bool;
    public function getErrors(): array;
}

// One class can implement many interfaces
class Product implements Cacheable, Serializable, Validatable
{
    private array $errors = [];

    public function __construct(
        private int    $id,
        private string $name,
        private float  $price,
        private int    $stock,
    ) {}

    // Cacheable implementation
    public function getCacheKey(): string
    {
        return "product:{$this->id}";
    }

    public function getCacheTtl(): int
    {
        return 3600; // 1 hour
    }

    // Serializable implementation
    public function toArray(): array
    {
        return [
            'id'    => $this->id,
            'name'  => $this->name,
            'price' => $this->price,
            'stock' => $this->stock,
        ];
    }

    public function toJson(): string
    {
        return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
    }

    // Validatable implementation
    public function isValid(): bool
    {
        $this->errors = [];

        if (empty(trim($this->name))) {
            $this->errors[] = "Product name must not be empty";
        }
        if ($this->price < 0) {
            $this->errors[] = "Price must not be negative";
        }
        if ($this->stock < 0) {
            $this->errors[] = "Stock must not be negative";
        }

        return empty($this->errors);
    }

    public function getErrors(): array
    {
        return $this->errors;
    }
}

$product = new Product(1, 'Laptop', 15_000_000, 5);

// Can be used in any context requiring one of its interfaces
var_dump($product instanceof Cacheable);    // true
var_dump($product instanceof Serializable); // true
var_dump($product instanceof Validatable);  // true

// A function accepting Cacheable works for every Cacheable object
function saveToCache(Cacheable $obj, string $data): void
{
    $key = $obj->getCacheKey();
    $ttl = $obj->getCacheTtl();
    // save to Redis/Memcached...
}

saveToCache($product, $product->toJson());

Interface Inheritance #

Interfaces can extend other interfaces — even more than one at once. This allows building granular interface hierarchies:

<?php
// Base interfaces — the minimum capabilities
interface ReadableInterface
{
    public function find(int $id): ?array;
    public function findAll(array $filter = []): array;
}

interface WritableInterface
{
    public function create(array $data): int;
    public function update(int $id, array $data): bool;
    public function delete(int $id): bool;
}

// A more specific interface — extends two interfaces at once
interface RepositoryInterface extends ReadableInterface, WritableInterface
{
    public function findByCriteria(array $criteria, int $limit = 10): array;
    public function count(array $filter = []): int;
}

// Read-only interface — only needs ReadableInterface
interface ReadOnlyRepositoryInterface extends ReadableInterface
{
    public function findPaginated(int $page, int $perPage): array;
}

// Full implementation
class ProductRepository implements RepositoryInterface
{
    public function __construct(private \PDO $db) {}

    public function find(int $id): ?array
    {
        $stmt = $this->db->prepare("SELECT * FROM products WHERE id = ?");
        $stmt->execute([$id]);
        return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
    }

    public function findAll(array $filter = []): array
    {
        // implementation with dynamic filter query
        return [];
    }

    public function create(array $data): int
    {
        $stmt = $this->db->prepare(
            "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)"
        );
        $stmt->execute([$data['name'], $data['price'], $data['stock']]);
        return (int) $this->db->lastInsertId();
    }

    public function update(int $id, array $data): bool
    {
        $stmt = $this->db->prepare(
            "UPDATE products SET name = ?, price = ?, stock = ? WHERE id = ?"
        );
        return $stmt->execute([$data['name'], $data['price'], $data['stock'], $id]);
    }

    public function delete(int $id): bool
    {
        $stmt = $this->db->prepare("DELETE FROM products WHERE id = ?");
        return $stmt->execute([$id]);
    }

    public function findByCriteria(array $criteria, int $limit = 10): array
    {
        return [];
    }

    public function count(array $filter = []): int
    {
        return 0;
    }
}

Important Built-in PHP Interfaces #

PHP provides many built-in interfaces that — when implemented — let your objects integrate directly with PHP language features like foreach, count(), string casting, and more.

Countable — Making Objects count()-able #

<?php
class Cart implements \Countable
{
    private array $items = [];

    public function add(array $product): void
    {
        $this->items[] = $product;
    }

    // Must be implemented: returns the number of elements
    public function count(): int
    {
        return count($this->items);
    }
}

$cart = new Cart();
$cart->add(['name' => 'Laptop', 'price' => 15_000_000]);
$cart->add(['name' => 'Mouse',  'price' => 250_000]);

echo count($cart); // 2 — PHP's built-in count() works!

Iterator — Making Objects foreach-able #

<?php
class ProductCollection implements \Iterator
{
    private int $position = 0;

    public function __construct(private array $items = []) {}

    // The current element
    public function current(): mixed
    {
        return $this->items[$this->position];
    }

    // The current key/index
    public function key(): int
    {
        return $this->position;
    }

    // Move to the next element
    public function next(): void
    {
        $this->position++;
    }

    // Rewind to the beginning
    public function rewind(): void
    {
        $this->position = 0;
    }

    // Is the current position valid?
    public function valid(): bool
    {
        return isset($this->items[$this->position]);
    }
}

$collection = new ProductCollection([
    ['name' => 'Laptop',  'price' => 15_000_000],
    ['name' => 'Monitor', 'price' => 5_000_000],
    ['name' => 'Mouse',   'price' => 250_000],
]);

// foreach works directly on the object
foreach ($collection as $index => $product) {
    echo "$index: {$product['name']} — Rp " . number_format($product['price']) . "\n";
}

IteratorAggregate — Easier Than Iterator #

Implementing the full Iterator requires 5 methods. IteratorAggregate only requires 1 — getIterator(), which returns something iterable:

<?php
class SimpleProductCollection implements \IteratorAggregate, \Countable
{
    private array $items = [];

    public function add(array $product): void
    {
        $this->items[] = $product;
    }

    // Just implement this one
    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->items);
    }

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

$collection = new SimpleProductCollection();
$collection->add(['name' => 'Laptop']);
$collection->add(['name' => 'Mouse']);

foreach ($collection as $product) {
    echo $product['name'] . "\n";
}

echo count($collection); // 2

Stringable — Objects Convertible to String #

<?php
class UserName implements \Stringable
{
    public function __construct(
        private string $first,
        private string $last,
    ) {}

    // Must be implemented
    public function __toString(): string
    {
        return trim("{$this->first} {$this->last}");
    }

    public function initials(): string
    {
        return strtoupper($this->first[0] . $this->last[0]);
    }
}

$name = new UserName('Budi', 'Santoso');
echo $name;              // "Budi Santoso" — via __toString
echo "Hello, $name!";    // "Hello, Budi Santoso!" — interpolation works
echo $name->initials();  // "BS"

// A function accepting string|Stringable
function greet(string|\Stringable $name): string
{
    return "Welcome, $name!";
}

echo greet("Budi");  // via a plain string
echo greet($name);   // via a Stringable object

ArrayAccess — Objects Accessible Like Arrays #

<?php
class Config implements \ArrayAccess
{
    private array $data = [];

    public function __construct(array $data = [])
    {
        $this->data = $data;
    }

    public function offsetExists(mixed $offset): bool
    {
        return isset($this->data[$offset]);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->data[$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        if ($offset === null) {
            $this->data[] = $value;
        } else {
            $this->data[$offset] = $value;
        }
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->data[$offset]);
    }
}

$config = new Config(['host' => 'localhost', 'port' => 3306]);

// Used like a normal array
echo $config['host'];         // localhost
$config['database'] = 'mydb'; // set a value
echo $config['database'];     // mydb
unset($config['port']);
var_dump(isset($config['port'])); // false

Interface vs Abstract Class #

This is one of the most frequently asked design questions. Both define contracts, but in different ways:

AspectInterfaceAbstract Class
Method implementations✗ None✓ Allowed
Instance properties✗ Not allowed✓ Allowed
Constructors✗ Not allowed✓ Allowed
Method visibilityAlways publicFree
Constants✓ Allowed✓ Allowed
Number that can be implemented/extendedManyOnly one
RepresentsCapability / contractA base type with partial implementation
flowchart TD
    A{Is there shared\nimplementation?} -- Yes --> B{Is this an\n'is-a' relationship?}
    B -- Yes --> C[Abstract Class\nExample: Vehicle → Car]
    B -- No --> D[Composition / Trait\nnot inheritance]
    A -- No --> E{Does one class need\nmany different contracts?}
    E -- Yes --> F[Interfaces\nExample: Cacheable + Serializable]
    E -- No --> G{Needed for\ndependency injection?}
    G -- Yes --> F
    G -- No --> H[Either works —\nAbstract or Interface]
<?php
// ✓ Interfaces — for contracts implementable by many classes
// that have no 'is-a' relationship
interface Notifiable
{
    public function sendNotification(string $message): bool;
}

// Email, SMS, Push Notification can all be "notifiable"
// but there's no hierarchy between them
class EmailNotifier implements Notifiable { /* ... */ }
class SmsNotifier implements Notifiable { /* ... */ }
class PushNotifier implements Notifiable { /* ... */ }

// ✓ Abstract class — for a shared base with shareable implementation
abstract class BaseRepository
{
    public function __construct(protected \PDO $db) {}

    // Shared implementation available to all subclasses
    protected function query(string $sql, array $params = []): \PDOStatement
    {
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt;
    }

    // Contracts each subclass must implement
    abstract public function find(int $id): ?array;
    abstract public function tableName(): string;

    // Template method using abstract methods
    public function delete(int $id): bool
    {
        $table = $this->tableName();
        return $this->query("DELETE FROM $table WHERE id = ?", [$id])
                    ->rowCount() > 0;
    }
}

// ✓ Combining both — the most expressive
interface ProductRepositoryInterface extends ReadableInterface, WritableInterface
{
    public function findByCategory(string $category): array;
}

abstract class AbstractProductRepository implements ProductRepositoryInterface
{
    // Shared implementation for find() and findAll()
    // Write methods remain abstract for concrete implementations
}

class MysqlProductRepository extends AbstractProductRepository
{
    // MySQL-specific implementation
}

Dependency Inversion with Interfaces #

Interfaces are the key to applying the Dependency Inversion Principle (DIP) — one of the SOLID principles. The principle: high-level modules must not depend on low-level modules; both must depend on abstractions.

<?php
// Without interfaces — tight coupling
class OrderController
{
    private MySqlOrderRepository $repo;     // depends directly on MySQL!
    private SmtpMailer $mailer;             // depends directly on SMTP!

    public function __construct()
    {
        $this->repo   = new MySqlOrderRepository(); // hard-coded
        $this->mailer = new SmtpMailer();           // hard-coded
    }
    // Can't be tested without a real MySQL connection and SMTP
    // Can't switch to PostgreSQL or another mailer without editing this class
}

// ---

// With interfaces — loose coupling
interface OrderRepositoryInterface
{
    public function save(array $data): int;
    public function find(int $id): ?array;
}

interface MailerInterface
{
    public function send(string $to, string $subject, string $body): bool;
}

class OrderController
{
    // Depends on abstractions, not implementations
    public function __construct(
        private OrderRepositoryInterface $repo,
        private MailerInterface          $mailer,
        private LoggerInterface          $logger,
    ) {}

    public function createOrder(array $data): array
    {
        $this->logger->info("Creating order", ['data' => $data]);

        $orderId = $this->repo->save($data);

        $this->mailer->send(
            $data['email'],
            "Order Confirmation #$orderId",
            "Thank you for your order!"
        );

        return ['id' => $orderId, 'status' => 'created'];
    }
}

// In production
$controller = new OrderController(
    repo:   new MySqlOrderRepository($pdo),
    mailer: new SmtpMailer($smtpConfig),
    logger: new FileLogger('/var/log/orders.log'),
);

// In testing — everything replaced with simple implementations
$controller = new OrderController(
    repo:   new InMemoryOrderRepository(),
    mailer: new FakeMailer(),
    logger: new NullLogger(),
);
// Tests run without a database, SMTP, or file system

Summary #

  • Interfaces define contracts — what an object must be able to do, not how it does it. All interface methods are automatically public abstract.
  • One class can implement many interfaces — this is the main advantage of interfaces over abstract classes for defining orthogonal capabilities like Cacheable, Serializable, Validatable.
  • Interfaces as type hints are the key to Dependency Inversion — a function accepting an interface works with all its implementations without needing changes.
  • Interfaces can extend other interfaces — even more than one. Use this to build granular contract hierarchies: ReadableInterface, WritableInterface, and RepositoryInterface extends ReadableInterface, WritableInterface.
  • Important built-in PHP interfaces: Countable (to be count()-able), IteratorAggregate (to be foreach-able), Stringable (to be usable as a string), ArrayAccess (to be accessible like an array).
  • Interface vs Abstract Class: use interfaces for capability contracts implementable by unrelated classes. Use abstract classes when there’s shared implementation to distribute among classes in one hierarchy.
  • NullLogger, InMemoryRepository, FakeMailer — no-op or in-memory interface implementations are the standard pattern for fast testing without real infrastructure.
  • Dependency Inversion — a high-level class (OrderController) depends on interfaces (LoggerInterface), not concrete implementations (FileLogger). This makes code easy to test, easy to change, and loosely coupled.

← Previous: Classes   Next: Traits →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact