01 · THE IDEAREAD → TRY → LEARN
Two ways to reuse behavior
PHP lets a class reuse behavior in two main ways. With extends, a child class inherits the constants, properties and methods of one parent. Composition takes a different route: your class holds other objects and asks them to do part of the work. Inheritance says "is a". Composition says "has a" or "uses a".
Inheritance is a good fit for real specializations, but it has costs. A class can extend only one parent. An overriding method must keep a signature compatible with the parent's method. Every change to the parent can ripple into every child. Composition avoids this coupling because each piece stays small and can be swapped on its own.
Delegation in its simplest form
The pattern is simple: accept a collaborator in the constructor, store it in a private property, and call it through $this. Inside a method, $this is the object the method was called on.
final class Engine {
public function start(): string {
return 'engine started';
}
}
final class Car {
public function __construct(private Engine $engine) {}
public function drive(): string {
return $this->engine->start() . ', driving';
}
}
$car = new Car(new Engine());
echo $car->drive(), PHP_EOL;engine started, driving
Car does not know how an engine starts. It only knows that an Engine has a start() method. Because objects are passed by handle, the Car shares the exact instance you gave it. It does not receive a copy.
Depend on a small contract
Delegation becomes much more useful when the collaborator is typed as an interface. Any class that implements the interface can then be plugged in, and the owner never changes.
interface Formatter {
public function format(string $text): string;
}
final class UpperFormatter implements Formatter {
public function format(string $text): string {
return strtoupper($text);
}
}
final class BracketFormatter implements Formatter {
public function format(string $text): string {
return '[' . $text . ']';
}
}
final class Message {
public function __construct(private string $text, private Formatter $formatter) {}
public function render(): string {
return $this->formatter->format($this->text);
}
}
echo (new Message('hello', new UpperFormatter()))->render(), PHP_EOL;
echo (new Message('hello', new BracketFormatter()))->render(), PHP_EOL;HELLO [hello]
To get a new behavior you write a new small class. Message stays untouched. With inheritance you would probably have created UpperMessage and BracketMessage, and then struggled to combine them.
Compose many collaborators
A class can also hold a list of collaborators and run them in order. Here each step does one tiny job, and the pipeline combines them.
interface Step {
public function run(string $value): string;
}
final class TrimStep implements Step {
public function run(string $value): string {
return trim($value);
}
}
final class UpperStep implements Step {
public function run(string $value): string {
return strtoupper($value);
}
}
final class ExclaimStep implements Step {
public function run(string $value): string {
return $value . '!';
}
}
final class Pipeline {
/** @param Step[] $steps */
public function __construct(private array $steps) {}
public function run(string $value): string {
foreach ($this->steps as $step) {
$value = $step->run($value);
}
return $value;
}
}
$pipeline = new Pipeline([new TrimStep(), new UpperStep(), new ExclaimStep()]);
echo json_encode([$pipeline->run(' php '), $pipeline->run('ok')], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;["PHP!","OK!"]
The order of the array is the order of the behavior. Reordering steps changes the result without touching any step class.
Exercise contract
Implement checkoutTotal(array $prices, int $discountPercent, int $taxPercent): int. Prices are integer cents. Build it from small collaborators rather than one big block of arithmetic:
- Sum the prices.
- Apply the discount rule: subtract
intdiv($cents * $discountPercent, 100). - Apply the tax rule to the discounted amount: add
intdiv($cents * $taxPercent, 100). - Return the final integer.
The order matters because of integer rounding, so apply the discount first. An empty price list totals 0, and a 100 percent discount totals 0 whatever the tax is. Use an interface such as PriceRule, one class per rule, and a Checkout class that holds an array of rules and applies them in turn. The tests call only checkoutTotal, so how you split the classes is up to you.
Recap
- Prefer holding collaborators over extending a parent just to reuse code.
- Type collaborators as interfaces so they can be replaced.
- Pass them through the constructor and delegate with
$this->collaborator->method(). - Keep each collaborator small enough to test alone.
Functions · Arrays · Types
- Delegate to local collaborators instead of deep inheritance
- Implement and test a deterministic PHP 8.4 function