Framework
Features

Enums

Your own enums join the ones the framework generates by implementing one interface and using one trait — which is what makes them safe to read from a request, store in a column and send back in a response.

Declaring one

A framework enum implements Enum and JsonSerializable, uses the IsEnum trait, and opens with a None case. A PHPStan rule enforces all three, so the shape is never optional:

use Framework\Enum\Enum;
use Framework\Enum\IsEnum;
use JsonSerializable;

enum Plan implements Enum, JsonSerializable {
    use IsEnum;

    case None;
    case Free;
    case Pro;
    case Enterprise;
}

None is what every conversion falls back to, which is why it comes first and why nothing in the framework returns null for an enum. A pure enum like the one above is identified by its case name; a backed one by its value, so use a backed enum when the stored string has to differ from the case:

enum Plan: string implements Enum, JsonSerializable {
    use IsEnum;

    case None       = "";
    case Free       = "free";
    case Pro        = "pro";
    case Enterprise = "enterprise";
}

Reading one in

Input arrives as a string, and fromValue() is the only conversion you need — it takes a string, a case, or another enum, and returns None when nothing matches instead of throwing:

Plan::fromValue("pro");        // Plan::Pro
Plan::fromValue("nonsense");   // Plan::None
Plan::fromValue($plan);        // already a case — returned as is

// Or fall back to something other than None
Plan::fromValue($request->plan, Plan::Free);

For a multi-select, fromList() takes the array a request sends and gives back a list of cases, skipping what does not map:

$plans = Plan::fromList($request->plans);   // list<Plan>

foreach ($plans as $plan) {
    // ...
}

Use these rather than the native from(), tryFrom() and cases(), which a rule disallows precisely because they throw or return null.

Working with the cases

The trait covers the questions you actually ask of an enum — is this valid, is it one of these, what are they all — so none of it needs writing per enum:

MethodDoes
fromValue($value, $default)The case for a string, or the default — None unless you say otherwise.
fromList($values)A list of cases from an array of strings.
isValid($value)Whether a value maps to a case.
contains($values, $case)Whether a case is in a given set.
getAll()Every case as a list.
getNames()The case names.
toString()The name, or the backing value when the enum is backed. None is the empty string.
jsonSerialize()That same string, so a case encodes cleanly inside a Response.
Plan::isValid("pro");                            // true
Plan::contains([ Plan::Pro, Plan::Enterprise ], $plan);   // is it a paid plan
Plan::getAll();                                  // [Plan::None, Plan::Free, …]

Plan::Pro->toString();     // "pro" when backed, "Pro" when not
Plan::None->toString();    // "" — always

Because match on an enum is exhaustive and a rule checks that every case is handled, adding a case to the enum turns every place that switches on it into a build error rather than a silent fallthrough:

$price = match ($plan) {
    Plan::None, Plan::Free => 0,
    Plan::Pro              => 20,
    Plan::Enterprise       => 100,
};

In a model and a query

Typing a model field as your enum is all it takes to store it — the column is written as the case name, and read back as the case:

src/Account/Model/AccountModel.php
namespace App\Account\Model;

use App\Account\Plan;

use Framework\Database\Model\Model;
use Framework\Database\Model\Field;
use Framework\Database\Model\Requested;

#[Model(hasTimestamps: true, canCreate: true, canEdit: true)]
class AccountModel {

    // ...

    #[Field]
    #[Requested]
    public Plan $plan;
}

The generated query then exposes that column as an EnumWhere, which takes the case itself, so a filter can never compare against a string that does not exist:

$query = new AccountQuery();
$query->plan->equal(Plan::Pro);
$query->plan->in([ Plan::Pro, Plan::Enterprise ]);

Returning one needs nothing either — jsonSerialize() means a case inside an entity or an array becomes its string on the way out:

return Response::data([
    "plan"  => $account->plan,     // "pro"
    "plans" => Plan::getAll(),     // every case, as strings
]);

Enum-keyed maps

A PHP array only takes int or string keys, so an enum case cannot be one. Map is the container that can — the keys are cases, and the getters are typed:

use Framework\Enum\Map;

$limits = new Map();
$limits->set(Plan::Free, 10);
$limits->set(Plan::Pro,  1000);

$limits->getInt(Plan::Pro);     // 1000
$limits->has(Plan::Enterprise); // false
$limits->count();               // 2

foreach ($limits as $plan => $limit) {
    // ...
}

It also has get() and getString(), isEmpty() / isNotEmpty(), and serializes to JSON with the case names as keys.

The framework's own enums — the generated Access and Status, the setting types, the date periods — are documented with the subsystem that uses them. They are all built exactly like the one above.