Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
PHPDevelopmentInterview questionsInterview preparation

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.

Cloudsoft Zone

Updated 8 min read

Young man in a grey suit writing on a sheet of paper at a glass table while a smiling interviewer talks
Photograph: Amtec Photos / CC BY-SA 2.0, via Wikimedia Commons
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
<?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
<?php

OrderStatus::from('paid');        // OrderStatus::Paid
OrderStatus::tryFrom('refunded'); // null
OrderStatus::from('refunded');
// ValueError: "refunded" is not a valid backing value for enum OrderStatus

The 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-only name and, for backed enums, value.
  • Be instantiated. new E() throws Error: 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 > returns false both ways. Add a method such as priority(): int if 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.

Interviewer gesturing while she speaks to a candidate across a glass table with a notepad and pen
Explain why tryFrom() belongs at input boundaries and from() belongs on trusted data.

Enums in real applications

6. How do enums serialise to JSON and other formats?

PHP
<?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 serialization

Backed 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?

AspectClass constantsBacked enum
Type safetyA string $status parameter accepts any stringAn OrderStatus $status parameter accepts only valid cases
BehaviourNeeds a separate helper classMethods live on the enum
Listing all valuesReflection or a manual arraycases()
Parsing inputManual in_array() checktryFrom()
ExtensibilitySubclasses can add constantsClosed 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
<?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::$amount

The "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).

Three people in business clothes reviewing documents from a red folder at a wooden table
A strong answer names the PHP version for each feature and the limits of readonly.

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
<?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 property

Our 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. Implicit protected(set): 8.4. clone with 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 name property and, for backed enums, value. They can have constants and methods.

  • What types can back a PHP enum?

    Only int or string, one type per enum. Each case needs an explicit, unique value.

  • What is the difference between from() and tryFrom()?

    from() throws a ValueError for an unknown value; tryFrom() returns null. Use tryFrom() 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 adds clone($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.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Enumerations — PHP Manual
  2. Backed enumerations — PHP Manual
  3. Properties: readonly properties — PHP Manual
  4. The Basics: readonly classes — PHP Manual
  5. 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.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.