greg0ire/enum
A lightweight PHP enumeration library that lets you define enum-like classes with constants, validate values, and access available constants. It provides a simple AbstractEnum base class for projects that need typed, reusable sets of values without heavy dependencies.
Install with Composer — the package is a single, dependency-free abstract class:
composer require greg0ire/enum
Then declare each enum-like concept as a final class of public constants extending AbstractEnum:
<?php
declare(strict_types=1);
namespace App\Domain\Order;
use Greg0ire\Enum\AbstractEnum;
final class Status extends AbstractEnum
{
public const PENDING = 'pending';
public const PAID = 'paid';
public const SHIPPED = 'shipped';
public const CANCELLED = 'cancelled';
}
Where to look first: open src/AbstractEnum.php. It is one small file of static helpers built on ReflectionClass::getConstants(). Reading it end-to-end (5 minutes) is faster and more reliable than skimming docs, because the entire contract of the package — which helpers exist, how they treat duplicate values, whether they cache reflection — is visible there.
First use case: validating an untrusted scalar at a boundary. Instead of scattering in_array($status, ['pending', 'paid', ...]) around the codebase:
if (!Status::isValid($request->get('status'))) {
throw new \InvalidArgumentException('Unknown order status');
}
You now have a single source of truth for the set of legal values, usable from controllers, form types, Doctrine types, fixtures and API docs alike.
Validation at the edge, string in the core. Since the library gives you no type enforcement, treat the enum class as a catalogue, not a type. Validate once when data enters (HTTP request, message consumer, CLI argument) and let plain strings flow through your services. This keeps signatures simple and avoids pretending you have type safety you don't.
Doctrine column type. Wrap the enum in a DBAL type so persistence validates on the way in:
final class StatusType extends Type
{
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return $platform->getStringTypeDeclarationSQL($column);
}
public function convertToPHPValue($value, AbstractPlatform $platform): ?string
{
if ($value !== null && !Status::isValid($value)) {
throw new \UnexpectedValueException("Invalid status: $value");
}
return $value;
}
}
Register it in doctrine.dbal.types and use type: "status" on the mapping.
Symfony Form / ChoiceType. Status::getConstants() gives a ready-made label => value array, which is exactly what ChoiceType wants:
$builder->add('status', ChoiceType::class, [
'choices' => array_flip(Status::getConstants()),
]);
Keep user-facing labels in translation files (enum.status.pending) rather than hard-coding them in the enum class — the constants should stay language-agnostic.
API contracts and OpenAPI. Generate an enum schema directly from Status::getValues(). Anything that accepts the field can be asserted against the same array, so the schema cannot drift from the code.
Fixtures and tests. Faker's randomElement(Status::getValues()) produces valid data without duplicating the list. For PHPUnit, write one data-provider-driven test that asserts isValid() accepts every value in getValues() and rejects a sentinel string — that test then covers every constant you add later.
Static analysis. Constants are untyped, so annotate the value union once and reuse it:
/** @psalm-type StatusValue = 'pending'|'paid'|'shipped'|'cancelled' */
This is the closest thing to real enum typing you get, and it makes PHPStan/Psalm flag impossible match arms.
static, so Status::isValid(...) is a runtime check that a string happens to be in a list. Design your signatures accordingly and don't build domain logic around the class.enum Status: string { case Pending = 'pending'; } with ::from() / ::tryFrom(). Treat this package as a legacy bridge. Migration is mechanical: isValid($v) → Status::tryFrom($v) !== null, getValues() → array_column(Status::cases(), 'value'), getConstants() → a map over cases(). If you adopt it today, add a TODO and set a deprecation date.private static ?array $cache in your own subclass, or hoist the call out of the loop.array_flip or array_search become ambiguous — getKey('x')-style helpers return only the first match. Avoid aliases unless you genuinely need backwards-compatible names.getConstants() vs getValues(). Use the former when you need names (forms, labels); the latter when you need payload values (persistence, API). Mixing them up is the most common source of "why is my select showing PENDING".final and use static::. Helpers rely on late static binding; a non-final subclass that redeclares a constant will silently change what the parent's helpers see for that subclass. final documents intent and prevents the footgun.getValues()[0]. Define an explicit DEFAULT constant and reference it by name.pending → paid), put that in a service or a real state machine, not in the constant catalogue.switch/match over the values and any generated API schema changes. Grep for getConstants() and getValues() consumers before merging.How can I help you explore Laravel packages today?