PHP 7: Maintaining and Upgrading Legacy Apps · Getting Legacy Code Under Test
Seams and Breaking Dependencies
Create seams so hard-to-test code can be isolated.
A seam is a place where you can change behavior without editing the code under test — for example by passing an object instead of creating it inside, or by overriding a method in a test subclass. Techniques from Working Effectively with Legacy Code: parameterize constructor/method, extract and override call, wrap static/global calls in an injectable adapter, and extract interface.
<?php
// Before: impossible to test without sending real email
class OrderProcessor {
public function complete($orderId) {
// ...
mail($customerEmail, 'Thanks!', $body);
}
}
// After: seam via an injectable mailer, defaulting to old behavior
interface Mailer { public function send(string $to, string $subject, string $body): void; }
final class NativeMailer implements Mailer {
public function send(string $to, string $subject, string $body): void { mail($to, $subject, $body); }
}
class OrderProcessor {
private $mailer;
public function __construct(Mailer $mailer = null) { $this->mailer = $mailer ?? new NativeMailer(); }
public function complete(int $orderId): void {
// ...
$this->mailer->send($customerEmail, 'Thanks!', $body);
}
}
Common pitfalls
- Large refactors before having any tests.
- Default-argument seams left forever instead of proper wiring later.
- Mocking everything instead of introducing a few meaningful seams.
Key points
- Seams let tests substitute behavior
- Wrap globals/static calls in adapters
- Keep old call sites working while introducing seams
Further reading: Working Effectively with Legacy Code
Practice questions
What is 'extract and override call'?
Answer
Move a problematic call (e.g., to a web service) into its own protected method, then in a test subclass override that method to return a canned value.
Why give a new constructor parameter a default value during legacy refactoring?
Answer
So existing callers keep working unchanged while tests can inject a fake; later you can wire the dependency properly and remove the default.
Open in the app to mark lessons complete, take notes and track your progress.