PHP 8+ Moderno: Features Clave que Redefinen el Desarrollo (Union Types, Enums, Readonly y Más) PHP

PHP 8+ Moderno: Features Clave que Redefinen el Desarrollo (Union Types, Enums, Readonly y Más)

DUGLAS MORENODUGLAS MORENO 👁 1

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: 7

El 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 (o Type1 | Type2) – es consistente con TypeScript y otros modernos lenguajes.
  • No abuses de uniones muy amplias (string|int|float|array|object|bool|null) – prefieren mixed o refinar con un parámetro opcional.
  • Cuando sea posible, prefiere Type|null a ?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/match ahora 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 int y un string en un mismo enum sin definir un tipo escalar (: string en este caso) – podrías tener un enum IntWeekday si 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: 75000

Readonly 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 readonly sin asignarla en __construct – PHP lanzará un TypeError en tiempo de ejecución.
  • Intentar sobrescribirla desde dentro de la clase (incluso en otro método) – no es permitido.
  • Mezclar readonly con static – 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 F

Match 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: no

Por 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) => $expr sin necesidad de un bloque.
  • Mayor legibilidad: la sintaxis se alinea con las modernas features de TypeScript y Rust.

Errores típicos

  • Usar match con 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/continue dentro de match – no son permitidos.
  • Esperar null como valor por defecto – necesitas default => 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.com

Attributes 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: @anon

Mejores 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á un Error.
  • 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

100

Por qué usar arrow functions

  • Menos código: ideal para pequeñas transformaciones.
  • Legibilidad: en algunas situaciones, fn hace 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 fn en un método que necesite self o static – 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 retorna

Mixed 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 vistas

Buenas Prácticas y Errores Comunes

1. Usa Union Types Con Cuidado

  • Don't: array|object|string|null $data
  • Do: array|object $data y luego trata null explí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 withX para 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 fn para lógica compleja – podrías perder el ability de debuggear.
  • Usa use si 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 match y enum requieren 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?->prop no protege contra $obj siendo null; 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: $var declarado como string|int pero asignas float – 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!


Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario