SOLID and DRY Recommended
Prefer composition over inheritance
Reuse behaviour by holding an object that does the job, not by extending a class. Inherit only when the child truly is a kind of the parent.
How to do it
Put shared behaviour in a small object or function and pass it in through the constructor. Keep inheritance at most two levels deep. Mark classes final or sealed unless they are built to be extended. A framework base class you must extend, such as a model or controller, counts as level zero. Every subclass keeps the parent's contract.
Example
// Bad: a report extends a mailer only to reuse send()
class SalesReport extends Mailer { /* ... */ }
// Good: the report is given a mailer and uses it
class SalesReport {
constructor(private readonly mailer: Mailer) {}
send(to: EmailAddress) { this.mailer.send(to, this.render()); }
}
// Good: final by default, and the mailer is passed in
final class SalesReport
{
public function __construct(private readonly Mailer $mailer) {}
public function send(EmailAddress $to): void { $this->mailer->send($to, $this->render()); }
}
How to check
A linter fails on deep inheritance. Examples are Pylint too-many-ancestors and PHPMD DepthOfInheritance, set to fail above two levels. Pylint ignored-parents skips the framework's own classes. PHP-CS-Fixer final_class makes classes final by default, as Kotlin already does.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers architecture playbook: https://agentwranglers.se/playbook/architecture/solid-and-dry/composition-over-inheritance.