Your next chapter / PHP enums and explicit state transitions

Intermediate · 45 MINUTES · VERSION 1

PHP enums and explicit state transitions

Distinguish recognized state values from permitted changes and keep authorization/concurrency separate.

Reviewed 23 Oct 2026

Your setup may differ. Windows, Ubuntu, and optional tool bundles →

Log in or create an account to keep private notes, bookmarks, and learning progress. Reading is free without an account.

What you will learn

  • Represent a closed set with a string-backed enum.
  • Validate unknown input before creating a state.
  • Separate valid values, allowed transitions and write permission.

Theory: values are not actions

An enum names a finite set of cases. A backed enum gives each case a scalar value for boundaries such as JSON or database fields. An arbitrary string is not automatically an enum case.

A valid destination value does not make every change valid. This exercise permits Todo→InProgress and InProgress→Done only. Skipping, moving backward and self-transitions are forbidden. These are explicit practice rules, not universal task-product rules.

An enum does not authorize a user, write a database or prevent concurrent updates. A real handler still needs permission checks and a deliberate transaction, stale-write and idempotency policy.

Worked example: an explicit transition table

Required: PHP 8.4+ CLI. Save as task_states.php in a personal folder. No framework/database/paid service. Laragon/XAMPP are optional.

<?php

declare(strict_types=1);

enum WorkState: string
{
    case Todo = 'todo';
    case InProgress = 'in_progress';
    case Done = 'done';

    public function permits(self $next): bool
    {
        return match ($this) {
            self::Todo => $next === self::InProgress,
            self::InProgress => $next === self::Done,
            self::Done => false,
        };
    }
}

function readWorkState(mixed $input): WorkState
{
    if (! is_string($input)) {
        throw new InvalidArgumentException('State must be text.');
    }
    $state = WorkState::tryFrom($input);
    if ($state === null) {
        throw new InvalidArgumentException('Use todo, in_progress, or done.');
    }

    return $state;
}

function moveWorkState(WorkState $current, WorkState $next): WorkState
{
    if (! $current->permits($next)) {
        throw new DomainException('Transition is not allowed.');
    }

    return $next;
}

if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
    $current = readWorkState('todo');
    $next = moveWorkState($current, WorkState::InProgress);
    echo 'Before: '.$current->value.PHP_EOL;
    echo 'Next: '.$next->value.PHP_EOL;
    try {
        moveWorkState(WorkState::Todo, WorkState::Done);
    } catch (DomainException $error) {
        echo 'Rejected: '.$error->getMessage().PHP_EOL;
    }
}

Run and inspect the state

Windows PowerShell:

php ./task_states.php

Ubuntu Bash:

php ./task_states.php
Before: todo
Next: in_progress
Rejected: Transition is not allowed.

PHP must be on PATH. Source is common to both shells. Windows PHP 8.4.26 was executed; Ubuntu was not.

Parsing checks the boundary type before tryFrom. Unknown text yields null and a useful rejection. The match handles each current case. A permitted move returns a case; the original variable remains Todo. The guard prevents demo output when tests require the file.

Guided lab

  1. Write the three states and all nine current/next pairs before running. Predict the two accepted pairs.
  2. Parse exact backing strings. Reject wrong case, whitespace, unknown strings, arrays, null, integers and booleans.
  3. Test every pair against your table. Verify rejected moves throw and the original case stays unchanged.
  4. Explain why a valid Done case cannot be reached directly from Todo.
  5. Consider a repeated request: this function rejects a self-transition. Request idempotency is separate; do not claim it was solved here.

Independent exercise

Add Cancelled: Todo and InProgress may cancel; Done and Cancelled are terminal. Update parsing and the full four-by-four transition matrix. Do not permit every move merely because both cases exist.

Add display labels separate from stable backing values. Hint: user-facing wording may change without changing persisted IDs. Implement this in a separate practice copy, keeping its chosen rules explicit.

Common mistakes and interview

Avoid silent type coercion, confusing enum membership with a permitted move, saving display labels as IDs, adding cases without updating match rules, and claiming enum validation prevents a race condition.

Interview: “Where do you enforce a state change?” Explain parsing, transition rules, authorization, persisted concurrency and repeated-request handling as different responsibilities.

English practice and glossary

Enum: a finite named case set. Backing value: a case’s stable scalar. Transition: a permitted state change. Terminal state: no outgoing moves under chosen rules. Boundary: where external data enters. Exhaustive match: handling each current case.

Say: “The value is recognized, but the action is rejected because __. I tested __. Permission and persistence are checked by __.” Save your test matrix in the journal.

Recap and next step

Use a closed value set and explicit rules for changes. Keep permissions and durable updates separate. Next learn generators and the difference between creating an iterator and completing its work.

Reveal exercise hints and self-check answers

Only Todo→InProgress and InProgress→Done succeed. Unknown or non-string input fails. A valid Done case does not permit a direct Todo→Done move. No database or user permission changes.

Exercise outline: add Cancelled with a stable backing string, make it terminal, explicitly allow cancellation from the two selected sources and test every pair. Keep labels and retry semantics separate.

Official references

Practise on your own computer. The exercise self-checks are not graded. This version also has a scored quiz for verified accounts. Signed-in learners can save reading, self-reported practice, private notes, and bookmarks.