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:
| Method | Does |
|---|---|
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:
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.