PHP backed enums are used throughout the framework for anything that represents a fixed set of values — entity status codes, type fields, queue names, user groups. The framework provides BasicEnumMethodsTrait as a standard interface for entity-related enums. The conventions described on this page are project conventions, not framework requirements — in your own projects, structure enums however makes sense to you.
The trait defines a standard interface that all entity enums in the project implement:
trait BasicEnumMethodsTrait
{
abstract public static function getById( int $id ): self;
abstract public function getId(): int;
abstract public function getName(): string;
abstract public function getNameCode(): string;
final public function getList(): array
{
return ( new ReflectionClass( static::class ) )
->getConstants( ReflectionClassConstant::IS_PUBLIC );
}
}
| Method | Purpose |
|---|---|
getById( int $id ) |
Look up an enum case by its database integer ID |
getId(): int |
Return the integer ID stored in the database |
getName(): string |
Return the human-readable translated name |
getNameCode(): string |
Return the machine-friendly string used in URLs, filenames, logs |
getList(): array |
Return all cases as an array via reflection |
The trait enforces a consistent API across all entity enums — any code that works with one enum works with any other because they all implement the same four methods.
UserGroupEnum is the clearest example of the full pattern in practice:
// App/Enums/UserGroupEnum.php
namespace App\Enums;
use App\Abstraction\Traits\BasicEnumMethodsTrait;
use App\Entity\UserGroup;
enum UserGroupEnum: string
{
use BasicEnumMethodsTrait;
case BANNED = 'banned';
case ADMINISTRATOR = 'administrator';
case MODERATOR = 'moderator';
case USER = 'user';
public static function getById( int $id ): self
{
return match ( $id ) {
-1 => self::BANNED,
1 => self::ADMINISTRATOR,
3 => self::MODERATOR,
6 => self::USER,
};
}
public function getId(): int
{
return match ( $this ) {
self::BANNED => -1,
self::ADMINISTRATOR => 1,
self::MODERATOR => 3,
self::USER => 6,
};
}
public function getName(): string
{
return _t(
match ( $this ) {
self::BANNED => 'Banned',
self::ADMINISTRATOR => 'Administrator',
self::MODERATOR => 'Moderator',
self::USER => 'User',
}
);
}
public function getNameCode(): string
{
return match ( $this ) {
self::BANNED => 'banned',
self::ADMINISTRATOR => 'administrator',
self::MODERATOR => 'moderator',
self::USER => 'user',
};
}
public function getEntity(): UserGroup
{
return UserGroup::find( $this->getId() );
}
}
A few things worth noting:
getName() passes the string through _t() — the displayed name is translated to the current locale. getNameCode() does not — it returns a stable machine-readable string that doesn't change with locale.
getEntity() is an optional addition not part of BasicEnumMethodsTrait but useful for enums that directly correspond to a lookup table entity.
getById() uses match without a default arm — if an unknown ID is passed, PHP throws an UnhandledMatchError. This is intentional. An unknown ID means something is wrong at the data layer and should be loud, not silently return null.
These are project conventions for any project built with this framework. They exist to keep enum files easy to find and their purpose immediately obvious from the file path alone.
Enums live in App/Enums/ with subdirectories mirroring the entity directory structure:
App/
├── Entity/
│ ├── User.php
│ ├── UserGroup.php
│ └── InvoicesUK/
│ └── Procedure.php
│
└── Enums/
├── Entities/
│ ├── UserGroupEnum.php
│ └── InvoicesUK/
│ ├── ProcedureInvoiceTypeEnum.php
│ └── ProcedureStatusCodeEnum.php
└── Amqp/
└── QueueEnum.php
The rule: if an entity lives at App/Entity/{SubDir}/{EntityName}.php, its enums live at App/Enums/Entities/{SubDir}/{EntityName}{DescriptorEnum}.php.
For non-entity enums (queues, feature flags, configuration options), use a descriptive subdirectory under App/Enums/ — Amqp/, Config/, Feature/, etc.
The pattern is {EntityName}{Descriptor}Enum:
Entity: App/Entity/InvoicesUK/Procedure.php
↓
Enum: App/Enums/Entities/InvoicesUK/ProcedureInvoiceTypeEnum.php
Enum: App/Enums/Entities/InvoicesUK/ProcedureStatusCodeEnum.php
{EntityName} — the entity class name, exactly as written.
{Descriptor} — what the enum represents on that entity. Keep it specific — InvoiceType, StatusCode, PaymentMethod, not just Type or Status.
Enum — always the suffix.
Enum cases use PascalCase — each word capitalised, no underscores:
enum ProcedureStatusCodeEnum: string
{
case FirstNotice = 'first_notice';
case SecondNotice = 'second_notice';
case FinalNotice = 'final_notice';
case Resolved = 'resolved';
case WrittenOff = 'written_off';
}
The case name is what you read in PHP code. The backed string value is what goes into the database or URL — use snake_case for the value, PascalCase for the name.
Not SECOND_NOTICE, not second_notice — SecondNotice. The reasoning: PHP enums aren't constants, they're proper types. ProcedureStatusCodeEnum::SecondNotice reads naturally as a value of that type, whereas ProcedureStatusCodeEnum::SECOND_NOTICE reads like a constant from a 2010 PHP codebase.
Building on the conventions above — an enum for a hypothetical Procedure entity in App/Entity/InvoicesUK/:
// App/Enum/Entities/InvoicesUK/ProcedureStatusCodeEnum.php
namespace App\Enum\Entities\InvoicesUK;
use App\Abstraction\Traits\BasicEnumMethodsTrait;
enum ProcedureStatusCodeEnum: string
{
use BasicEnumMethodsTrait;
case FirstNotice = 'first_notice';
case SecondNotice = 'second_notice';
case FinalNotice = 'final_notice';
case Resolved = 'resolved';
case WrittenOff = 'written_off';
public static function getById( int $id ): self
{
return match ( $id ) {
1 => self::FirstNotice,
2 => self::SecondNotice,
3 => self::FinalNotice,
4 => self::Resolved,
5 => self::WrittenOff,
};
}
public function getId(): int
{
return match ( $this ) {
self::FirstNotice => 1,
self::SecondNotice => 2,
self::FinalNotice => 3,
self::Resolved => 4,
self::WrittenOff => 5,
};
}
public function getName(): string
{
return _t(
match ( $this ) {
self::FirstNotice => 'First Notice',
self::SecondNotice => 'Second Notice',
self::FinalNotice => 'Final Notice',
self::Resolved => 'Resolved',
self::WrittenOff => 'Written Off',
}
);
}
public function getNameCode(): string
{
return $this->value;
}
public function isActive(): bool
{
return match ( $this ) {
self::FirstNotice,
self::SecondNotice,
self::FinalNotice => true,
default => false,
};
}
}
And the corresponding invoice type enum for the same entity:
// App/Enum/Entities/InvoicesUK/ProcedureInvoiceTypeEnum.php
namespace App\Enum\Entities\InvoicesUK;
use App\Abstraction\Traits\BasicEnumMethodsTrait;
enum ProcedureInvoiceTypeEnum: string
{
use BasicEnumMethodsTrait;
case Standard = 'standard';
case Proforma = 'proforma';
case CreditNote = 'credit_note';
public static function getById( int $id ): self
{
return match ( $id ) {
1 => self::Standard,
2 => self::Proforma,
3 => self::CreditNote,
};
}
public function getId(): int
{
return match ( $this ) {
self::Standard => 1,
self::Proforma => 2,
self::CreditNote => 3,
};
}
public function getName(): string
{
return _t(
match ( $this ) {
self::Standard => 'Standard Invoice',
self::Proforma => 'Proforma Invoice',
self::CreditNote => 'Credit Note',
}
);
}
public function getNameCode(): string
{
return $this->value;
}
}
Enums integrate with entities via the getById() / getId() pattern — the database stores the integer ID, the application works with enum cases:
// App/Entity/InvoicesUK/Procedure.php
#[Validate\OneOfTheNumbers( [ 1, 2, 3, 4, 5 ] )]
#[TranslatablePropertyName( 'Status Code' )]
#[ORM\Column( type: 'integer', options: [ 'default' => 1 ] )]
protected int $statusCode = 1;
public function getStatusCode(): ProcedureStatusCodeEnum
{
return ProcedureStatusCodeEnum::getById( $this->statusCode );
}
public function setStatusCode( ProcedureStatusCodeEnum $statusCode ): self
{
$this->statusCode = $statusCode->getId();
return $this;
}
The getter returns the enum case, the setter accepts an enum case and stores the integer. The database column stores only the integer:
// Reading
$procedure->getStatusCode() // ProcedureStatusCodeEnum::SecondNotice
$procedure->getStatusCode()->getName() // 'Second Notice' (translated)
$procedure->getStatusCode()->getNameCode() // 'second_notice'
$procedure->getStatusCode()->getId() // 2
$procedure->getStatusCode()->isActive() // true
// Writing
$procedure->setStatusCode( ProcedureStatusCodeEnum::Resolved );
// Comparison
if ( $procedure->getStatusCode() === ProcedureStatusCodeEnum::WrittenOff ) {
// handle written off case
}
Not all enums map to entity fields. QueueEnum is a good example — it defines available RabbitMQ queues and provides access to the message bus:
// App/Enums/Amqp/QueueEnum.php
enum QueueEnum: string
{
case DEFAULT = 'default_queue';
public function getMessageBus(): RabbitMQ
{
return RabbitMQ::getInstance( $this );
}
public function consume( callable $callback ): void
{
( new RabbitMQConsumer() )->consume( $this, $callback );
}
}
QueueEnum doesn't implement BasicEnumMethodsTrait — it doesn't need getId(), getName(), or getNameCode() because it's not representing a database field value. Only add the trait where it makes sense.
BasicEnumMethodsTrait::getList() returns all public constants of the enum via reflection — useful for populating select dropdowns or validation:
// All cases as array
ProcedureStatusCodeEnum::getList();
// [
// 'FirstNotice' => ProcedureStatusCodeEnum::FirstNotice,
// 'SecondNotice' => ProcedureStatusCodeEnum::SecondNotice,
// ...
// ]
// Populate a select element in a view
<?php foreach ( ProcedureStatusCodeEnum::getList() as $case ) : ?>
<option value="<?= $case->getId() ?>">
<?= $case->getName() ?>
</option>
<?php endforeach ?>
// Dynamic validation against all valid IDs
$validIds = array_map(
fn( ProcedureStatusCodeEnum $case ) => $case->getId(),
ProcedureStatusCodeEnum::getList()
);
Inconsistent ID assignment — getId() values must be stable. If you change the integer returned by getId() for an existing case, every row in the database with that stored integer now maps to the wrong case. Treat enum IDs as immutable once they're in production data.
Adding a case without a database migration — if your validation uses OneOfTheNumbers with a hardcoded list of valid IDs and you add a new enum case, the validator will reject the new ID until the validation list is updated. Keep OneOfTheNumbers values in sync with the enum's getId() returns, or derive them dynamically:
// Fragile — must be updated manually
#[Validate\OneOfTheNumbers( [ 1, 2, 3, 4, 5 ] )]
// Better — derive from the enum
// (requires a const or static method on the enum)
Using the backed string value as the database ID — the backed string value (case SecondNotice = 'second_notice') is for getNameCode() and URL usage. The integer from getId() is what gets stored in the database column. Don't conflate the two — they serve different purposes.
Skipping getNameCode() when value would do — for string-backed enums where the backed value is already the name code, getNameCode() can just return $this->value. That's fine and is what ProcedureStatusCodeEnum does above. Don't add unnecessary logic just to feel like you're implementing the interface properly.