PHP Enums
Um pedido da lojinha da Ana pode estar Pendente, Pago ou Enviado, e nada além disso. Um tamanho de camiseta é P, M ou G. Um dia da semana é um de sete. Quando um valor só pode ser um de uma lista fechada, o PHP tem um tipo feito para isso: o enum (de enumeration, "enumeração", uma lista de opções). PHP 8.1+
- O problema de guardar opções em strings soltas.
- Como criar um enum puro e usar os seus casos.
cases(): a lista de todas as opções.- Enums com valor (
stringouint),->value,from()etryFrom(), e oValueErrorreal. - Métodos dentro do enum e o
matchcom enum. - Os erros mais comuns, com as mensagens reais.
Pré-requisitos: classes e objetos, tipos em parâmetros e match. Os exemplos rodam na máquina fictícia da Ana, na pasta ~/poo.
O problema: opções escritas como texto
Sem enum, a Ana guarda o status do pedido como string. Funciona… até alguém digitar diferente:
<?php
function descrever(string $status): string
{
if ($status === "pago") {
return "Pagamento confirmado";
}
return "Aguardando";
}
echo descrever("pago"), "\n";
echo descrever("Pago"), "\n";
echo descrever("pgo"), "\n";
$ php status-texto.php Pagamento confirmado Aguardando « "Pago" com P maiúsculo: errado, e sem aviso Aguardando « "pgo": erro de digitação, e sem aviso
O PHP não tem como saber que "Pago" e "pgo" estão errados: para ele, são só textos. O pedido pago aparece como "Aguardando" e ninguém percebe. A ideia do enum é trocar o texto livre por uma lista fechada que o PHP conhece e confere.
A ideia: um cardápio, não um papel em branco
Num restaurante, se você escreve o pedido num papel em branco, pode sair "fejoada". Se aponta no cardápio, só dá para escolher o que existe. O enum é o cardápio: o PHP conhece a lista inteira e recusa qualquer coisa fora dela.
1. Enum puro
<?php
enum Status
{
case Pendente;
case Pago;
case Enviado;
}
$s = Status::Pago;
var_dump($s);
echo $s->name, "\n";
var_dump($s === Status::Pago);
var_dump($s === Status::Enviado);
var_dump($s instanceof Status);
- enum Status
- Cria um enum chamado
Status. Como uma classe, o nome começa com maiúscula. - case Pendente;
- Cada
case("caso") é uma das opções. Aqui são três, e serão sempre só essas três. - Status::Pago
- Para usar um caso: nome do enum,
::, nome do caso. É um valor que você guarda em variável, passa para funções, compara. - $s->name
- Todo caso tem a propriedade
name("nome"), com o nome do caso como texto. - $s === Status::Pago
- Compare casos com
===. Cada caso existe uma vez só, então "é o mesmo caso?" é sempre uma pergunta exata. - instanceof Status
- Por baixo, cada caso é um objeto do tipo
Status.
$ php status.php enum(Status::Pago) « o var_dump mostra que é um enum, e qual caso Pago bool(true) bool(false) bool(true)
O enum como tipo: o PHP confere por você
A grande vantagem aparece quando o enum vira o tipo do parâmetro. A mesma função de antes, agora exigindo um Status:
<?php
enum Status
{
case Pendente;
case Pago;
case Enviado;
}
function descrever(Status $status): string
{
if ($status === Status::Pago) {
return "Pagamento confirmado";
}
return "Aguardando";
}
echo descrever(Status::Pago), "\n";
echo descrever(Status::Pendente), "\n";
echo descrever("pago"), "\n";
$ php descrever.php
Pagamento confirmado
Aguardando
Fatal error: Uncaught TypeError: descrever(): Argument #1 ($status) must be of type Status, string given, called in /home/ana/poo/descrever.php on line 18 and defined in /home/ana/poo/descrever.php:9
Stack trace:
#0 /home/ana/poo/descrever.php(18): descrever('pago')
#1 {main}
thrown in /home/ana/poo/descrever.php on line 9
"O argumento nº 1 precisa ser do tipo Status, foi passada uma string". Agora o texto solto não passa. E um erro de digitação no caso também é pego na hora:
$ php digitacao.php
Fatal error: Uncaught Error: Undefined constant Status::Pgo in /home/ana/poo/digitacao.php:8
Stack trace:
#0 {main}
thrown in /home/ana/poo/digitacao.php on line 8
(O programa tinha echo Status::Pgo->name;. Para o PHP, os casos funcionam como constantes do enum, daí "constante indefinida".) Compare com a versão de strings, em que "pgo" passava em silêncio.
cases(): todas as opções
Todo enum ganha o método estático cases(), que devolve um array com todos os casos, na ordem em que foram escritos. Útil para montar um menu, um <select> de formulário, um relatório:
<?php
enum Status
{
case Pendente;
case Pago;
case Enviado;
}
foreach (Status::cases() as $caso) {
echo "- ", $caso->name, "\n";
}
echo count(Status::cases()), " opções\n";
$ php cases.php - Pendente - Pago - Enviado 3 opções
Missão: crie um enum Tamanho com os casos P, M e G. O resto do código já está pronto: ele guarda Tamanho::M e mostra o nome e quantos casos existem.
<?php // crie o enum Tamanho aqui $camiseta = Tamanho::M; echo "Tamanho: ", $camiseta->name, "\n"; echo "Casos: ", count(Tamanho::cases()), "\n";
<?php
enum Tamanho
{
case P;
case M;
case G;
}
$camiseta = Tamanho::M;
echo "Tamanho: ", $camiseta->name, "\n";
echo "Casos: ", count(Tamanho::cases()), "\n";
2. Enum com valor (backed enum)
O enum puro vive só dentro do programa. Mas o status do pedido precisa ser guardado num banco de dados, mandado num JSON, recebido de um formulário. Nesses lugares só existem textos e números. Para isso, cada caso pode ter um valor associado, uma string ou um int. Em inglês, esse tipo se chama backed enum ("enum com lastro").
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
case Enviado = "enviado";
}
echo Status::Pago->name, "\n";
echo Status::Pago->value, "\n";
var_dump(Status::from("enviado"));
var_dump(Status::tryFrom("cancelado"));
echo json_encode(["pedido" => 1001, "status" => Status::Pago]), "\n";
foreach (Status::cases() as $caso) {
echo $caso->name, " = ", $caso->value, "\n";
}
- enum Status: string
- O
: stringdiz que todo caso tem um valor do tipo string. Pode ser: int. Nenhum outro tipo é aceito. - case Pago = "pago";
- O caso
Pagotem o valor"pago". Os valores precisam ser diferentes entre si. - ->value
- O valor do caso (
"pago"). É o que você grava no banco ou manda no JSON. - Status::from("enviado")
- O caminho de volta: "de" um valor, encontra o caso. Se o valor não existir, lança um erro.
- Status::tryFrom("cancelado")
- "Tenta de": igual ao
from, mas devolvenullem vez de erro. - json_encode(…)
- Um enum com valor vira o próprio valor no JSON (veja JSON).
$ php status-valor.php
Pago « name: o nome do caso, no código
pago « value: o valor, para fora do programa
enum(Status::Enviado) « from achou o caso
NULL « tryFrom não achou: null
{"pedido":1001,"status":"pago"}
Pendente = pendente
Pago = pago
Enviado = enviado
name × value
name é para o programador (o nome no código, como Pago). value é para o mundo de fora (banco, JSON, URL). Assim você pode renomear um caso no código sem mexer nos dados já gravados, desde que mantenha o valor.
from × tryFrom: quando o valor não existe
Valores de fora do programa podem vir errados. Veja o que o from faz com um valor que não está no enum (repare: "Pago" com maiúscula é diferente de "pago"):
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
case Enviado = "enviado";
}
var_dump(Status::from("Pago"));
$ php from-erro.php
Fatal error: Uncaught ValueError: "Pago" is not a valid backing value for enum Status in /home/ana/poo/from-erro.php:8
Stack trace:
#0 /home/ana/poo/from-erro.php(8): Status::from('Pago')
#1 {main}
thrown in /home/ana/poo/from-erro.php on line 8
""Pago" não é um valor válido para o enum Status". É um ValueError, uma exceção: dá para capturar com try/catch. Na prática, escolha assim:
| Use | Quando | Valor inválido |
|---|---|---|
from($v) | O valor deveria estar certo (veio do seu próprio banco). Valor errado é um bug que precisa aparecer. | ValueError |
tryFrom($v) | O valor veio de fora (formulário, URL, arquivo) e pode estar errado. Você trata o null. | null |
Um padrão muito comum junta o tryFrom com o ?? para ter um valor padrão. Aqui o status vem da linha de comando ($argv[1], veja PHP na Linha de Comando); num site viria de $_GET:
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
case Enviado = "enviado";
}
$texto = $argv[1] ?? "";
$status = Status::tryFrom($texto) ?? Status::Pendente;
echo "Mostrando pedidos: ", $status->name, "\n";
$ php filtro.php enviado Mostrando pedidos: Enviado $ php filtro.php Enviado Mostrando pedidos: Pendente « "Enviado" não é um valor: caiu no padrão $ php filtro.php Mostrando pedidos: Pendente « sem nada: também o padrão
Enum com valor int
Com : int, os valores são números. Bom para níveis, prioridades, códigos:
<?php
enum Prioridade: int
{
case Baixa = 1;
case Media = 2;
case Alta = 3;
}
var_dump(Prioridade::from(3));
var_dump(Prioridade::tryFrom(7));
var_dump(Prioridade::from("2"));
var_dump(Prioridade::from(9));
$ php prioridade.php
enum(Prioridade::Alta)
NULL
enum(Prioridade::Media) « o texto "2" foi convertido para o número 2
Fatal error: Uncaught ValueError: 9 is not a valid backing value for enum Prioridade in /home/ana/poo/prioridade.php:11
Stack trace:
#0 /home/ana/poo/prioridade.php(11): Prioridade::from(9)
#1 {main}
thrown in /home/ana/poo/prioridade.php on line 11
Missão: as entradas vieram de um formulário, e uma delas ("perdido") está errada. Rode e veja o ValueError. Troque o from por tryFrom e, quando o resultado for null, mostre perdido: inválido em vez de parar o programa.
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
case Enviado = "enviado";
}
$entradas = ["pago", "enviado", "perdido"];
foreach ($entradas as $texto) {
$status = Status::from($texto);
echo "$texto: ", $status->name, "\n";
}
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
case Enviado = "enviado";
}
$entradas = ["pago", "enviado", "perdido"];
foreach ($entradas as $texto) {
$status = Status::tryFrom($texto);
if ($status === null) {
echo "$texto: inválido\n";
} else {
echo "$texto: ", $status->name, "\n";
}
}
3. Métodos dentro do enum e match
Um enum pode ter métodos, como uma classe. Dentro deles, $this é o caso atual. E a ferramenta perfeita para "fazer uma coisa diferente para cada caso" é o match. A Ana quer, para cada status do pedido, um texto para o cliente, o próximo passo e se o pedido ainda pode ser cancelado:
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
case Enviado = "enviado";
public function rotulo(): string
{
return match ($this) {
Status::Pendente => "Aguardando pagamento",
Status::Pago => "Pagamento confirmado",
Status::Enviado => "A caminho",
};
}
public function proximo(): ?Status
{
return match ($this) {
self::Pendente => self::Pago,
self::Pago => self::Enviado,
self::Enviado => null,
};
}
public function podeCancelar(): bool
{
return $this === self::Pendente;
}
public static function padrao(): self
{
return self::Pendente;
}
}
$s = Status::padrao();
while ($s !== null) {
$cancelar = $s->podeCancelar() ? "sim" : "não";
echo "{$s->name}: {$s->rotulo()} (pode cancelar? $cancelar)\n";
$s = $s->proximo();
}
$ php pedido-status.php Pendente: Aguardando pagamento (pode cancelar? sim) Pago: Pagamento confirmado (pode cancelar? não) Enviado: A caminho (pode cancelar? não)
- public function rotulo(): string
- Um método comum. Chamado num caso:
Status::Pago->rotulo(). - match ($this) { … }
- Compara o caso atual com cada opção (usando
===) e devolve o texto correspondente. - self::Pendente
- Dentro do enum,
selfé o próprio enum:self::Pagoé o mesmo queStatus::Pago. - ?Status
- O
proximo()devolve umStatusounull(depois de Enviado, não há próximo). O?antes do tipo permite onull. - public static function padrao()
- Métodos estáticos também são permitidos:
Status::padrao()(veja static). - while ($s !== null)
- Começa em Pendente e vai pedindo o próximo até acabar.
Por que match combina tanto com enum
O match não tem "caso nenhum, faça qualquer coisa" a menos que você escreva um default. Se aparecer um caso que ele não conhece, ele para com erro. Com enum, isso é uma rede de segurança: a Ana acrescentou o caso Cancelado e esqueceu do rotulo():
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
case Enviado = "enviado";
case Cancelado = "cancelado";
public function rotulo(): string
{
return match ($this) {
Status::Pendente => "Aguardando pagamento",
Status::Pago => "Pagamento confirmado",
Status::Enviado => "A caminho",
};
}
}
echo Status::Pago->rotulo(), "\n";
echo Status::Cancelado->rotulo(), "\n";
$ php cancelado.php
Pagamento confirmado
Fatal error: Uncaught UnhandledMatchError: Unhandled match case Status::Cancelado in /home/ana/poo/cancelado.php:14
Stack trace:
#0 /home/ana/poo/cancelado.php(19): Status->rotulo()
#1 {main}
thrown in /home/ana/poo/cancelado.php on line 14
"Caso do match não tratado: Status::Cancelado". O erro diz exatamente qual caso faltou. Por isso, com enums, evite o default no match: sem ele, todo caso novo que você esquecer aparece logo no primeiro teste, em vez de cair num texto genérico. (Com switch, o caso esquecido passaria em silêncio; veja switch.)
Constantes e interfaces
Um enum também pode ter constantes (const PADRAO = self::Pendente;) e implementar interfaces (enum Status: string implements TemCor), com os métodos que a interface exige (veja Interfaces). O que ele não pode ter são propriedades: os únicos dados de um caso são name e value. Informação extra (um rótulo, uma cor) vem de métodos com match, como você viu.
Enum puro ou com valor?
Puro: enum Status | Com valor: enum Status: string | |
|---|---|---|
->name | sim | sim |
->value | não | sim |
cases() | sim | sim |
from() / tryFrom() | não | sim |
No json_encode | falha: Non-backed enums have no default serialization | vira o valor |
| Use quando | a opção só existe dentro do programa | precisa ser gravada, enviada ou recebida (banco, JSON, formulário) |
Na dúvida, se o valor um dia vai sair do programa, use enum com valor string: fica legível no banco ("pago" diz mais que 2).
Quando dá errado
new
$ php new-enum.php
Fatal error: Uncaught Error: Cannot instantiate enum Status in /home/ana/poo/new-enum.php:7
Stack trace:
#0 {main}
thrown in /home/ana/poo/new-enum.php on line 7
O que significa: o programa tinha $s = new Status();. "Não é possível instanciar o enum". Os casos já existem prontos; não se cria caso novo em tempo de execução.
Como consertar: use um dos casos: $s = Status::Pendente;.
<?php
enum Status
{
case Pendente;
case Pago;
}
echo "Status: " . Status::Pago . "\n";
$ php enum-texto.php
Fatal error: Uncaught Error: Object of class Status could not be converted to string in /home/ana/poo/enum-texto.php:7
Stack trace:
#0 {main}
thrown in /home/ana/poo/enum-texto.php on line 7
O que significa: um caso é um objeto, não um texto, e enums não podem ter __toString.
Como consertar: escolha o texto que você quer: Status::Pago->name, ->value (se tiver valor) ou um método como ->rotulo().
<?php
enum Status
{
case Pendente = "pendente";
case Pago = "pago";
}
$ php puro-valor.php
Fatal error: Case Pendente of non-backed enum Status must not have a value in /home/ana/poo/puro-valor.php on line 4
Stack trace:
#0 {main}
O que significa: "o caso Pendente de um enum sem valor não pode ter valor". Faltou o : string depois do nome. O contrário (enum : string com um caso sem = "…") dá Case Pago of backed enum Status must have a value.
Como consertar: ou todos os casos têm valor e o enum declara o tipo (enum Status: string), ou nenhum tem.
->value ou from() num enum puro
$ php puro-from.php
Warning: Undefined property: Status::$value in /home/ana/poo/puro-from.php on line 7
Fatal error: Uncaught Error: Call to undefined method Status::from() in /home/ana/poo/puro-from.php:8
Stack trace:
#0 {main}
thrown in /home/ana/poo/puro-from.php on line 8
O que significa: o programa fazia echo Status::Pago->value; e Status::from("Pago") num enum puro. Ele não tem value, nem from/tryFrom.
Como consertar: se você precisa converter de/para texto, transforme-o em enum com valor.
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
}
$contagem = [];
$contagem[Status::Pago] = 1;
$ php chave.php
Fatal error: Uncaught TypeError: Cannot access offset of type Status on array in /home/ana/poo/chave.php:8
Stack trace:
#0 {main}
thrown in /home/ana/poo/chave.php on line 8
O que significa: chaves de array só podem ser int ou string.
Como consertar: use o valor ou o nome como chave: $contagem[Status::Pago->value] = 1;.
Escrever public string $cor; dentro de um enum dá Fatal error: Enum Status cannot include properties. E name/value são somente leitura: $x->value = "x"; dá Cannot modify readonly property Status::$value. Use métodos para informação extra.
Referência rápida
| Escrita | Significado |
|---|---|
enum Nome { case A; case B; } | Enum puro |
enum Nome: string { case A = "a"; } | Enum com valor string (ou : int) |
Nome::A | Um caso |
$caso->name | Nome do caso ("A") |
$caso->value | Valor do caso (só com valor) |
Nome::cases() | Array com todos os casos, em ordem |
Nome::from($v) | Caso com esse valor, ou ValueError |
Nome::tryFrom($v) | Caso com esse valor, ou null |
$a === Nome::A | Comparar casos |
match ($this) { self::A => … } | Um resultado para cada caso (sem default, de propósito) |
function f(Nome $x) | Aceita só casos desse enum |
Missão final
Missão: a lojinha passou a aceitar cancelamentos, e o caso Cancelado já está no enum. Rode e veja o UnhandledMatchError. Conserte o match do rotulo() para que o caso Cancelado mostre Pedido cancelado (sem usar default).
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
case Enviado = "enviado";
case Cancelado = "cancelado";
public function rotulo(): string
{
return match ($this) {
Status::Pendente => "Aguardando pagamento",
Status::Pago => "Pagamento confirmado",
Status::Enviado => "A caminho",
};
}
}
foreach (Status::cases() as $s) {
echo $s->name, ": ", $s->rotulo(), "\n";
}
<?php
enum Status: string
{
case Pendente = "pendente";
case Pago = "pago";
case Enviado = "enviado";
case Cancelado = "cancelado";
public function rotulo(): string
{
return match ($this) {
Status::Pendente => "Aguardando pagamento",
Status::Pago => "Pagamento confirmado",
Status::Enviado => "A caminho",
Status::Cancelado => "Pedido cancelado",
};
}
}
foreach (Status::cases() as $s) {
echo $s->name, ": ", $s->rotulo(), "\n";
}
Exercícios
1. Declarar
Complete a palavra que cria uma lista fechada de opções:
2. Do banco para o enum
O banco devolveu o texto "enviado" em $linha["status"], e ele com certeza é válido. Complete para obter o caso (com erro se o valor estiver errado):
3. Do enum para o banco
Complete para obter o texto "pago" a partir de Status::Pago:
4. O que Status::tryFrom("cancelado") devolve se Status não tem nenhum caso com o valor "cancelado"?
5. Em enum Status: string { case Pago = "pago"; }, o que mostram Status::Pago->name e Status::Pago->value?
6. Por que é bom não usar default num match sobre um enum?
7. Qual destas coisas um enum não pode ter?
Resumo
- Um enum é uma lista fechada de opções (
case). Use quando um valor só pode ser um de poucos. - Use o enum como tipo de parâmetro: o PHP recusa qualquer outra coisa (
must be of type Status, string given). - Compare com
===;->namedá o nome;cases()dá a lista toda. - Enum com valor (
: stringou: int) tem->value,from()(erroValueErrorse não achar) etryFrom()(nullse não achar). frompara dados confiáveis;tryFrom … ?? padrãopara dados de fora.- Enums podem ter métodos, métodos estáticos, constantes e interfaces, mas não propriedades.
match ($this)semdefaulté a dupla perfeita: esquecer um caso viraUnhandledMatchError.