Passing money as an integer means every function taking it validates it, and every one of those validations needs a test — a type that refuses to exist in an invalid state moves all of that to one place.
final class Money
{
private $cents;
private $currency;
public function __construct(int $cents, string $currency)
{
if ($cents < 0) {
throw new InvalidArgumentException('negative');
}
$this->cents = $cents;
$this->currency = $currency;
}
public function add(Money $other): self
{
$this->assertSameCurrency($other);
return new self($this->cents + $other->cents, $this->currency);
}
}
Immutability is what makes it safe to pass around — add returning a new instance means nothing holding a reference is surprised. The currency check catches the bug integer arithmetic cannot: adding pounds to euros. The objection is always that it is more code, and the answer is that it is less code than the validation and the tests it replaces, spread across the twelve places that handled a raw integer.