final Classes and Methods
final slams the door on further extension or overriding. Used deliberately, it protects invariants and communicates design intent.
The final keyword says "this ends here." A final class can't be extended; a final method can't be overridden by a subclass. It's not about being restrictive for its own sake — it's about guaranteeing that certain behaviour can never be quietly changed underneath you.
Easy example
<?php
final class Money {
public function __construct(public readonly int $cents) {}
}
// class Bonus extends Money {} // Fatal error: cannot extend final classValue objects — Money, Email, Uuid — are prime candidates for final. Their whole point is that an Email is always a valid email; a subclass could break that promise.
Example
When to use it
- Mark a SecurityToken class as final to prevent subclasses from weakening its constant-time comparison logic.
- Declare a Money class final because value-object semantics break if a subclass adds mutable state.
- Use final on an overridden method in a subclass to freeze that specific behaviour while allowing further subclassing.
More examples
final class cannot be extended
final prevents any subclass from overriding Uuid's validation logic, preserving the invariant that every instance is valid.
<?php
final class Uuid {
public function __construct(public readonly string $value) {
if (!preg_match('/^[0-9a-f-]{36}$/', $value)) {
throw new \InvalidArgumentException('Invalid UUID');
}
}
public static function generate(): self {
return new self(sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
));
}
}
$id = Uuid::generate();
echo $id->value;final method in a subclass
Marking an overridden method final in a subclass freezes that layer's behaviour while still allowing further subclassing.
<?php
class BaseController {
public function dispatch(string $action): void {
echo "Dispatching: $action";
}
}
class ApiController extends BaseController {
final public function dispatch(string $action): void {
// Adds JSON header, then delegates
echo 'application/json\n';
parent::dispatch($action);
}
}
// Any class extending ApiController cannot override dispatch()
class UserController extends ApiController {}final communicates design intent
final on the value object signals that inheritance is not part of its design; composition should be used instead.
<?php
final class Money {
public function __construct(
public readonly int $amount,
public readonly string $currency
) {}
public function add(self $other): self {
if ($this->currency !== $other->currency) {
throw new \LogicException('Currency mismatch');
}
return new self($this->amount + $other->amount, $this->currency);
}
}
$a = new Money(1000, 'USD');
$b = new Money(500, 'USD');
$c = $a->add($b);
echo $c->amount; // 1500
Discussion