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.
| Concept | What it protects | Decision rule |
|---|---|---|
| Interface | Stable caller contract | Describe a capability with no storage assumptions. |
| Composition | Replaceable behavior | Prefer it when parts vary independently. |
| Trait | Small method reuse | Use 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.
- Describe the polymorphic collaboration 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
Swap notification channels
The enrollment service depends only on the capability it uses.
<?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.
<?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
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
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.