The key phrase here is "composition vs inheritance".
Inheritance represents an "is-a" relationship. You might say that an HTMLEMail
is an Email
- it's the same kind of thing, but with extra or different behaviour. But it doesn't make sense to say Checkout
is an Emailer
- they're two different things, neither is a type of the other.
Composition represents a "has-a" relationship. A Checkout
might have an Emailer
, which it uses to send e-mails. So they are different classes that you "compose" together. That basically means that rather than $this->sendEmail()
, you would write $emailer->sendEmail()
, where $emailer
is another object that's come from somewhere.
The cleanest way to implement "composition" is via "dependency injection", which is mostly a fancy way of saying "passing objects to other objects".
That might be passing it into the individual method:
class Checkout extends Base {
public function doCheckout($emailer) {
// Call send email
$emailer->sendEmail('email@example.com', 'Hello');
}
}
More flexibly, since you might want to use in multiple methods, passing it into the constructor and storing it on a property:
class Checkout extends Base {
private Emailer $emailer;
public function __construct(Emailer $emailer) {
$this->emailer = $emailer;
}
public function doCheckout() {
// Call send email
$this->emailer->sendEmail('email@example.com', 'Hello');
}
}
This kind of setting of properties in the constructor is so common that PHP 8 has a special short-hand for it called "Constructor Property Promotion":
class Checkout extends Base {
// note the "private" keyword next to the argument
public function __construct(private Emailer $emailer) {
}
public function doCheckout() {
// Call send email
$this->emailer->sendEmail('email@example.com', 'Hello');
}
}
More flexibly still, you can depend on an interface
, which means "anything that has the right behaviour can be passed in here, I'll decide on the exact implementation later".
The idea is that you can write the Checkout
class without knowing what the inside of the Emailer
class is going to look like. You just need to know that sendEmail
requires certain arguments, and returns certain things when it's done. Then you can have one implementation that uses a username and password to talk to an SMTP server, one implementation that uses an API key to use a web service, and a third that just logs to a local file for test purposes. The Checkout
class doesn't need to know how to create each one, or when to use them, it just waits for one to be provided.
Somewhere else, you have some co-ordinating code that actually knows how to create all these objects. The fancy way to do this in a larger project is with a library like PHP-DI that uses configuration and "auto-wiring" to work out what's needed when, but a simple version just looks like this:
$config = parse_config_file('config.ini');
if ( $config['mode'] === 'debug' ) {
$emailer = new DebugEmailer($config['logfiles']['email_debug']);
}
else {
$emailer = new SMTPEmailer($config['smtp']['username'], $config['smtp']['password']);
}
$checkout = new Checkout($emailer);
$checkout->doCheckout();