What you'll learn
Design code that changes locally. SOLID principles reveal unstable coupling; dependency injection and a handful of patterns give those seams concrete form without turning simple features into pattern collections.
By the end of this lesson, you'll be able to:
- Recognize SOLID pressure in real code
- Inject collaborators instead of constructing them internally
- Apply Strategy and Adapter where variation exists
Core mental model
Professional PHP is less about memorizing APIs and more about choosing a clear boundary for each responsibility. Use this table as a decision guide while reading the examples.
| Concept | What it protects | Decision rule |
|---|---|---|
| Single responsibility | One reason to change | Split policy, persistence, and presentation when they evolve separately. |
| Dependency inversion | Stable business rules | Core services depend on ports, not vendor details. |
| Strategy | Replaceable policy | Use when one calculation has multiple named algorithms. |
Professional workflow
Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.
- Describe the application design boundary: its inputs, outputs, invariants, and expected failures.
- Implement the smallest happy path behind an explicit contract.
- Add validation and translate low-level failures into language the caller understands.
- Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
- Refactor only after behavior is protected by a repeatable check.
Make the boundary visible
Guided code lab
Inject a persistence port
The use case coordinates domain work without knowing whether storage is PDO, an API, or a test fake.
<?php
interface CourseRepository
{
public function get(int $id): Course;
public function save(Course $course): void;
}
final readonly class PublishCourse
{
public function __construct(private CourseRepository $courses) {}
public function __invoke(int $id): void
{
$course = $this->courses->get($id);
$course->publish();
$this->courses->save($course);
}
}Replace pricing policy
New pricing rules become new implementations instead of branches scattered across checkout.
<?php
interface Pricing { public function total(int $subtotal): int; }
final readonly class RegularPricing implements Pricing
{
public function total(int $subtotal): int { return $subtotal; }
}
final readonly class PercentageDiscount implements Pricing
{
public function __construct(private int $percent) {}
public function total(int $subtotal): int
{
return (int) round($subtotal * (100 - $this->percent) / 100);
}
}Production practice
Contract
Introduce an abstraction at a real volatility boundary, and keep each interface owned by its consumer.
Verification
Test strategies through shared examples and use in-memory adapters for fast use-case tests.
Operations
Keep dependency wiring in one composition root so production implementations remain visible.
Common failure mode
Independent workshop
Refactor a checkout that directly sends email and writes PDO into a use case with ports and a discount strategy.
Your finished workshop must include:
- Small repository and notifier interfaces
- Two pricing strategies
- A composition root plus isolated tests
Definition of done
Recap & quick check
Key takeaways
- SOLID guides dependency direction.
- Injection makes collaborators explicit.
- Patterns name proven shapes, not mandatory layers.
- A composition root owns concrete wiring.
Quick check
1. Where should concrete dependencies be assembled?
2. What does Strategy encapsulate?
3. When is an interface justified?
Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.