Depend on Abstractions
The Dependency Inversion Principle says: high-level code — your business logic — should not depend on low-level details like a specific database or file format. Both should depend on an abstraction: in PHP, an interface.
The "inversion" is the direction of the arrow. Instead of the service pointing at one concrete storage, the service points at an interface — and every storage points at that same interface from the other side.
Hard-Wired to the Details
This user service creates and uses a concrete DiskStore directly. It works — but the service is welded to one storage. A different store, a faster store, a fake store for tests: every one of them means editing the service itself.
The service welded to its storage
UserService builds its own DiskStore inside. High-level registration logic now depends on a low-level detail — the file on disk.
Output: PHP script
<?php
class DiskStore {
public function save(string $user): void {
echo "writing $user to file users.txt on disk\n";
}
}
// UserService builds its own storage — welded to the detail.
class UserService {
private DiskStore $store;
public function __construct() {
$this->store = new DiskStore();
}
public function register(string $user): void {
echo "registering user: $user\n";
$this->store->save($user);
}
}
$service = new UserService();
$service->register("ana");
Invert the Dependency
The fix: the service asks for a Storage interface in its constructor and never learns which one it got. The disk store and the memory store both implement the interface.
Now the arrow points from both sides to the abstraction — and swapping the storage is a one-line change at the place where the service is created, not inside it.
The service accepts an interface
Same registration logic, two different storages. new UserService(new DiskStore()) or new UserService(new MemoryStore()) — the service code itself never changes.
Output: PHP script
<?php
// The abstraction both sides depend on.
interface Storage {
public function save(string $user): void;
}
class DiskStore implements Storage {
public function save(string $user): void {
echo "writing $user to file users.txt on disk\n";
}
}
class MemoryStore implements Storage {
public function save(string $user): void {
echo "keeping $user in memory\n";
}
}
class UserService {
public function __construct(private Storage $store) {
}
public function register(string $user): void {
echo "registering user: $user\n";
$this->store->save($user);
}
}
$onDisk = new UserService(new DiskStore());
$onDisk->register("ana");
$inMemory = new UserService(new MemoryStore());
$inMemory->register("ben");
The payoff: testing without a disk
Because the service depends on the abstraction, a test can hand it a memory store and check the result instantly — no files, no cleanup. Dependency inversion is what makes code easy to unit-test.
Output: PHP script
<?php
interface Storage {
public function save(string $user): void;
}
// MemoryStore records what was saved — perfect for checks.
class MemoryStore implements Storage {
public array $users = [];
public function save(string $user): void {
$this->users[] = $user;
}
}
class UserService {
public function __construct(private Storage $store) {
}
public function register(string $user): void {
$this->store->save($user);
}
}
$store = new MemoryStore();
$service = new UserService($store);
$service->register("ana");
$service->register("ben");
if (count($store->users) === 2) {
echo "check passed: 2 users saved\n";
}
echo "saved users: ", implode(", ", $store->users), "\n";