Interfaces — Contracts & Dependency Inversion
An interface defines a contract — it specifies what methods a class must implement, without any implementation. Interfaces enable polymorphism and are the cornerstone of testable PHP code: you code against the interface, inject the implementation — swapping a real database for a test double is trivial.
Interfaces in Practice
<?php
declare(strict_types=1);
// ── Interface — a contract, no implementation ───────────────────
interface PostRepositoryInterface {
public function findById(int $id): ?Post;
public function findAll(): array;
public function findPublished(int $limit, int $offset): array;
public function save(Post $post): Post;
public function delete(int $id): bool;
public function countPublished(): int;
}
// ── Multiple interfaces on one class ───────────────────────────
interface Cacheable {
public function getCacheKey(): string;
public function getTtl(): int;
}
interface JsonSerializable {
public function toJson(): string;
}
class Post implements PostRepositoryInterface, Cacheable, JsonSerializable {
// implements PostRepositoryInterface — but Post IS the entity...
// Better: separate Repository from Entity (shown below)
public function getCacheKey(): string { return "post:{$this->id}"; }
public function getTtl(): int { return 3600; }
public function toJson(): string { return json_encode($this->toArray()); }
// PostRepositoryInterface methods omitted for brevity
}
// ── Real-world pattern — program to interface ──────────────────
interface PostRepositoryInterface {
public function findById(int $id): ?Post;
public function findAll(): array;
public function save(Post $post): Post;
public function delete(int $id): bool;
}
// Production implementation — real database
class PdoPostRepository implements PostRepositoryInterface {
public function __construct(private readonly PDO $pdo) {}
public function findById(int $id): ?Post {
$stmt = $this->pdo->prepare("SELECT * FROM posts WHERE id = :id");
$stmt->execute([":id" => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? Post::fromArray($row) : null;
}
public function findAll(): array {
$stmt = $this->pdo->query("SELECT * FROM posts ORDER BY created_at DESC");
return array_map([Post::class, "fromArray"], $stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function save(Post $post): Post { /* ... */ return $post; }
public function delete(int $id): bool { /* ... */ return true; }
}
// Test double — in-memory implementation for unit tests
class InMemoryPostRepository implements PostRepositoryInterface {
private array $posts = [];
private int $nextId = 1;
public function findById(int $id): ?Post {
return $this->posts[$id] ?? null;
}
public function findAll(): array {
return array_values($this->posts);
}
public function save(Post $post): Post {
$this->posts[$this->nextId] = $post;
$this->nextId++;
return $post;
}
public function delete(int $id): bool {
if (!isset($this->posts[$id])) return false;
unset($this->posts[$id]);
return true;
}
}
// Service depends on the INTERFACE — not the implementation
class PostService {
public function __construct(
private readonly PostRepositoryInterface $repository,
) {}
public function getPost(int $id): Post {
$post = $this->repository->findById($id);
if ($post === null) throw new PostNotFoundException($id);
return $post;
}
}
// Production: new PostService(new PdoPostRepository($pdo))
// Unit tests: new PostService(new InMemoryPostRepository())Quick Quiz
Tip
Tip
Practice Interfaces Contracts Dependency Inversion in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
PHP processes each request through the server-side engine
Practice Task
Note
Practice Task — (1) Write a working example of Interfaces Contracts Dependency Inversion from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Common Mistake
Warning
A common mistake with Interfaces Contracts Dependency Inversion is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready php code.