Phase 3 · Object-Oriented & Modern PHPModule 16~48 min read

Inheritance, Interfaces & Traits

Create flexible contracts with interfaces and composition while using inheritance and traits deliberately.

What you'll learn

Use interfaces to separate what a service needs from how the work is performed. You will compare inheritance, traits, and composition, then choose the smallest coupling that supports change.

By the end of this lesson, you'll be able to:

  • Define and consume an interface
  • Use polymorphism without type switches
  • Choose composition, inheritance, or a trait deliberately

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
InterfaceStable caller contractDescribe a capability with no storage assumptions.
CompositionReplaceable behaviorPrefer it when parts vary independently.
TraitSmall method reuseUse sparingly; never hide a major dependency in one.

Professional workflow

Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.

  1. Describe the polymorphic collaboration 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

Swap notification channels

The enrollment service depends only on the capability it uses.

Notifier.php
<?php
interface Notifier { public function send(string $recipient, string $message): void; }

final class EmailNotifier implements Notifier
{
    public function send(string $recipient, string $message): void
    {
        echo "Email to $recipient: $message\n";
    }
}

final readonly class EnrollmentService
{
    public function __construct(private Notifier $notifier) {}
    public function enroll(string $email): void
    {
        // persist enrollment...
        $this->notifier->send($email, 'Enrollment confirmed');
    }
}

Reuse a focused timestamp behavior

A tiny trait can share a mechanical implementation; the domain class still controls when it runs.

RecordsUpdates.php
<?php
trait RecordsUpdates
{
    private ?DateTimeImmutable $updatedAt = null;
    protected function touch(): void { $this->updatedAt = new DateTimeImmutable(); }
    public function updatedAt(): ?DateTimeImmutable { return $this->updatedAt; }
}

final class Lesson
{
    use RecordsUpdates;
    public function revise(): void { /* change content */ $this->touch(); }
}

Production practice

Contract

Keep interfaces owned by the caller and narrow enough to describe one role.

Verification

Run the same contract test against every implementation; use a fake at the consuming service boundary.

Operations

Ensure implementations expose meaningful failures instead of silently swallowing transport errors.

Common failure mode

Deep inheritance trees make behavior depend on distant parent classes. A shared noun is not enough reason to inherit.

Independent workshop

Build a report exporter supporting CSV and JSON through one interface, then decorate it with audit logging.

Your finished workshop must include:

  • Exporter interface plus two implementations
  • Logging decorator using composition
  • Contract tests shared by both exporters

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

  • Interfaces enable substitution.
  • Composition keeps dependencies visible.
  • Inheritance models a real is-a relationship.
  • Traits are implementation reuse, not architectural boundaries.

Quick check

1. What should a consumer type-hint for replaceable behavior?

2. When is composition strongest?

3. What is a trait primarily for?

Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.