Tutorials Logic, IN info@tutorialslogic.com

PHP Enums: Closed Value Sets, Backed Cases, and Domain Behavior

PHP Closed Sets

PHP enums are closed types whose cases are singleton objects. Pure cases carry identity, backed cases add one unique string or integer value, and methods and interfaces can express deterministic behavior without opening inheritance.

Reliable enum design uses stable storage values, explicit parsing, exhaustive matches, separate authorization, versioned API mapping, schema-aware rollout, and tests that automatically include every declared case.

Pure Enum

A pure enum has named cases but no string or integer backing values. Cases are singleton objects and can be used in type declarations and strict comparisons.

Delivery State

Delivery State
<?php
enum DeliveryState
{
    case Pending;
    case Packed;
    case Shipped;
}

function canCancel(DeliveryState $state): bool
{
    return $state === DeliveryState::Pending;
}

echo canCancel(DeliveryState::Pending) ? 'Cancelable' : 'Locked';
Output
Cancelable

The function cannot receive a misspelled status string because its parameter requires a DeliveryState case.

Backed Enum

A backed enum maps every case to a unique string or integer. That value is useful at request, JSON, and database boundaries; keep the enum object inside domain code.

Convert Request Input

Convert Request Input
<?php
enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
}

$status = OrderStatus::tryFrom('paid');
echo $status?->name ?? 'Invalid status';
Output
Paid

tryFrom() returns null for an unknown value; from() throws ValueError and is better only when invalid input is already impossible.

Enum Behavior

Enums may implement interfaces and define methods. Keep behavior that depends only on the case close to the enum; inject external services elsewhere.

Case-Specific Label

Case-Specific Label
<?php
enum Priority: string
{
    case Low = 'low';
    case Normal = 'normal';
    case Urgent = 'urgent';

    public function label(): string
    {
        return match ($this) {
            self::Low => 'Low priority',
            self::Normal => 'Normal priority',
            self::Urgent => 'Urgent priority',
        };
    }
}

echo Priority::Urgent->label();
Output
Urgent priority

Boundary Rules

  • Validate request strings with tryFrom() and handle null.
  • Bind the backed value when writing to SQL and reconstruct the enum when reading.
  • Use cases() when the interface needs the current allowed choices.
  • Do not create an enum for an open-ended set such as user-created tags.

Closed Value Sets

PHP enums, available from PHP 8.1, define a type whose valid values are a closed set of named cases. Each case is a singleton object, so `Status::Draft === Status::Draft` and a plain string is never the same value as the case.

Use an enum when every valid choice is known by the code and callers benefit from exhaustive handling. Use a value object when new values can arrive without a code release or when each value carries independent runtime data.

Pure enum cases have names but no scalar backing value. Their readonly `name` property is useful for diagnostics, but persisting the source case name couples stored data to refactoring. Define a stable external mapping when that risk matters.

Enums cannot be extended, which preserves the closed-set guarantee. If independent modules must add implementations, use an interface and registration mechanism instead of an enum pretending to be open.

  • Use enums for code-owned finite choices.
  • Compare case identity rather than strings.
  • Keep source names separate from stable external identifiers.
  • Use interfaces for open extension sets.

Backed Cases

A backed enum assigns every case one unique string or integer value. One enum uses one backing type, and values are explicit rather than automatically numbered. The readonly `value` property provides the transport or storage representation.

Choose backing values that remain stable across labels and code reorganization. Human-readable display text changes through translation and product wording, so it should come from a method or presentation map rather than the persisted value.

`from()` returns the matching case and throws ValueError for an unknown scalar. It fits trusted invariant data whose mismatch is a defect. `tryFrom()` returns null for an unknown value and fits request, database, cache, and external API input that needs controlled validation.

Strict versus coercive typing affects backed conversion arguments. Parse external numeric and string forms before enum conversion so the boundary contract does not change when the calling file enables strict types.

  • Use one explicit string or integer backing type.
  • Keep backing values stable and presentation-neutral.
  • Use from for trusted data and tryFrom for untrusted data.
  • Normalize external scalar types before conversion.

Enum Methods and Interfaces

Enums may define methods and implement interfaces. A method can use `$this` and an exhaustive `match` to return case-specific policy, label keys, transitions, or capabilities. Keep database and network effects in services rather than case methods.

An interface lets enum cases participate wherever that capability is accepted. This works well for formatting or policy queries whose inputs are already contained in the case. It does not make enums inheritable.

Static methods can provide parsing or grouped subsets, but cannot replace the built-in `cases()` method. Return typed cases and keep aliases explicit so one external token does not silently map to different meanings over time.

Traits can share permitted behavior with an enum under language restrictions, but broad traits can conceal requirements. Prefer a small direct method or collaborator when behavior needs changing dependencies.

  • Put case-owned deterministic behavior on the enum.
  • Use interfaces for shared capabilities.
  • Keep parsing aliases explicit and stable.
  • Avoid hidden dependencies in enum methods or traits.

Matching and Transitions

A match expression over enum cases can be exhaustive without a default. Adding a new case then exposes every decision that needs review instead of sending it through a generic fallback.

Do not equate case order with business order. The array from `cases()` follows declaration order, which is useful for listing but should not silently define workflow, severity, or sorting. Provide an explicit rank or transition map.

State transitions need current state, requested action, actor permission, and sometimes resource context. An enum can state which transitions are structurally possible, while a service enforces contextual authorization and persists atomically.

Reject impossible transitions with a domain result or exception before side effects. Concurrency still requires a database condition or version check so two valid requests cannot both update stale state.

  • Prefer exhaustive match for closed decisions.
  • Define order independently of declaration position.
  • Separate structural transitions from contextual authorization.
  • Protect state changes against concurrent stale writes.

Storage and JSON

Persist a backed value in a column whose type and constraints match the contract, then convert at the repository boundary. A database check constraint or reference table can prevent unsupported values written outside PHP.

Adding a case may require schema, API, analytics, UI, and consumer updates. Removing or renaming a backing value requires migrating stored rows and messages before code stops recognizing the old representation.

JSON encoding a backed case produces its scalar value under native enum serialization behavior, while a pure enum cannot be encoded that way without a deliberate representation. Explicit response mapping remains clearer and protects public contracts from internal changes.

PHP serialization preserves enum case identity through a dedicated representation, but long-lived serialized class and case names are refactor-sensitive. Prefer versioned transport data over native serialization across deployments or services.

  • Convert stored scalars at the repository boundary.
  • Constrain persisted values outside application code too.
  • Map API representations explicitly.
  • Avoid long-lived native serialization contracts.

Enum Design

Avoid a miscellaneous enum that groups unrelated constants merely to gain namespacing. Every case should answer the same domain question and support the same meaningful operations.

Flags that may be combined are not naturally one enum case unless every valid combination is separately named. Use a set of cases or a dedicated permission value object and validate mutually exclusive combinations.

If a value needs user-defined additions, archival without deletion, localized labels in storage, or per-tenant metadata, a database entity may fit better. Code enums should not force operational data into deployments.

A boolean can be clearer than a two-case enum when the property truly has one stable yes/no meaning. Use an enum when names distinguish several states or prevent ambiguous true and false parameters.

  • Keep every case inside one cohesive domain question.
  • Model combinations as sets or value objects.
  • Use database entities for operator-extensible values.
  • Choose a two-case enum only when names add meaning.

Enum Verification

Test every case through methods and interfaces, every valid backed conversion, unknown values, exhaustive matches, transitions, storage round trips, and JSON mappings. A data provider based on `cases()` helps ensure a new case enters shared tests.

Also keep explicit assertions for business groupings and ordering so a newly declared case cannot inherit an accidental default. Mutation or property-based tests can probe transition invariants across every state pair.

Run syntax checks on PHP 8.1 or later and label features that require newer versions, including constant-expression changes. Static analysis should report unhandled match cases and impossible comparisons between cases and scalars.

During rollout, deploy readers that understand old and new backing values before writers emit the new value. Monitor unknown-value failures because they often reveal mixed service versions or stale data rather than a local enum defect.

  • Exercise every case through shared behavior tests.
  • Assert explicit ordering and grouping rules.
  • Check minimum-version syntax and exhaustive matches.
  • Roll out readers before new persisted values.
Before you move on

Mastery Check

5 checks
  • Use an enum only for a code-owned finite set.
  • Choose pure or stable backed cases deliberately.
  • Parse untrusted values with controlled failure.
  • Keep ordering, transitions, storage, and JSON policy explicit.
  • Test every case and roll out readers before new values.

Pick the Enum Shape

0 of 2 checked

Q1. Which conversion safely handles unknown request input?

Q2. When is an enum a poor fit?

Enum Boundary

  • Untrusted backed value

    Enum::from throws for an unknown value. Use tryFrom at an input boundary and return a validation error rather than allowing an uncaught ValueError.

Try this next

Model a Closed State

0 of 2 completed

  1. Define draft, issued, paid, and void backed cases and reject an unknown request value.
  2. Add a canTransitionTo() method and test valid and invalid state changes.
Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.