PHP enums and readonly classes: 12 interview questions answered
Twelve PHP interview questions on enums, readonly properties and readonly classes, answered with code and real error messages tested on PHP 8.5.
Updated 8 min read

In this article
Enums and readonly properties are now standard interview topics for PHP roles, because they show whether a candidate writes modern, type-safe PHP or still relies on class constants and setters. This guide answers the questions interviewers ask most often, with code that we ran on PHP 8.5.7 and the exact error messages PHP produced, so you can explain not only what works but why.
Enums arrived in PHP 8.1, as did readonly properties. Readonly classes followed in PHP 8.2, cloning changes in 8.3 and 8.4, and clone with property overrides in PHP 8.5.
Enum fundamentals
1. What is the difference between a pure enum and a backed enum?
A pure enum has cases with no scalar value, such as enum Suit { case Hearts; case Spades; }. A backed enum declares a type after the name and gives every case a value: enum OrderStatus: string { case Paid = 'paid'; }. Use backed enums when the value is stored in a database, sent in JSON or received from a form.
The manual is specific about the rules: the backing type is int or string, never a union such as int|string; every case must define its value explicitly; and no two cases may share a value. There is no auto-numbering.
2. Show a realistic enum with behaviour
Interviewers like to see that enums can hold logic, not just values. This one implements an interface, has a constant alias, an instance method using match, a state-transition rule and a static helper:
<?php
declare(strict_types=1);
interface HasLabel
{
public function label(): string;
}
enum OrderStatus: string implements HasLabel
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public const DEFAULT = self::Pending;
public function label(): string
{
return match ($this) {
self::Pending => 'Awaiting payment',
self::Paid => 'Paid',
self::Shipped => 'On its way',
self::Cancelled => 'Cancelled',
};
}
public function canTransitionTo(self $next): bool
{
return in_array($next, match ($this) {
self::Pending => [self::Paid, self::Cancelled],
self::Paid => [self::Shipped, self::Cancelled],
self::Shipped, self::Cancelled => [],
}, true);
}
/** @return list<string> */
public static function values(): array
{
return array_column(self::cases(), 'value');
}
}Running it confirmed OrderStatus::Pending->label() returns Awaiting payment, OrderStatus::Pending->canTransitionTo(OrderStatus::Shipped) returns false, and OrderStatus::values() returns pending,paid,shipped,cancelled. Because match has no default arm, adding a new case without handling it causes an UnhandledMatchError at runtime, and static analysers flag the missing arm earlier.
3. When do you use from() and when tryFrom()?
<?php
OrderStatus::from('paid'); // OrderStatus::Paid
OrderStatus::tryFrom('refunded'); // null
OrderStatus::from('refunded');
// ValueError: "refunded" is not a valid backing value for enum OrderStatusThe manual describes from() as useful when the input is trusted and tryFrom() when it is not. In practice, use tryFrom() on request data and return a validation error for null; use from() when reading values your own code wrote to the database, where an unknown value really is exceptional. Under strict_types, passing an int to a string-backed enum throws a TypeError; we saw OrderStatus::from(): Argument #1 ($value) must be of type string, int given. You cannot define your own from() or tryFrom(); doing so is a fatal error.
4. What can an enum not do?
- Hold state. Declaring a property fails at compile time with
Enum Bad2 cannot include properties. The only properties are the read-onlynameand, for backed enums,value. - Be instantiated.
new E()throwsError: Cannot instantiate enum E. - Be extended. Enums are final and cannot extend classes. They can implement interfaces and use traits, provided the traits declare no properties.
- Be ordered. Comparing two cases with
<or>returnsfalseboth ways. Add a method such aspriority(): intif you need ordering.
5. How are enum cases compared?
Each case is a singleton object, so === compares identity and is the comparison to use. OrderStatus::from('paid') === OrderStatus::Paid is true. Cases from different enums are never equal, even if their backing values match. Every enum implements the UnitEnum interface, and backed enums also implement BackedEnum; we confirmed that a pure enum case is not an instance of BackedEnum.

Enums in real applications
6. How do enums serialise to JSON and other formats?
<?php
enum Suit
{
case Hearts;
case Spades;
}
echo json_encode(['status' => OrderStatus::Paid]); // {"status":"paid"}
json_encode(Suit::Hearts, JSON_THROW_ON_ERROR);
// JsonException: Non-backed enums have no default serializationBacked enums encode as their value; pure enums cannot be JSON-encoded at all. serialize() uses a dedicated format, which for our case was E:16:"OrderStatus:Paid";, and unserialize() returns the same singleton, so identity comparison still works after a round trip.
7. Can enums be used in constant expressions?
Yes. Cases can be used as parameter defaults, in class constants and in attribute arguments. Since PHP 8.2 you can also read ->value and ->name in a constant expression, so public const STATUS_VALUE = OrderStatus::Paid->value; is valid. We ran it and it printed paid.
8. Enums or class constants: what is the trade-off?
| Aspect | Class constants | Backed enum |
|---|---|---|
| Type safety | A string $status parameter accepts any string | An OrderStatus $status parameter accepts only valid cases |
| Behaviour | Needs a separate helper class | Methods live on the enum |
| Listing all values | Reflection or a manual array | cases() |
| Parsing input | Manual in_array() check | tryFrom() |
| Extensibility | Subclasses can add constants | Closed set; cannot be extended |
The closed set is the point: a function that accepts an enum can handle every case and know it has.
9. A gotcha: duplicate backing values
Ask a candidate when PHP reports two cases with the same value. On PHP 8.5.7 our file with case A = "x"; case B = "x"; passed php -l and only failed when the enum was first used, with Error: Duplicate value in enum Bad for cases A and B. Tests that load every enum, or static analysis, catch this before production.
Readonly properties and classes
10. What exactly does readonly guarantee?
A readonly property can be initialised once, from inside the declaring class scope, and never changed afterwards. It must be typed (use mixed if you need no constraint), it cannot have a default value (Readonly property P::$x cannot have default value), and static readonly properties are not supported. Since PHP 8.4, readonly properties are implicitly protected(set), so a child class can initialise them too.
11. What is a readonly class, and how does PHP 8.5 change immutable objects?
Since PHP 8.2, readonly class marks every declared property readonly and forbids dynamic properties, even with the #[AllowDynamicProperties] attribute. All properties must be typed and none may be static. A readonly class can only be extended by another readonly class.
<?php
declare(strict_types=1);
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {
}
public function add(int $cents): static
{
// PHP 8.5: clone with new property values
return clone($this, ['amount' => $this->amount + $cents]);
}
}
$price = new Money(1000, 'EUR');
$total = $price->add(250);
echo $price->amount, ' ', $total->amount; // 1000 1250
$price->amount = 5; // Error: Cannot modify readonly property Money::$amountThe "with-er" pattern used to need reflection or a constructor call with every argument. PHP 8.3 allowed readonly properties to be reinitialised inside __clone(), and PHP 8.5 adds clone($object, [...]), which sets new values during cloning. The RFC notes that public readonly properties are protected(set) by default, so the override works from inside the class, as above. Code outside the class needs the property declared public(set).

12. Is a readonly object deeply immutable?
No, and this is a favourite follow-up. Readonly protects the property, not the object stored in it:
<?php
final class Box
{
public function __construct(public readonly ArrayObject $items)
{
}
}
$box = new Box(new ArrayObject());
$box->items[] = 'still mutable'; // allowed: the object inside changes
$box->items = new ArrayObject(); // Error: Cannot modify readonly propertyOur run appended to the ArrayObject successfully, then threw Cannot modify readonly property on reassignment. For true immutability, store only scalars, arrays or other immutable objects in readonly properties.
How to answer well in the interview
- Lead with the why. Enums make invalid states unrepresentable; readonly makes accidental mutation impossible.
- Name the versions. Enums and readonly properties: 8.1. Readonly classes: 8.2. Reinitialising in
__clone(): 8.3. Implicitprotected(set): 8.4.clonewith properties: 8.5. - Mention boundaries. Use
tryFrom()at input boundaries and map enums to database columns by their backing value. - Know the limits. No state in enums, no ordering, no deep immutability from readonly.
What to do next
Copy the OrderStatus and Money examples into a scratch file and run them on PHP 8.4 or 8.5. Then change one thing at a time, such as adding a property to the enum or assigning a readonly property twice, and read the exact error. Being able to predict those messages is what separates a memorised answer from real understanding.
Frequently asked questions
Which PHP version introduced enums?
PHP 8.1, released in November 2021, introduced both enumerations and readonly properties. Readonly classes followed in PHP 8.2.
Can a PHP enum have properties?
No. Declaring a property is a compile-time error. Enums only expose the read-only
nameproperty and, for backed enums,value. They can have constants and methods.What types can back a PHP enum?
Only
intorstring, one type per enum. Each case needs an explicit, unique value.What is the difference between from() and tryFrom()?
from()throws aValueErrorfor an unknown value;tryFrom()returnsnull. UsetryFrom()on untrusted input.Can I change a readonly property when cloning?
Since PHP 8.3 you can reinitialise readonly properties inside
__clone(). PHP 8.5 addsclone($obj, ['prop' => $value]), subject to the property's set visibility.Are readonly objects immutable?
Only shallowly. The property cannot be reassigned, but an object stored in it can still change its own internal state.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- Enumerations — PHP Manual
- Backed enumerations — PHP Manual
- Properties: readonly properties — PHP Manual
- The Basics: readonly classes — PHP Manual
- PHP RFC: Clone with v2 — PHP Wiki
Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.
Comments
No comments yet. Be the first to join the conversation.
Leave a comment
Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.
More from Cloudsoft Zone

TutorialPHP
PHPStan tutorial: catch PHP bugs before production with levels
Install PHPStan 2.3, choose a rule level, adopt a baseline for legacy code and fail CI only on new errors, with real output from a worked PHP 8.5 example.
By Cloudsoft Zone

Resource guidePHP
PHP 8.2 reaches end of life on 31 December 2026: an upgrade checklist
PHP 8.2 stops receiving security fixes on 31 December 2026. Here is the current support table from php.net, how to choose between PHP 8.4 and 8.5, and a step-by-step checklist for a safe upgrade.
By Cloudsoft Zone

TutorialPHP
Build a read-only PHP MCP server with MySQL and the official SDK
Expose product lookup, bounded low-stock searches and an inventory summary to AI clients with the official PHP MCP SDK, PDO and local STDIO transport.
By Cloudsoft Zone
