Object-oriented PHP
Classes and promoted constructors, visibility, static members, inheritance, interfaces, traits, readonly properties and enums.
Classes, constructors and visibility
<?php
declare(strict_types=1);
final class Invoice
{
public function __construct(
public readonly string $number,
private array $lines = [],
protected ?DateTimeImmutable $issuedAt = null,
) {
}
public function total(): float
{
return array_sum(array_map(fn(Line $l) => $l->total(), $this->lines));
}
public static function fromRow(array $row): self
{
return new self(number: $row["number"]);
}
}- Constructor property promotion declares and assigns in one place; the properties are real properties with real types.
readonlymeans written once, in the constructor, then immutable - the cheapest way to keep a value object honest.publicis readable and writable by anyone,protectedis open to subclasses,privatestays inside the class.staticmembers belong to the class, not to an instance; inside the class useself::for the defining class andstatic::when a subclass should be able to override.finalon the class documents that inheritance is not part of the design, and the static analyser will warn if someone tries.
Interfaces, abstract classes, traits and enums
interface Repository
{
public function find(int $id): ?Post;
public function save(Post $post): void;
}
abstract class BaseRepository implements Repository
{
abstract protected function table(): string;
public function find(int $id): ?Post
{
$stmt = $this->db->prepare("SELECT * FROM " . $this->table() . " WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetchObject(Post::class) ?: null;
}
}
trait LogsQueries
{
private function logQuery(string $sql): void
{
$this->logger->debug($sql);
}
}
enum Status: string
{
case Draft = "draft";
case Live = "live";
public function label(): string
{
return match ($this) {
Status::Draft => "Draft",
Status::Live => "Live",
};
}
}| Tool | Use it when |
|---|---|
| Interface | Several unrelated classes must satisfy one contract |
| Abstract class | Subclasses share implementation and must fill in a gap |
| Trait | A method or two is copied into otherwise unrelated classes |
| Enum | A fixed set of named values replaces loose strings or integers |
| Readonly class | An immutable value object with no setters at all |
💡
Program against an interface, not a concrete class: type-hint
Repository in the service and the storage can change - database, cache, fake in a test - without touching the caller.Worked example: a value object and two implementations
final class Money
{
private function __construct(public readonly int $cents, public readonly string $currency)
{
}
public static function of(float $amount, string $currency = "EUR"): self
{
return new self((int) round($amount * 100), $currency);
}
public function add(self $other): self
{
if ($other->currency !== $this->currency) {
throw new InvalidArgumentException("Currency mismatch");
}
return new self($this->cents + $other->cents, $this->currency);
}
}
interface Notifier
{
public function send(string $to, string $message): void;
}
final class MailNotifier implements Notifier
{
public function send(string $to, string $message): void { /* smtp */ }
}
final class FakeNotifier implements Notifier
{
public array $sent = [];
public function send(string $to, string $message): void { $this->sent[] = [$to, $message]; }
}The value object validates once and then cannot be wrong; the interface lets the test double replace the mail server with an array. Those two moves - immutable values, explicit contracts - carry most of the practical benefit of object orientation.
FAQ
When should I prefer composition over inheritance?
By default. Inherit only when the subclass genuinely is a more specific version of the parent; otherwise hold a collaborator in a property and delegate to it.
Can an enum have methods and interfaces?
Yes. Enum cases are objects: they can implement interfaces, carry methods, use traits, and be used in
match and as typed parameters.Related
Functions, control flow and type declarations Modern PHP 8 features in practice
Last refreshed 2026-09-18.