SOLID Principles in Practice
Apply the five SOLID principles to real PHP classes.
SOLID Principles in Practice is a free PHP Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why SOLID
SOLID is five design principles that keep object-oriented PHP flexible, testable and resistant to rot. They are not rules to follow blindly but heuristics that reduce coupling and clarify responsibilities. In this lesson we apply each principle to concrete PHP classes you would actually ship.
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
Single Responsibility
A class should have one reason to change. A class that builds a report, formats it as HTML, and emails it has three reasons to change. Split persistence, formatting and transport.
Below, Invoice only models data; rendering and saving live elsewhere.
<?php
final class Invoice {
public function __construct(
public readonly string $number,
public readonly int $cents
) {}
public function total(): float { return $this->cents / 100; }
}
final class InvoiceRenderer {
public function toText(Invoice $i): string {
return sprintf('Invoice %s: $%.2f', $i->number, $i->total());
}
}
$i = new Invoice('INV-1', 12500);
echo (new InvoiceRenderer())->toText($i), PHP_EOL;
SRP: spotting the smell
The classic SRP violation is a class whose name contains and or a Manager that does everything. Watch for methods that change for unrelated business reasons. If a tax-rule change and a PDF-layout change both touch the same class, it has too many responsibilities.
Open/Closed Principle
Software entities should be open for extension, closed for modification. Adding a new discount type should not force edits to a giant switch. Use polymorphism: each rule is its own class implementing a shared interface.
<?php
interface Discount { public function apply(float $total): float; }
final class PercentOff implements Discount {
public function __construct(private float $pct) {}
public function apply(float $t): float { return $t * (1 - $this->pct / 100); }
}
final class FlatOff implements Discount {
public function __construct(private float $amount) {}
public function apply(float $t): float { return max(0, $t - $this->amount); }
}
function checkout(float $total, Discount ...$discounts): float {
foreach ($discounts as $d) { $total = $d->apply($total); }
return $total;
}
echo checkout(100, new PercentOff(10), new FlatOff(5)), PHP_EOL; // 85
Liskov Substitution
Subtypes must be usable anywhere their base type is expected, without surprising the caller. The infamous example: Square extends Rectangle breaks LSP because setting width must not silently change height. Prefer modelling them as separate types behind a common Shape interface.
<?php
interface Shape { public function area(): float; }
final class Rectangle implements Shape {
public function __construct(private float $w, private float $h) {}
public function area(): float { return $this->w * $this->h; }
}
final class Square implements Shape {
public function __construct(private float $side) {}
public function area(): float { return $this->side ** 2; }
}
$shapes = [new Rectangle(2, 3), new Square(4)];
foreach ($shapes as $s) { echo $s->area(), PHP_EOL; }
LSP and contracts
LSP also constrains method contracts. A subtype may weaken preconditions and strengthen postconditions, never the reverse. Throwing a new exception type the base never declared, or returning null where the base guarantees an object, violates substitutability. PHP's covariant return types and contravariant parameter types enforce part of this at the type level.
Interface Segregation
Clients should not depend on methods they do not use. A fat Worker interface with work() and eat() forces a RobotWorker to stub eat(). Split into focused role interfaces so each implementer only commits to what it truly does.
<?php
interface Workable { public function work(): string; }
interface Feedable { public function eat(): string; }
final class Human implements Workable, Feedable {
public function work(): string { return 'coding'; }
public function eat(): string { return 'lunch'; }
}
final class Robot implements Workable {
public function work(): string { return 'welding'; }
}
foreach ([new Human(), new Robot()] as $w) { echo $w->work(), PHP_EOL; }
Dependency Inversion
High-level policy should depend on abstractions, not concrete details. Inject an interface, not a hard-coded class. This lets you swap a real mailer for a fake in tests and a different vendor in production without touching the consumer.
<?php
interface Mailer { public function send(string $to, string $body): void; }
final class SmtpMailer implements Mailer {
public function send(string $to, string $body): void {
echo "SMTP -> $to: $body" . PHP_EOL;
}
}
final class SignupService {
public function __construct(private Mailer $mailer) {}
public function register(string $email): void {
$this->mailer->send($email, 'Welcome!');
}
}
(new SignupService(new SmtpMailer()))->register('a@b.com');
DIP and the container
Dependency Inversion is the principle; dependency Injection is one technique to satisfy it. A DI container (PSR-11) wires the concrete SmtpMailer to the Mailer abstraction at the composition root. The consumer never says new SmtpMailer(), so the direction of source-code dependency points toward the abstraction, not the detail.
<?php
// Composition root wiring (pseudo-container)
$bindings = [
Mailer::class => fn() => new SmtpMailer(),
];
$resolve = fn(string $id) => $bindings[$id]();
$service = new SignupService($resolve(Mailer::class));
$service->register('user@example.com');
SOLID together
The principles reinforce each other. ISP keeps interfaces small so DIP injections stay focused; OCP relies on polymorphism that LSP keeps safe; SRP gives you the small classes that make all of this possible. Aim for cohesion within, loose coupling between.
- Don't over-abstract a single-use class.
- Introduce an interface when a second implementation or a test double appears.
Pragmatism
SOLID is a means, not an end. A premature interface with one implementation adds indirection for no payoff ("speculative generality"). Apply the principles when change actually arrives or is clearly imminent. Refactoring toward SOLID is cheap once you have tests; refactoring blindly toward it on day one is waste.
Quick Check
Which principle does the Square/Rectangle inheritance problem violate?
Recap
You applied all five SOLID principles to real PHP:
- SRP: one reason to change per class.
- OCP: extend via new polymorphic classes, not edits to a switch.
- LSP: subtypes honor the base contract.
- ISP: small role interfaces over fat ones.
- DIP: depend on abstractions, inject details at the composition root.
Use them to guide refactoring, not as ceremony.
Frequently asked questions
Is the “SOLID Principles in Practice” lesson free?
Yes — the full text of “SOLID Principles in Practice” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.
What will I learn in “SOLID Principles in Practice”?
Apply the five SOLID principles to real PHP classes. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start PHP Academy?
No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “SOLID Principles in Practice” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this PHP Academy lesson?
Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.