In PHP, what can an enum declare besides its cases, and what does the engine forbid inside an enum?
answer
- methods, static methods, constants
- interfaces and property-free traits
- no properties, constructors or abstract methods
- cannot extend or be extended
- UnitEnum and BackedEnum are implicit
basics
~20 sAn enum may declare methods, static methods, constants, interfaces and traits without properties, plus __call, __callStatic and __invoke. It may not have properties, constructors, destructors, abstract methods or other magic methods, and cannot extend or be extended.
solid answer
~40 sEnums are classes with a fixed set of singleton instances, so they allow **behaviour but not state**. Allowed: instance methods (with `$this` being the case, often used in `match ($this)`), static methods such as a named lookup, constants (including aliases like `const DEFAULT = self::Placed;`), any number of interfaces, traits that contain no properties, attributes, and the magic methods `__call`, `__callStatic` and `__invoke`. Forbidden, mostly at compile time: static or instance properties (`Enum OrderStatus cannot include properties`), `__construct`, `__destruct` and the other magic methods, abstract methods, `extends` in either direction, `new OrderStatus()` (`Cannot instantiate enum`), `clone`, and redeclaring `cases()`, `from()` or `tryFrom()`. Every enum implicitly implements `UnitEnum`, backed ones also `BackedEnum`; ordinary classes cannot implement those.
code
php · 24 lines<?php
declare(strict_types=1);
interface HasLabel { public function label(): string; }
enum OrderStatus: string implements HasLabel
{
case Placed = 'placed';
case Preparing = 'preparing';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public const DEFAULT = self::Placed;
public function label(): string { return ucfirst($this->value); }
public function canTransitionTo(self $next): bool
{
return in_array($next, match ($this) {
self::Placed => [self::Preparing, self::Cancelled],
self::Preparing => [self::Delivered, self::Cancelled],
self::Delivered, self::Cancelled => [],
}, true);
}
}
var_dump(OrderStatus::DEFAULT->canTransitionTo(OrderStatus::Delivered)); // bool(false)go deeper
Recall that enums can have methods and constants but no properties, and cannot be created with new.
Explain why cases are stateless singletons, how match ($this) gives per-case behaviour, and what UnitEnum and BackedEnum provide.
Put domain rules such as allowed transitions on the enum, and keep per-entity state and I/O out of it.
Decide how far behaviour should live on enums versus services, keeping enums small, pure and easy to evolve.
## Behaviour yes, state no An enum is compiled to a class whose only instances are its cases. Because each case is a **singleton** shared by all code in the request, it must not carry mutable state: if `OrderStatus::Placed` had a writable property, one order's data would leak into every other order. The rules follow from that. ## What an enum may declare - **Instance methods**, public, protected or private. Inside them `$this` is the current case, so `match ($this) { self::Placed => ..., }` is the idiomatic way to vary behaviour per case. - **Static methods**, for example `OrderStatus::active(): array` returning the non-final cases, or a lookup by legacy code. - **Constants**, including constants that refer to a case: `public const DEFAULT = self::Placed;` makes `OrderStatus::DEFAULT` an alias of that case. - **Interfaces**, any number, so an enum can satisfy `HasLabel` and be passed wherever that interface is expected. - **Traits**, as long as they contain no properties; a trait with properties is a fatal error. - **Attributes** on the enum and on individual cases. - The magic methods **`__call`, `__callStatic` and `__invoke`**, and no others. ## What the engine forbids | Attempt | Result | |---|---| | `public static $cache;` or any property | `Enum OrderStatus cannot include properties` (compile time) | | `__construct`, `__destruct`, `__get`, `__toString`, ... | `Enum OrderStatus cannot include magic method ...` | | `abstract public function label();` | `Enum method OrderStatus::label() must not be abstract` | | `class Special extends OrderStatus` | `Class Special cannot extend enum OrderStatus` | | `new OrderStatus()` | `Error: Cannot instantiate enum OrderStatus` | | `clone OrderStatus::Placed` | `Error: Trying to clone an uncloneable object of class OrderStatus` | | declaring `cases()`, `from()` or `tryFrom()` | fatal error, cannot redeclare | | implementing `Serializable` | compile error | The only properties an enum has are the engine-provided read-only `name` and, on backed enums, `value`. ## The implicit interfaces Every enum implements **`UnitEnum`** (`cases()`), and every backed enum also implements **`BackedEnum`** (`from()`, `tryFrom()`). That lets generic code work across enums: 1. A parameter typed `UnitEnum` accepts a case of any enum, and `$case->name` is always available. 2. A parameter typed `BackedEnum` accepts any backed case, and `$case->value` is always available, which is handy for a serializer or a form helper. 3. `$case instanceof BackedEnum` tells a pure case from a backed one. These interfaces are reserved: `class Foo implements UnitEnum {}` fails with `Non-enum class Foo cannot implement interface UnitEnum`, and a pure enum cannot implement `BackedEnum`. ## Static helpers built from `cases()` Because `cases()` returns every case, static methods can derive subsets without keeping a second list in sync: ```php public static function active(): array { return array_values(array_filter( self::cases(), fn (self $s) => !$s->isFinal(), )); } ``` Adding a case automatically includes it wherever it qualifies. The same pattern builds dropdown options (`value` to `label()`) or validation lists for an API schema. A constant alias such as `OrderStatus::DEFAULT` is the very same object as the case it names, so `OrderStatus::DEFAULT === OrderStatus::Placed` is `true`. ## Closed on purpose An enum is a **closed set**: no subclass can add cases, and the engine rejects `extends` in both directions. That is the point for order states, where every part of the system must agree on the full list. When a set genuinely needs to be open, for example payment methods contributed by plugins, the extension point should be an **interface** such as `PaymentMethod`, which a built-in enum can implement alongside ordinary classes from other packages. Code that consumes the interface then works with both, while the enum keeps its own cases closed. ## Putting it together for order states A food-delivery `OrderStatus` enum typically carries: - `label(): string` for the customer-facing text; - `canTransitionTo(self $next): bool` encoding the allowed state machine; - `isFinal(): bool` for delivered and cancelled orders; - a static `active(): array` for dashboards; - `implements HasLabel`, so a generic dropdown renderer can accept it. Per-order data such as timestamps stays on the order entity; the enum only answers questions that are the same for every order in a given state.
- In PHP, how do you give each enum case different behaviour without abstract methods?Write one method on the enum and branch with `match ($this)`, one arm per case. Enum methods cannot be abstract, and cases cannot override methods individually, so per-case behaviour is expressed as data inside a single method.
- In PHP, can a function accept a case of any backed enum and read its value?Yes: type the parameter as `BackedEnum`. Every backed enum implements it implicitly, so the function can call `$case->value`, and `UnitEnum` covers pure enums too, with `$case->name`.
saying these in an interview costs you the question
- Adds a cache property to an enum and expects it to compile
- Tries to extend an enum to add cases in a subclass
- Declares abstract methods on an enum for per-case behaviour
- Implements UnitEnum on an ordinary class to make it enum-like
- Says enums cannot implement interfaces