GAP·MAP

Modern PHP (8.1+)

[ middle depth ]

Pure vs backed enums, and cases(), from(), tryFrom()

An enum (PHP 8.1) declares a fixed set of cases. A pure enum gives each case only a name; a backed enum pins each case to a unique int or string, read through ->value.

enum Status: string {
    case Draft = 'draft';
    case Published = 'published';

    public function label(): string {
        return match ($this) {           // $this is the current case
            Status::Draft => 'Draft',
            Status::Published => 'Published',
        };
    }
}

Status::cases();               // [Status::Draft, Status::Published], declaration order
Status::from('draft');         // Status::Draft; throws ValueError on an unknown value
Status::tryFrom('archived');   // null; no exception

Every enum gets cases(). Backed enums add from() and tryFrom(). Reach for tryFrom($x) ?? Status::Draft when the input is untrusted, and from() when a bad value is a bug you want to surface loudly.

readonly properties with constructor promotion

Promoting a constructor parameter (adding a visibility modifier) declares and assigns the property in one line. Add readonly (8.1) and it can be written once, from inside the class, then never again.

final class Money {
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}
}

$m = new Money(500, 'USD');
$m->amount = 600;   // Error: Cannot modify readonly property Money::$amount

A readonly property must be typed and cannot carry a default in its declaration, and there is no static readonly. This is the standard shape for a value object: build it in the constructor, then hand it around knowing its fields are fixed.

match compares with === and returns a value

match is an expression, so it yields a value you assign or return, and it compares the subject against each arm with strict ===. One arm runs, there is no fall-through, and a comma lists several subjects for one arm.

$tier = match (true) {       // match(true): first truthy arm wins
    $score >= 90 => 'A',
    $score >= 80 => 'B',
    default      => 'F',
};

Wrong "match is just a tidier switch." switch compares with loose == and falls through until a break; match uses ===, runs exactly one arm, and throws UnhandledMatchError when nothing matches and there is no default. Right treat match as a total function from subject to value, and add default unless you want the throw.

First-class callables and named arguments

strlen(...) builds a Closure from a callable. The ... is three literal dots, not argument unpacking, and it does not call the function; you invoke the returned closure later.

$fn = strlen(...);            // Closure, not a call
$fn('hello');                 // 5
htmlspecialchars($s, double_encode: false);  // named arg skips earlier optionals

Named arguments pass by parameter name, so you can skip optional parameters and stop caring about position. Positional arguments must come before named ones.

[ senior depth ]

Fibers suspend a whole call stack

A Fiber (PHP 8.1) is an interruptible function with its own call stack. It can suspend from anywhere in that stack, including a deeply nested call, and resume later where it left off. You drive it from outside with start(), resume(), throw(), and read its result with getReturn(); from inside the running fiber you call the static Fiber::suspend() and Fiber::getCurrent().

$f = new Fiber(function (): void {
    $v = Fiber::suspend('paused');   // control leaves the fiber; 'paused' -> start()
    echo "resumed with $v";
});
$x = $f->start();   // runs to the suspend; $x === 'paused'
$f->resume('go');   // 'go' becomes the return of suspend(); prints "resumed with go"

The trap is treating this as async/await. A Fiber ships no scheduler and no event loop. Revolt, AMPHP, and ReactPHP build a scheduler on top of fibers; the fiber itself is only the mechanism that lets a coroutine yield control. getReturn() is valid only after termination, and resuming a fiber that is not suspended raises a FiberError.

Fiber versus generator

Both are cooperative and single-threaded, with no parallelism, but the stack is the dividing line. A generator is stackless: only its own top frame can yield, so a helper it calls cannot yield through it without an explicit yield from. A generator also changes the function's return type to Generator and forces every caller to iterate it. A fiber keeps its full stack, suspends transparently from any frame, and leaves the calling convention of the code inside it untouched. Generators are pull-based iterators; fibers are general control transfer for scheduling.

readonly is shallow, and it is not a constant

readonly guards the property slot, not the graph behind it. An object held in a readonly property can still be mutated internally; only reassigning the property fails. Arrays behave differently, because they are value types: $o->arr[] = 1 writes the property in place and throws. A readonly class (8.2) marks every property readonly, forbids untyped and static properties, blocks dynamic properties, and can be extended only by another readonly class.

Wrong "readonly is basically a constant." A class constant is compile-time, class-level, shared by all instances, and must be a constant expression. A readonly property is per-instance, assigned at runtime in the constructor, and can hold a freshly built object unique to each instance. It gives write-once immutability of per-instance state. Since 8.3 you may re-initialize a readonly property inside __clone(), which enables copy-with-changes; in 8.4 the implicit set scope widened to protected(set), so a child class can initialize an inherited readonly property.

Attributes stay inert until Reflection reads them

An attribute (#[Route('/users')]) is structured metadata, the typed replacement for docblock annotations. The engine parses it but never runs it. Nothing happens until Reflection reads it: getAttributes() returns ReflectionAttribute objects, and newInstance() constructs the attribute class.

$rm = new ReflectionMethod(C::class, 'create');
foreach ($rm->getAttributes(Route::class) as $a) {
    $route = $a->newInstance();   // constructor runs HERE, not at declaration
}

Target and IS_REPEATABLE validation also happens at newInstance() time rather than at compile time, so an attribute placed on an illegal target fails only when something reflects it.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more PHP

was this useful?