PHP Enums and Readonly Properties

Model fixed choices with enum cases and protect initialized property bindings with readonly.

Lesson 45 of 86Advanced10 min read

An enum represents a fixed set of named choices, such as draft, paid and cancelled order states. Instead of passing arbitrary strings throughout the application, code can require an OrderStatus case. A readonly property protects an initialized property binding from ordinary reassignment, making value objects easier to reason about.

Both features arrived in PHP 8.1, but they solve different problems. Enums restrict the allowed choices. Readonly controls assignment to a property. This reference explains backed enums at data boundaries, initialization rules, shallow immutability and later version changes so you can use the features without assuming they validate every business transition.

Pure and backed enums

A pure enum declares named cases without external scalar values. Each case is an object singleton and can be compared by identity. A backed enum also gives every case a unique int or string value. One enum uses one backing type. Backed values are useful for database storage, JSON fields and incoming strings that need conversion.

Every enum case exposes its name. Backed cases also expose value. Use names for internal diagnostics and backed values for a deliberately stable external representation. Renaming a case can affect code and name-based consumers; changing a backing value can affect stored data. Treat both as compatibility decisions rather than cosmetic edits.

Enums can define methods and implement interfaces, but they cannot be used like ordinary stateful classes with arbitrary instance properties. Their cases represent the fixed choices. Put per-order changing data in an Order object, not in the shared enum case. A status case is not a unique order instance.

Enum choices do not define transitions

An enum prevents a status from becoming a misspelled arbitrary string when typed correctly. It does not decide whether a paid order may return to draft, whether a cancelled order can ship, or which user may change a status. Those are domain and authorization rules around the enum.

A transition method can receive an enum and check the current state before changing an order. A match expression can map cases to labels or allowed actions. If you omit a default, adding a new enum case can reveal incomplete mappings through an error. That can be useful, but the application still needs a thoughtful migration and test strategy.

At a boundary, convert trusted-format input into the enum once, then pass the typed case through domain code. On output, choose the backed value or a separate display label deliberately. Do not expose an internal case name as a permanent API field by accident.

Readonly initialization and property bindings

A readonly property must have a declared type. Once initialized, ordinary reassignment is forbidden, as are mutations to an array stored directly in that property. The restriction helps value objects keep their initialized values stable. Required values are commonly supplied through constructor property promotion.

The precise permitted initialization scope has evolved. PHP 8.1's initial behavior restricted writes to the declaring class scope; PHP 8.4 treats readonly properties as implicitly protected for setting unless a stricter set visibility is specified. Write simple constructor-owned initialization for portable examples, and check the exact minimum version before relying on newer asymmetric visibility rules.

Readonly does not recursively freeze the object graph. If a readonly property holds a mutable object, that object's own properties can still change. Only the property's reference binding is protected. Use immutable nested values, defensive copies or a clear mutability policy when deep immutability is actually required.

Readonly classes and cloning notes

PHP 8.2 introduced readonly classes, which apply readonly behavior to declared instance properties and impose related restrictions. They can reduce repetitive property declarations for value objects. They still do not make referenced nested objects deeply immutable, and they are not a replacement for defining a valid object state.

PHP 8.3 added limited readonly reinitialization during cloning through __clone, enabling deliberate copy adjustments. Later versions also extend cloning features, so do not assume every readonly cloning example runs on PHP 8.1. Keep the examples here to construction and reading, which demonstrate the core feature without those newer mechanisms.

The enum basics manual, backed enums reference and property reference provide the exact language rules. Confirm version constraints in Composer and deployment before introducing syntax older workers cannot parse.

PHP Enums and Readonly Properties Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Convert a stored status

From returns the case for a valid backed value and throws ValueError for an unknown value. TryFrom returns null for an unknown backed value. Validate the incoming scalar type too; tryFrom is not a universal parser for arrays or objects. Its null result must be handled rather than passed into code requiring a status.

PHP
<?php
enum OrderStatus: string {
    case Draft = 'draft';
    case Paid = 'paid';
    case Cancelled = 'cancelled';
}
$status = OrderStatus::from('paid');
echo $status->name . PHP_EOL;
echo $status->value . PHP_EOL;
var_export(OrderStatus::tryFrom('unknown') === null);
echo PHP_EOL;
Output
Paid
paid
true
Example 2

Construct a small readonly value

The promoted property receives its initial value during construction. The constructor rejects an empty string, showing that readonly and a string type do not replace validation. An assignment such as $code->value = 'P-102' would fail; to represent a different code, construct another value object.

PHP
<?php
class ProductCode {
    public function __construct(public readonly string $value) {
        if ($value === '') {
            throw new InvalidArgumentException('Code is required');
        }
    }
}
$code = new ProductCode('P-101');
echo $code->value . PHP_EOL;
Output
P-101

PHP Enums and Readonly Properties in Real Projects

Practical task: a typed immutable shipment choice

Create a backed DeliveryMode enum and a small choice object whose mode and fee cannot be reassigned after construction. The constructor validates the fee. Use a separate match to choose the fee, then print the backing value and amount. This keeps data conversion, pricing and object storage visible as separate steps.

PHP
<?php
enum DeliveryMode: string {
    case Pickup = 'pickup';
    case Courier = 'courier';
}
class DeliveryChoice {
    public function __construct(
        public readonly DeliveryMode $mode,
        public readonly int $fee
    ) {
        if ($fee < 0) {
            throw new InvalidArgumentException('Fee cannot be negative');
        }
    }
}
$mode = DeliveryMode::from('courier');
$fee = match ($mode) {
    DeliveryMode::Pickup => 0,
    DeliveryMode::Courier => 5,
};
$choice = new DeliveryChoice($mode, $fee);
echo $choice->mode->value . ':' . $choice->fee . PHP_EOL;
Output
courier:5

The enum prevents an unsupported mode from entering the object. Readonly protects the stored binding and integer value. The constructor still allows any nonnegative fee, so an application requiring a fixed fee-to-mode relationship should enforce that relationship in its factory or constructor rather than relying only on this example's caller.

PHP Enums and Readonly Properties FAQ

Should I use enum or constants for statuses?

An enum provides a typed set of cases. Constants are useful for fixed values but do not create the same constrained parameter type.

Does readonly mean a property is private?

No. Read visibility and assignment restrictions differ. A public readonly property can be read publicly.

Can a readonly object contain mutable data?

Yes through referenced objects. Deep immutability requires deliberate nested design.

Should invalid input use from or tryFrom?

Use from when invalid input should throw under the caller's error policy. Use tryFrom when you want a null result and will explicitly handle it.

Can enums validate user permissions?

No. They model choices. Authorization and allowed state changes remain application rules.

PHP Enums and Readonly Properties Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

A readonly property holds a mutable Address object. Which statement is correct?

PHP Enums and Readonly Properties Quiz

2 questions to check what you learned.

  1. Which PHP version introduced enums and readonly properties?

    • PHP 7.4
    • PHP 5.6
    • PHP 8.1
  2. What does tryFrom return for an unknown valid-type backed value?

    • null
    • A new case with that value
    • Always the first case