Write idiomatic PHP 8 classes: property promotion, readonly, named constructors and immutable value objects.
Open this lesson in KodokonYou've been writing classes for a long time, but PHP 8 has made several habits obsolete. The most visible one: property promotion in the constructor. Adding a visibility keyword (private, protected, public) in front of a constructor parameter creates the property and assigns it in a single step. The three repetitive blocks of the old style - declaration, parameter, assignment - collapse into one.
<?php
declare(strict_types=1);
class LegacyProduct
{
private string $name;
private int $priceCents;
public function __construct(
string $name,
int $priceCents,
) {
$this->name = $name;
$this->priceCents = $priceCents;
}
}
final class Product
{
public function __construct(
private string $name,
private int $priceCents,
) {
}
public function label(): string
{
return sprintf(
'%s (%.2f EUR)',
$this->name,
$this->priceCents / 100,
);
}
}
$product = new Product('Keyboard', 4990);
echo $product->label();Visibility remains your first tool for encapsulation: private by default, protected only if a child class needs access, public as a last resort. Since PHP 8.1, readonly adds a stronger guarantee: the property can be written only once, from within the scope of the class that declares it. Combined with promotion, it produces immutable value objects in just a few lines: any "change" returns a new instance, which eliminates shared side effects.
<?php
declare(strict_types=1);
final class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {
}
public function add(Money $other): self
{
if ($other->currency !== $this->currency) {
throw new InvalidArgumentException(
'Currency mismatch'
);
}
return new self(
$this->amount + $other->amount,
$this->currency,
);
}
}
$total = (new Money(500, 'EUR'))
->add(new Money(250, 'EUR'));
echo $total->amount;On the method side, two patterns dominate among experienced developers. Named constructors: a private constructor paired with static factories makes each intent explicit and forbids inconsistent instantiations. Fluent interfaces: returning $this (mutable) or new self(...) (immutable) lets you chain calls. The trade-off is well known: the immutable version is safer but allocates one object per call - negligible almost everywhere, measurable in a hot loop.
<?php
declare(strict_types=1);
final class User
{
private function __construct(
public readonly string $email,
public readonly string $role,
) {
}
public static function admin(string $email): self
{
return new self($email, 'admin');
}
public static function member(string $email): self
{
return new self($email, 'member');
}
}
$user = User::admin('lea@example.com');
echo $user->role;readonly property be written?