Kodokon kodokon.com

Modern OOP: classes, promoted properties, visibility

Write idiomatic PHP 8 classes: property promotion, readonly, named constructors and immutable value objects.

9 min · 3 questions

Open this lesson in Kodokon

You'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
<?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();
Before / after: promotion removes all the boilerplate assignment code.

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
<?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;
Immutable value object: add() returns a new instance.

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
<?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;
Private constructor + static factories: only valid states can be constructed.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What does property promotion do in a PHP 8 constructor?
    • It automatically makes every property public
    • It declares the property and assigns it from the parameter, in a single step
    • It generates accessors (getters) for every property
    • It converts properties into class constants
  2. When can a readonly property be written?
    • Never: it must receive a default value at declaration
    • Exactly once, from within the scope of the class that declares it
    • At any time, as long as the write comes from the class itself
  3. Which visibility should you choose for a property that only the class and its child classes should use?
    • public
    • protected
    • private