osama-98/laravel-enum-translatable
A Laravel package for adding translations to PHP enums. Define localized labels and descriptions for enum cases using Laravel’s translation system, making it easy to display human-readable enum values across multiple languages.
EnumWrappable trait to the backed enum you want to hydrate from untrusted input (request payloads, query strings, DB columns, queue payloads).src/EnumWrappable.php or equivalent) — wrap() is the only surface you'll use day to day; everything else is implementation detail.wrap(). 2.4 changed that behavior, and it's the part most likely to bite an existing codebase.use App\Enums\Status;
use Illuminate\Http\Request;
$status = Status::wrap($request->input('status')); // ?Status
The return type is now ?static, so static analysis and your IDE both know Status::wrap(...) yields ?Status — no instanceof guard needed before using its methods.
Form requests / controllers. wrap() is the natural replacement for Status::tryFrom($value) ?: null guards and for try/catch around from(). Resolve once at the boundary and pass the typed enum inward:
final class UpdateOrderRequest extends FormRequest
{
public function rules(): array
{
return ['status' => ['required', 'string']];
}
public function status(): Status
{
return Status::wrap($this->input('status'))
?? throw ValidationException::withMessages([
'status' => 'Unknown status.',
]);
}
}
Strict vs non-strict. Use non-strict (null on no-match) at HTTP boundaries so you can convert failures into validation errors with your own message. Use strict mode (throws ValueError) in seeders, config resolvers, migrations, and anywhere a bad value indicates a programming error rather than user input. Keep that split deliberate — do not let strict mode leak into request handling, where it turns a 422 into a 500.
Foreign-enum interop. Since 2.4 you can safely bridge two enums that share a backing vocabulary. The value is unwrapped and re-resolved against the calling enum:
// Role::ADMIN is backed by 'admin'; Status also has a case backed by 'admin'
$status = Status::wrap(Role::ADMIN); // ?Status — Status::Active, not Role::ADMIN
This is the pattern to use when a domain event carries an enum from one bounded context and a consumer needs it expressed in its own vocabulary. Write a small dedicated method (e.g. Status::fromRole()) as the named entry point rather than sprinkling Status::wrap($role) calls around — it documents the mapping and gives you one place to change when the vocabularies drift.
Collection / batch hydration. array_map(Status::wrap(...), $values) works cleanly now that the return type is ?static, but filter with !== null rather than truthiness (see gotchas).
Testing. The package's own suite covers the interesting edges — foreign enum matching/non-matching under both modes, and '0'-backed cases. Mirror those cases in your app tests; they are the regressions that actually happen.
Status::wrap(Role::ADMIN) returns a Status (or throws, or null) instead of a Role. Any code doing $result === Role::ADMIN will now fail, and any code relying on the pass-through for type detection is broken. Grep for wrap( calls that receive something other than a string|int|null.Status::wrap(Role::ADMIN) returning Status::Active may be exactly right or a nasty domain bug — the package can't tell. If the vocabularies aren't genuinely shared, reject foreign enum instances at the edge and only accept scalars.'0' is no longer falsy. The emptiness check now only short-circuits on null and ''; the old empty() call treated the string '0' as absent and returned null for a case legitimately backed by '0'. If you had a workaround for that, remove it. But audit your own code too: if (!$status) and ?: chains have the same '0' blind spot at the call site. Test with $status === null.ValueError vs null is a policy decision, not a detail. The same call can either throw or return null depending on mode. Centralize the choice — a config value or a single resolver — otherwise you get inconsistent failure modes for the same bad input across endpoints.?static versus ?BackedEnum can change PHPStan/Psalm results: previously-narrowed unions may now resolve differently, and any @var BackedEnum annotations you added to paper over the old signature should be deleted. Re-run your static analysis after the bump — the fixes are usually deletions.null result. Trace in order: was the input null or '' (short-circuits to null by design)? Was it a foreign enum whose value doesn't exist in the calling enum (non-strict → null)? Was it a scalar that simply doesn't match any case? Those three paths look identical from the call site, so log the raw input, not just the result.'active' → 'enabled'), wrap wrap() in your own static factory rather than forking the trait. That keeps you on the upgrade path for fixes like the '0' one.How can I help you explore Laravel packages today?