PHP 8+ Moderno: Features Clave que Redefinen el Desarrollo (Union Types, Enums, Readonly y Más)
Introducción
PHP 8 ha marcado un antes y un después para los desarrolladores que buscan un lenguaje más seguro, expresivo y moderno. Las nuevas características no sólo mejoran la legibilidad del código, sino que también permiten capturar mejor la intención del programador al compilar tiempo, reduciendo así los errores comunes en tiempo de ejecución.
Este tutorial está dirigido a desarrolladores que ya conocen PHP (sintaxis básica, POO, manejo de arrays, etc.) y quieren actualizarse a PHP 8+ para empezar a usar sus features clave. A lo largo del artículo veremos union types, enums, propiedades readonly, match expression, attributes, el operador nullsafe, arrow functions y otras novedades. Cada concepto viene acompañado de un ejemplo mínimo y su salida esperada, para que podés ejecutarlo directamente en tu máquina.
Al finalizar, tendrás un repaso práctico de las herramientas más útiles de PHP 8+, sabrás por qué cada una mejora la robustez del código y tendrás una referencia rápida para aplicarlas en tus propios proyectos.
Union Types (Tipos Unionados)
Antes de PHP 8, si querías aceptar varios tipos en un parámetro, tenías que usar mixed, array o hacer un montón de comprobaciones is_*. Las union types permiten que una variable sea uno de varios tipos a la vez, de forma declarativa.
Cómo funcionan
<?php
/**
* Retorna un string o un int.
* Si el parámetro es negativo, devuelve el absolute value como int.
*/
function getValue(string|int $input): string|int {
if (is_string($input)) {
return "El valor es: $input";
}
// es int
return $input > 0 ? "Positivo: $input" : "Negativo convertido: " . abs($input);
}
// Casos de uso:
foreach (["hola", 42, -7] as $item) {
echo getValue($item) . "
";
}Salida esperada
El valor es: hola
Positivo: 42
Negativo convertido: 7El IDE y las herramientas de static analysis (como Psalm, PhpStorm) ahora conocen cada posibilidad dentro de la función. También pueden advertirte si estás tratando de usar un tipo que no está en la unión.
Mejor práctica
- Usa la sintaxis
Type1|Type2(oType1 | Type2) – es consistente con TypeScript y otros modernos lenguajes. - No abuses de uniones muy amplias (
string|int|float|array|object|bool|null) – prefierenmixedo refinar con un parámetro opcional. - Cuando sea posible, prefiere
Type|nulla?Type(ambos son válidos, pero la barra|se alinea mejor con las union types).
Enums (Enumeraciones)
PHP antes tenía las antiguas define() constantsy una forma limitada deenum` en PHP 8.1. Las enums son tipos de primera clase que proporcionan un conjunto cerrado de valores con comportamiento asociado.
Sintaxis básica
<?php
enum DiaSemana {
case Lunes;
case Martes;
case Miércoles;
case Jueves;
case Viernes;
case Sábado;
case Domingo;
}
// Uso
$hoy = DiaSemana::Lunes;
// DiaSemana::Lunes es una instancia de DiaSemana
match ($hoy) {
DiaSemana::Lunes => echo "Inicio de la semana
",
DiaSemana::Viernes => echo "¡Ya casi es fin de semana!
",
default => echo "Día normal
",
};Salida esperada
Inicio de la semana
¡Ya casi es fin de semana!Enums con métodos y propiedades
PHP 8.1 permite añadir métodos, propiedades constantes e incluso implementaciones completas dentro de una enum.
<?php
enum TamanioPizza: string {
case Pequeña = 'S';
case Mediana = 'M';
case Grande = 'L';
public function tamanioEnCentimetros(): int {
return match ($this) {
self::Pequeña => 25,
self::Mediana => 35,
self::Grande => 45,
};
}
}
// Obtener la descripción del caso actual
enum DescripcionTamanio {
case Pequeña = 'Pequeña';
case Mediana = 'Mediana';
case Grande = 'Grande';
}
$tam = TamanioPizza::Grande;
printf("Pizza %s (%d cm)
", $tam->value, $tam->tamanioEnCentimetros());Salida esperada
Pizza L (45 cm)Por qué son útiles
- Seguridad de tipos:
switch/matchahora sabe que cada branch corresponde a un único caso. - Acceso a métodos: el código puede tener helpers personalizados sin necesidad de una clase auxiliar separada.
- Extensibilidad: podés añadir nuevas funcionalidades (serialización, factories, etc.) directamente sobre la enumeración.
Errores comunes
- Intentar instanciar una enum con
new EnumName()– no está permitido; solo podés referenciar los casos. - Usar un valor que no esté definido – el parser de PHP lo detectará en tiempo de análisis.
- Mezclar un
inty unstringen un mismoenumsin definir un tipo escalar (: stringen este caso) – podrías tener unenum IntWeekdaysi quisieras números.
Propiedades Readonly
Antes de PHP 8, tenías que manualmente clonar un array o usar __set_state para hacer propiedades inmutables. Las propiedades readonly se declaran con readonly y garantizan que el valor no pueda ser cambiado después de la creación del objeto (excepto durante el __construct).
Declaración sencilla
<?php
class Empleado {
public readonly string $nombre;
public readonly int $salario;
public function __construct(string $nombre, int $salario) {
$this->nombre = $nombre;
$this->salario = $salario;
}
}
$juan = new Empleado("Juan Pérez", 75000);
// $juan->nombre = "Otro"; // Esto lanza TypeError: No se puede sobrescribir una propiedad readonly
echo "Empleado: {$juan->nombre}, Salario: {$juan->salario}
";Salida esperada
Empleado: Juan Pérez, Salario: 75000Readonly con tipos de propiedad de clase
PHP 8 soporta propiedades de tipo (public string $nombre), y con readonly podés combinarlas para mayor seguridad.
<?php
class Configuracion {
public readonly string $baseUrl;
public readonly array $opciones;
public function __construct(string $baseUrl, array $opciones) {
$this->baseUrl = $baseUrl;
$this->opciones = $opciones; // array es mutable, pero la referencia se guarda
}
}
$cfg = new Configuracion("https://api.ejemplo.com", ["timeout" => 5000]);
// $cfg->baseUrl = "otro"; // TypeError
var_export($cfg->opciones); // ["timeout" => 500]Salida esperada
["timeout" => 500]Por qué usar readonly
- Seguridad referencial: garantiza que ciertas propiedades nunca cambien después de la creación, lo que ayuda a prevenir bugs sutiles.
- Serialización/deserialización: es más seguro obtener una copia inmutable tras la hidratación.
- Optimización: permite a algunos optimizadores inferir que el valor es constante, mejorando levemente el rendimiento.
Errores a evitar
- Declarar una propiedad
readonlysin asignarla en__construct– PHP lanzará unTypeErroren tiempo de ejecución. - Intentar sobrescribirla desde dentro de la clase (incluso en otro método) – no es permitido.
- Mezclar
readonlyconstatic– son incompatibles.
Match Expression (Expression Switch)
match es un switch que devuelve un valor, no solo ejecuta statements. Fue introducido en PHP 8 como una expression más segura y concisa.
Uso básico
<?php
function notaFinal(float $puntuacion): string {
return match (true) {
$puntuacion >= 90 => "A",
$puntuacion >= 80 => "B",
$puntuacion >= 70 => "C",
$puntuacion >= 60 => "D",
default => "F",
};
}
foreach ([95, 82, 73, 61, 49] as $p) {
printf("Puntuación %d => Nota %s
", $p, notaFinal($p));
}Salida esperada
Puntuación 95 => Nota A
Puntuación 82 => Nota B
Puntuación 73 => Nota C
Puntuación 61 => Nota D
Puntuación 49 => Nota FMatch con enums
Dado que las enums son comparables por identidad, match puede aprovechar eso.
<?php
enum NivelAcceso {
case Admin;
case Editor;
case Lector;
}
function puedeEditar(NivelAcceso $rol): bool {
return match ($rol) {
NivelAcceso::Admin => true,
NivelAcceso::Editor => true,
default => false,
};
}
foreach ([NivelAcceso::Admin, NivelAcceso::Lector] as $r) {
echo $r->name . " puede editar: " . (puedeEditar($r) ? 'sí' : 'no') . "
";
}Salida esperada
Admin puede editar: sí
Lector puede editar: noPor qué match es mejor que switch
- Expression: retorna un valor directamente, no solo ejecuta statements.
- Seguridad de tipos: soporta tipos escalares como
int,string,bool,enum. - Cierre implícito: puedes usar
match ($cond) => $exprsin necesidad de un bloque. - Mayor legibilidad: la sintaxis se alinea con las modernas features de TypeScript y Rust.
Errores típicos
- Usar
matchcon un patrón que no es una expresión válida (por ejemplo,match ($x) { 1 => ... }– solo funciona con expresiones constantes en PHP 8.0; PHP 8.1 permite patrones más avanzados). - Intentar usar
break/continuedentro de match – no son permitidos. - Esperar
nullcomo valor por defecto – necesitasdefault => null.
Attributes (PHP 8.1)
Las attributes son metadatos declarativos que pueden ser procesados por el framework o por herramientas de static analysis. Son similares a las decorators de Python o las annotations de Java.
Declarar y usar una attribute
<?php
#[Attribute]
class ValidarEmail {
public string $mensaje = "El email proporcionado no es válido";
public function __construct(string $mensaje = "") {
if ($mensaje) {
$this->mensaje = $mensaje;
}
}
}
class Usuario {
#[ValidarEmail("El correo debe tener @ y .")]
private string $email;
public function __construct(string $email) {
$this->email = $email;
}
}
$usuario = new Usuario("test@ejemplo.com");
// Aquí podrías usar un validador que inspecciona las attributes
// mediante getAttributes() o ReflectionAttribute.
echo "Usuario creado con email: {$usuario->email}
";Salida esperada
Usuario creado con email: test@ejemplo.comAttributes con múltiples parámetros
<?php
#[Attribute]
class ValidarLongitud {
public int $min;
public int $max;
public string $mensaje = "Longitud inválida";
public function __construct(int $min, int $max, string $mensaje = "") {
$this->min = $min;
$this->max = $max;
if ($mensaje) {
$this->mensaje = $mensaje;
}
}
}
class Password {
#[ValidarLongitud(8, 32, "La contraseña debe tener entre 8 y 32 caracteres")]
private string $valor;
public function __construct(string $valor) {
$this->valor = $valor;
}
}
$pwd = new Password("miContraseñaSegura123");
// Validación podría ocurrir aquí
printf("Contraseña creada (%d caracteres)
", strlen($pwd->valor));Salida esperada
Contraseña creada (22 caracteres)Por qué attributes son útiles
- Metadatos declarativos: el framework sabe qué validaciones, interceptaciones o configuraciones tiene una clase sin necesidad de examinar strings o arrays.
- Soporte del framework: Laravel, Symfony y otros tiene soporte integrado para attributes (por ejemplo,
#[Route]). - Static analysis: herramientas como PHPStan pueden leer attributes para inferir posibles errores.
Errores comunes
- Declarar una attribute sin
#[Attribute]– PHP la tratará como un comentario. - Usar
#[Attribute]en scope de función – permitido, pero asegúrate de que el decorador es compatible con el framework que usas. - Esperar que attributes sean automaticamente invocados – debes manualmente inspeccionarlos con
ReflectionAttribute.
Operador Nullsafe (PHP 7.4)
El operador nullsafe (?->) permite encadenar llamadas sin necesidad de verificar que cada paso intermedio es null. Es especialmente útil para navegar objetos anidados de APIs.
Ejemplo
<?php
class Usuario {
public ?Perfil $perfil = null;
}
class Perfil {
public ?RedesSociales $redes = null;
}
class RedesSociales {
public string $twitter = "@anon";
}
function obtenerTwitter(?Usuario $u): string {
// Sin nullsafe necesitarías checks anidados:
// if ($u && $u->perfil && $u->perfil->redes) {
// return $u->perfil->redes->twitter;
// }
// Con nullsafe:
return $u?->perfil?->redes?->twitter ?? "@desconocido";
}
$usuario1 = null;
$usuario2 = new Usuario();
$usuario2->perfil = new Perfil();
$usuario2->perfil->redes = new RedesSociales();
printf("Usuario 1 Twitter: %s
", obtenerTwitter($usuario1));
printf("Usuario 2 Twitter: %s
", obtenerTwitter($usuario2));Salida esperada
Usuario 1 Twitter: @desconocido
Usuario 2 Twitter: @anonMejores prácticas
- Usa nullsafe en APIs complejas o de tercero donde sea común que algunos niveles sean
null. - No reemplaces la validación de tipo – aún necesitas tipos fuertes.
- Ten cuidado con el rendimiento en loops intensivos – el operador es conveniente pero crea un chain extra.
Errores a evitar
- Intentar usar
?->en un non- objeto (como un array) – lanzará unError. - Pensar que
?->es lo mismo que??–??solo funciona en la expresión inmediata izquierda, mientras?->recorre la cadena de objetos.
Arrow Functions (Functions anónimas de una línea)
Las arrow functions proporcionan una sintaxis más compacta para functions anónimas, especialmente útiles para array_map, usort, etc.
Forma tradicional vs flecha
<?php
$nums = [1, 2, 3, 4];
// Tradicional:
$result1 = array_map(function ($n) {
return $n * 2;
}, $nums);
// Arrow:
$result2 = array_map(fn ($n) => $n * 2, $nums);
var_export($result1);
var_export($result2);Salida esperada
array (4) { [0]=> int (2) [1]=> int (4) [2]=> int (6) [3]=> int (8) }
array (4) { [0]=> int (2) [1]=> int (4) [2]=> int (6) [3]=> int (8) }Usos avanzados
- Captura por referencia (
use &$foo). - Return type declaration (
fn(int $a): int => $a * 2).
<?php
$base = 10;
$multiplicar = fn (int $x) => $x * $base;
// Captura por referencia
$referencia = & $base;
$base = 20;
echo $multiplicar(5) . "
"; // 100 (usa $base original)Salida esperada
100Por qué usar arrow functions
- Menos código: ideal para pequeñas transformaciones.
- Legibilidad: en algunas situaciones,
fnhace que sea obvio que estás mapeando/transformando. - Compatibilidad: funciona en PHP 7.4+ (PHP 8.0 no añade restricciones).
Errores típicos
- Intentar usar
fnen un método que necesiteselfostatic– la closure no tiene acceso a$this. - Sobrecargar demasiadas lógicas simples en una sola línea – puede volver el código difícil de depurar.
Otras Novedades Relevantes
Never Type (never)
never indica una función que nunca retorna (siempre lanza una excepción o termina el script). Útil para assert() o throw.
<?php
function lanzar(): never {
throw new RuntimeException("Esto nunca retorna");
}
try {
lanzar();
} catch (RuntimeException $e) {
echo "Excepción capturada: " . $e->getMessage() . "
";
}Salida esperada
Excepción capturada: Esto nunca retornaMixed Type
mixed es un placeholder para "cualquier tipo". Puede ser útil cuando deseas ser flexible pero aún así permitir type checking.
<?php
function imprimir($valor): void {
echo gettype($valor) . ": " . var_export($valor, true) . "
";
}
imprimir("hola");
imprimir(42);
imprimir([1, 2, 3]);Salida esperada
string: 'hola'
int: 42
array: array (3) { [0]=> int (1) [1]=> int (2) [2]=> int (3) }Tipos de Propiedad de Clase Escalares (int|float|bool|string)
PHP 8.0 introduce string $nombre, int $conteo, etc. Esto permite validares tipos de propiedad en tiempo de análisis.
<?php
class Article {
public string $title;
public int $views = 0;
public function increment(): void {
$this->views++;
}
}
$art = new Article();
$art->title = "PHP 8 es genial";
$art->increment();
printf("%s — %d vistas
", $art->title, $art->views);Salida esperada
PHP 8 es genial — 1 vistasBuenas Prácticas y Errores Comunes
1. Usa Union Types Con Cuidado
- Don't:
array|object|string|null $data - Do:
array|object $datay luego tratanullexplícitamente con?.
2. Aprovecha los Enums para Sets Cerrados
Si sabes que una propiedad solo puede ser un puñado de valores, usa un enum. Esto le da al IDE y a PHP un autocompletado fuerte y previene typos.
3. Marca Propiedades como Readonly Cuando Sea Posible
- Util cuando la lógica del valor se define completamente en
__construct. - Evita setters adicionales – podrías usar un método
withXpara retornar un nuevo objeto.
4. Usa Match en Vez de Switch
Cuando necesites una expresión que retorne un valor, match es más clara.
5. Attributes para Configuración y Validación
- Mantén las attributes separadas del modelo de datos – úsalas en el constructor o en un
#[Inject]. - No copies la lógica de validation manualmente – deja que el framework use reflection.
6. Combina Nullsafe con Tipos Fuertes
?->es conveniente, pero aún necesitas tipos (?Usuario) para una documentación clara.
7. Arrow Functions Para Pequenas Transformaciones
- No uses
fnpara lógica compleja – podrías perder el ability de debuggear. - Usa
usesi necesitas capturar variables externas.
8. Verifica la Versión Mínima Soportada
- Si tu proyecto soporta PHP 7.4+, podés usar nullsafe y arrow functions, pero
matchyenumrequieren PHP 8.0+. - Considera el
declare(strict_types=1);al inicio del archivo – mejora la consistencia del tipado.
Errores Comunes Relacionados con el Tipo en Tiempo de Ejecución
- Olvidar
null:$obj?->propno protege contra$objsiendonull; solo evita errores de propiedad no existente. - sobrescribir readonly: PHP 8 detecta intentos en tiempo de ejecución – asegúrate de no hacerlo accidentalmente.
- Combinação incorrecta de tipos:
$vardeclarado comostring|intpero asignasfloat– PHP permitirá la asignación, pero podrías tener un error oculto.
Conclusión
PHP 8 ha llevado al lenguaje a un estado más moderno y seguro. Las union types eliminan la necesidad de múltiples comprobaciones is_*, enums proveen un sistema de tipos cerrado y rico en comportamiento, propiedades readonly bloquean cambios accidentales, match expression permite writes más seguros y expresivos, attributes introducen un sistema de metadatos declarativo, nullsafe operator simplifica la navegación segura de cadenas nulas, y arrow functions condensan código trivial.
Incorporar estas features no sólo mejora la claridad del código, sino que también alinea PHP con las expectativas contemporáneas de seguridad de tipos y composición concisa. El siguiente paso es empezar a refactorizar proyectos existentes, empezando con areas donde el tipado es débil (por ejemplo, parámetros de API) y añadir gradualmente enums para constantes relacionadas con el dominio.
Si estás empezando un nuevo proyecto o planeas migrar uno antiguo a PHP 8+, recordá fijar declare(strict_types=1); al principio y considera usar herramientas como PHPStan o Psalm junto con attributes para un análisis estático aún más rico.
Ahora que tienes una referencia práctica, eres libre de experimentar con union types, enums, match, y el resto. El lenguaje está aquí para ayudarte a escribir un código más robusto sin sacrificar la flexibilidad que tantos desarrolladores aman en PHP. ¡Feliz coding!
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.