Lesson 79 of 100
Classes15 min5 checksPHP 8.5

Composition: build objects from collaborators

Learn to give an object small collaborators and delegate work to them instead of building deep inheritance chains, then apply it in a checkout exercise.

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.

Example
<?php
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;
Output
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.

Example
<?php
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;
Output
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.

Example
<?php
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;
Output
["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:

  1. Sum the prices.
  2. Apply the discount rule: subtract intdiv($cents * $discountPercent, 100).
  3. Apply the tax rule to the discounted amount: add intdiv($cents * $taxPercent, 100).
  4. 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.
Before you start

Functions · Arrays · Types

You’ll practice
  • Delegate to local collaborators instead of deep inheritance
  • Implement and test a deterministic PHP 8.4 function
Your task

Make checkoutTotal() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.