Readonly Classes Can’t Use Traits in PHP 8.2 — Here’s the Fix

📖 2 minutes read

PHP 8.2’s readonly class modifier is great for DTOs and value objects. But the moment you try to use a trait that declares non-readonly properties, PHP throws a fatal error at compile time.

The Scenario

You’re building a queued event listener as a clean readonly class. You need InteractsWithQueue for retry control — release(), attempts(), delete(). Seems straightforward:

readonly class SendWelcomeEmail implements ShouldQueue
{
    use InteractsWithQueue; // 💥 Fatal error

    public function __construct(
        public string $email,
        public string $name
    ) {}

    public function handle(Mailer $mailer)
    {
        if ($this->attempts() > 3) {
            $this->delete();
            return;
        }
        // Send the email...
    }
}

This explodes with:

Fatal error: Readonly class SendWelcomeEmail cannot use trait
with a non-readonly property InteractsWithQueue::$job

Why It Happens

A readonly class enforces that every property must be readonly. The InteractsWithQueue trait declares a $job property that Laravel’s queue system needs to write to at runtime. Since trait properties are merged into the using class, PHP rejects the non-readonly $job property at compile time.

This isn’t just InteractsWithQueue — any trait with mutable properties will trigger the same error. Common Laravel culprits include Dispatchable, InteractsWithQueue, and any trait that maintains internal state.

The Fix

Drop readonly from the class declaration and mark individual properties as readonly instead:

class SendWelcomeEmail implements ShouldQueue
{
    use InteractsWithQueue;

    public function __construct(
        public readonly string $email,  // readonly per-property
        public readonly string $name    // readonly per-property
    ) {}

    public function handle(Mailer $mailer)
    {
        if ($this->attempts() > 3) {
            $this->delete();
            return;
        }
        // Send the email...
    }
}

You get the same immutability guarantee on your data properties while allowing the trait’s mutable properties to exist alongside them.

The Rule

Use readonly class for pure data objects that don’t use traits with mutable state — DTOs, value objects, API response models. The moment you need framework traits that maintain internal state (queue interaction, event dispatching, etc.), switch to per-property readonly declarations.

It’s a small syntactic difference with a big compatibility impact. Check your traits before reaching for readonly class.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *