Phase 3 · Object-Oriented & Modern PHPModule 21~55 min read

SOLID & Essential Design Patterns

Improve changeability with SOLID, dependency injection, and a small set of patterns applied only where they earn their keep.

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.

ConceptWhat it protectsDecision rule
Single responsibilityOne reason to changeSplit policy, persistence, and presentation when they evolve separately.
Dependency inversionStable business rulesCore services depend on ports, not vendor details.
StrategyReplaceable policyUse 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.

  1. Describe the application design boundary: its inputs, outputs, invariants, and expected failures.
  2. Implement the smallest happy path behind an explicit contract.
  3. Add validation and translate low-level failures into language the caller understands.
  4. Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
  5. Refactor only after behavior is protected by a repeatable check.

Make the boundary visible

Name inputs, outputs, side effects, and failure cases before adding framework or infrastructure code. That habit keeps advanced PHP understandable as the application grows.

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.

PublishCourse.php
<?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.

Pricing.php
<?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

A pattern without a change pressure adds indirection, names, and files but no flexibility. Start concrete and extract the seam when evidence appears.

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

Run the happy path and at least two failure paths, explain one design tradeoff in a short README, and leave the code formatted and ready for review.

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.