planeta jupiter planeta tierra

Enums en PHP: adiós a las constantes y a los strings mágicos

· 5 min de lectura · PHP

La palabra enum con tres casos de ejemplo
nave extraterrestre
La palabra enum con tres casos de ejemplo

Durante años, en PHP un estado se guardaba como const PAGADO = 'pagado' y se pasaba por ahí como un string cualquiera. Funcionaba hasta que alguien escribía 'pagao'. Desde PHP 8.1 el lenguaje tiene enums: un tipo con una lista cerrada de valores que el motor comprueba por ti. Vamos a verlos con ejemplos cortitos.

El problema: los strings mágicos

function cambiarEstado(string $estado) { /* ... */ }

cambiarEstado('pagado'); // bien
cambiarEstado('pagao');  // también «bien»… y es un bug

La función pide un string, así que cualquier string vale. El error no aparece aquí, sino mucho después, cuando algo no encuentra el valor que esperaba.

Tu primer enum

enum Estado
{
    case Pendiente;
    case Pagado;
    case Enviado;
}

function cambiarEstado(Estado $estado) { /* ... */ }

cambiarEstado(Estado::Pagado); // bien
cambiarEstado('pagao');        // TypeError

Ahora la función solo acepta uno de los tres casos. Ni typos ni valores inventados, y el editor te autocompleta Estado:: con las opciones válidas.

Para comparar, ===:

$estado = Estado::Pagado;

$estado === Estado::Pagado; // true
$estado->name;              // 'Pagado'

Enums con valor (backed)

Si el estado tiene que guardarse en una base de datos o viajar en un JSON, necesitas un valor que lo represente. Se lo das así:

enum Estado: string
{
    case Pendiente = 'pendiente';
    case Pagado    = 'pagado';
    case Enviado   = 'enviado';
}

Estado::Pagado->value; // 'pagado'

Y para ir al revés, del texto al caso:

Estado::from('pagado');    // Estado::Pagado
Estado::from('pagao');     // ValueError

Estado::tryFrom('pagado'); // Estado::Pagado
Estado::tryFrom('pagao');  // null

Usa from() cuando el valor tiene que ser válido, y tryFrom() cuando viene de fuera (un formulario, la URL) y puede traer cualquier cosa. Con ?? pones un valor por defecto en una línea:

$estado = Estado::tryFrom($_GET['estado'] ?? '') ?? Estado::Pendiente;

Y cases() te da todos los casos, por ejemplo para pintar un <select>:

foreach (Estado::cases() as $estado) {
    echo $estado->value; // pendiente, pagado, enviado
}

Los enums pueden tener métodos

Aquí está lo mejor: la lógica que depende del estado puede vivir dentro del propio enum.

enum Estado: string
{
    case Pendiente = 'pendiente';
    case Pagado    = 'pagado';
    case Enviado   = 'enviado';

    public function color(): string
    {
        return match ($this) {
            Estado::Pendiente => 'gris',
            Estado::Pagado    => 'verde',
            Estado::Enviado   => 'azul',
        };
    }
}

Estado::Pagado->color(); // 'verde'

Fíjate en que el match no tiene default. Si mañana añades case Cancelado y te olvidas de darle color, PHP lanza un error en vez de devolver cualquier cosa en silencio. Mejor enterarte así.

Enums en Symfony y Doctrine

Todo el ecosistema los entiende sin que tengas que hacer nada especial.

Doctrine guarda el value en la columna y te devuelve el caso al leer:

#[ORM\Column(enumType: Estado::class)]
private Estado $estado = Estado::Pendiente;

En una ruta, Symfony convierte el parámetro solo. Si el valor no existe, devuelve un 404:

#[Route('/pedidos/{estado}')]
public function listar(Estado $estado): Response
{
    // /pedidos/pagado → Estado::Pagado
    // /pedidos/pagao  → 404
}

En un formulario, EnumType crea el desplegable con todos los casos:

$builder->add('estado', EnumType::class, ['class' => Estado::class]);

Y en Twig llamas a sus métodos como a los de cualquier objeto:

<span class="badge {{ pedido.estado.color }}">{{ pedido.estado.value }}</span>

¿Con valor o sin él?

Si el enum… Usa
se guarda en base de datos o sale en una API enum Estado: string
solo se usa dentro del código enum Estado

Ante la duda, con string: un 'pagado' en la base de datos se entiende a simple vista; un 2, no.

Lo que se rompe si no lo sabes

  • No sirven como clave de array. $totales[Estado::Pagado] da error. Usa $totales[Estado::Pagado->value].
  • No guardan datos. Un caso no puede tener propiedades que cambien. Para eso, una clase normal.
  • Un enum sin valor no va a JSON. json_encode(Estado::Pagado) devuelve false si el enum no tiene valor.
  • from() con datos del usuario es un error 500 esperando. Si viene de fuera, tryFrom().
  • Cambiar un value rompe los datos viejos. Las filas que ya tenían el valor antiguo dejan de poder leerse. Migra antes.

Y ya está

Si tienes un grupo de constantes con el mismo prefijo, o un string que en realidad solo puede valer tres o cuatro cosas, ahí hay un enum esperando. Empieza por uno y verás cuántos if y validaciones a mano desaparecen.

← Volver al blog

Mi hija de 11 años me ayudó a crear el diseño de este portfolio.