lamoda/enum-bundle
Lamoda EnumBundle is a lightweight Symfony bundle for modeling enumerated values as typed PHP classes, with labels, validation, Doctrine DBAL mapping, and form choice integration, helping teams keep reusable enum definitions consistent across domain logic and UI in Symfony applications.
Install the bundle; it pulls in lamoda/php-enum transitively:
composer require lamoda/enum-bundle
As of 1.6 the bundle supports Symfony 6 (alongside the 4/5 lines it previously targeted), so it can be dropped into a current LTS app without a compatibility shim. Symfony Flex registers it automatically; otherwise add it to config/bundles.php:
return [
// ...
Lamoda\EnumBundle\LamodaEnumBundle::class => ['all' => true],
];
Define your first enum by extending the base Enum class and declaring constants:
namespace App\Enum;
use Lamoda\Enum\Enum;
final class OrderStatus extends Enum
{
public const NEW = 'new';
public const PAID = 'paid';
public const SHIPPED = 'shipped';
}
Use it as a value object:
$status = new OrderStatus(OrderStatus::NEW);
$status->getValue(); // 'new'
OrderStatus::getValues(); // ['new', 'paid', 'shipped']
OrderStatus::isValid('paid'); // true
Where to look first: the bundle's form type (Lamoda\EnumBundle\Form\Type\EnumType) and its serializer normalizer service, since those are the two integration points you'll touch daily. The first realistic use case is replacing a plain string $status on an entity with an OrderStatus and wiring it into a form and the Symfony Serializer.
Forms. Add the bundle's enum type wherever you previously used a ChoiceType with hard-coded choices:
use Lamoda\EnumBundle\Form\Type\EnumType;
$builder
->add('status', EnumType::class, [
'class' => OrderStatus::class,
]);
The class option is required; the type derives the allowed choices from the enum's values. Override choice_label if you want human-readable labels instead of raw values.
Entities and serializer. Declare the property with the enum type and let the normalizer translate it:
class Order
{
private OrderStatus $status;
public function getStatus(): OrderStatus { return $this->status; }
public function setStatus(OrderStatus $status): void { $this->status = $status; }
}
With the bundle enabled, serializing an Order emits "status": "paid" and denormalizing JSON back into Order constructs the enum from the scalar, as long as the target property/constructor parameter is typed OrderStatus. If you build a Serializer manually (outside the framework), register the bundle's enum normalizer explicitly and place it ahead of generic object normalizers:
$serializer = new Serializer([
new EnumNormalizer(),
new ObjectNormalizer(/* ... */),
]);
Doctrine. There is no automatic DBAL type; the common pattern is to persist the scalar and hydrate the object:
// storing
$order->setStatus(new OrderStatus(OrderStatus::PAID));
$conn->insert('orders', ['status' => $order->getStatus()->getValue()]);
// reading
$status = new OrderStatus($row['status']);
For query building, compare against getValue() strings rather than enum objects.
Controllers and templates. The form's getData() returns your entity with a real enum instance, so a standard flow works unchanged:
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$order = $form->getData(); // $order->getStatus() is an OrderStatus
$this->em->persist($order);
}
In Twig, either use __toString() ({{ order.status }}) or access the scalar ({{ order.status.value }}), the latter being safer for output escaping and comparisons.
Validation. Use the static isValid() when validating raw input (e.g., query parameters not going through a form), or write a small custom constraint that delegates to it.
composer.json platform constraints line up.=== compares instance identity, so two OrderStatus('paid') objects are not identical. Compare getValue() or use the enum's equals() helper where available. Never compare an enum to a raw string.OrderStatus $status cannot hold null. Use ?OrderStatus for optional fields, and make sure Doctrine hydrators and form data mappers handle the null case.class option is mandatory on EnumType. Forgetting it produces a confusing form-building error rather than a "missing option" message.bin/console debug:container and, if needed, reorder or decorate.mixed will stay strings. For DTOs, type constructor parameters explicitly.string column (or register a custom DBAL type) and construct the enum on hydrate; wrap construction so invalid legacy data surfaces as a clear error, not a raw constructor exception.Lamoda\Enum\Enum. The enum keyword produces a different construct and won't be picked up by the form type or normalizer. This is the sharpest edge now that Symfony 6 apps are typically on PHP 8.1+ — the language feature is right there, but it is not interchangeable with this package.bin/console cache:clear; stale containers are a frequent source of "service not found" confusion — particularly after a version bump where the container was compiled against the old constraint set.Enum for shared behavior, override form options in a custom type, decorate the normalizer for custom scalar formats, or add a DBAL type if you want transparent Doctrine mapping.composer.lock pinned and isolate usages behind thin adapters (form type wrappers, a serialization layer) so migrating to native enums or myclabs/php-enum later is a contained change.How can I help you explore Laravel packages today?