PHP moderno cambia por completo la forma en que se programaba con PHP viejo. Si dejaste de ver código PHP hace una década, probablemente todavía lo asocias con mysql_query(), arrays sin tipar y un montón de if anidados para manejar errores — esa imagen ya no corresponde a la realidad.
Hoy, PHP moderno es más rápido, más seguro y más eficiente que el PHP de hace 10 años: tiene tipado real, manejo de errores basado en excepciones, y una sintaxis que compite de tú a tú con cualquier lenguaje popular en 2026.

En esta súper guía comparamos, característica por característica, cómo se hacía antes en PHP viejo y cómo se hace ahora en PHP moderno — con 30 ejemplos de código, listos para descargar y ejecutar.
Te invito a mi Guia de Desarrollo de Sistemas con PHP y MySQL
PHP no está muerto
Entre 2014 y 2018 circuló mucho la idea de que PHP era un lenguaje en decadencia: sin tipado, inseguro por defecto, y superado por Node.js y Python. Parte de esa crítica tenía sentido en su momento.
Lo que cambió no fue la percepción, sino el lenguaje mismo: desde PHP 7.0 (2015) hasta las versiones actuales, cada release agregó características que lo acercan a lenguajes tipados modernos, sin perder la simplicidad que lo hizo popular.
Características principales de PHP moderno
- Compilador JIT
- Promoción de propiedades en el constructor
- Tipos de unión (Union Types)
- Atributos (Attributes)
- Match Expression
- Operador Nullsafe
- Enumeraciones (Enums)
- Tipos de retorno estricto y
never - Fibers (fibras)
- Property Hooks
- Propiedades readonly
- Argumentos con nombre (Named Arguments)
- Funciones flecha (Arrow Functions)
- Operador de fusión de null (
??y??=) - Funciones
str_contains,str_starts_with,str_ends_with - Errores de tipo como excepciones, no como warnings silenciosos
newen inicializadores de parámetrosarray_is_list()json_validate()- Operador spread en arrays
- First-class callable syntax
- Enums con interfaces y métodos propios
- Clases readonly completas
- Tipos de intersección
array_find,array_any,array_all- Interfaz
Stringable - Atributo
#[\Override] - Nueva API de números aleatorios (
Random\Engine) WeakMap- Constantes en traits
A continuación, cada una explicada con código real: cómo se resolvía antes, y cómo se resuelve hoy.
1. Compilador JIT (Just-In-Time)
Desde PHP 8.0 existe un compilador JIT que traduce partes del código a instrucciones de máquina en tiempo de ejecución. En aplicaciones web típicas (donde el tiempo se va en base de datos y red) la ganancia es modesta, pero en tareas de cómputo intensivo — procesamiento de imágenes, cálculos matemáticos, algoritmos — el JIT ofrece mejoras de rendimiento que antes de PHP 8 simplemente no existían.
Antes: no había forma de acelerar código PHP puro sin extensiones externas como HHVM. Ahora: el rendimiento mejora activando una opción en php.ini, sin cambiar una sola línea de código.
2. Promoción de propiedades en el constructor
Antes:
class Producto
{
private $nombre;
private $precio;
public function __construct($nombre, $precio)
{
$this->nombre = $nombre;
$this->precio = $precio;
}
}
Ahora:
class Producto
{
public function __construct(
private string $nombre,
private float $precio,
) {}
}
Lo que antes tomaba 8 líneas repetitivas ahora se resuelve en la firma del constructor. Sin embargo, como $nombre y $precio quedaron private, para consultarlas desde afuera hace falta agregar un método, por ejemplo:
class Producto
{
public function __construct(
public readonly string $nombre,
public readonly float $precio,
) {}
}
$producto = new Producto('Teclado', 899.99);
echo "{$producto->nombre}: \${$producto->precio}\n";
3. Tipos de unión (Union Types)
Antes: un parámetro solo podía tener un tipo, o ninguno. Aceptar “int o string” obligaba a quitar el tipado por completo y validar manualmente dentro de la función.
function procesarId($id)
{
if (!is_int($id) && !is_string($id)) {
throw new InvalidArgumentException('Tipo no válido');
}
// ...
}
Ahora:
function procesarId(int|string $id): void
{
// ...
}
procesarId(42); // válido
procesarId('abc-123'); // también válido
PHP valida el tipo automáticamente antes de ejecutar la función, sin código adicional.
4. Atributos (Attributes)
Antes: los metadatos sobre una clase o método se escribían como comentarios especiales (docblocks), que no eran código real y solo herramientas externas podían leer:
/**
* @Route("/productos", methods={"GET"})
*/
public function listar() { /* ... */ }
Ahora:
#[Route('/productos', methods: ['GET'])]
public function listar() { /* ... */ }
Los atributos son código real, verificado por el propio PHP, no texto suelto dentro de un comentario que cualquier error tipográfico podía romper en silencio. Y a diferencia del docblock, se pueden leer en tiempo de ejecución con Reflection:
$reflexion = new ReflectionMethod(ProductoController::class, 'listar');
$atributos = $reflexion->getAttributes(Route::class);
foreach ($atributos as $atributo) {
$instancia = $atributo->newInstance();
echo "Ruta: {$instancia->path}\n";
}
5. Match Expression
Antes:
if ($rol == 'admin') {
$nivel = 1;
} elseif ($rol == 'subadmin') {
$nivel = 2;
} else {
$nivel = 4;
}
Ahora:
$rol = 'subadmin';
$nivel = match ($rol) {
'admin' => 1,
'subadmin' => 2,
default => 4,
};
echo "Nivel de acceso: {$nivel}\n";
match usa comparación estricta (===) por defecto, evitando los errores clásicos de comparación floja de PHP, y no necesita break.
6. Operador Nullsafe
Antes:
class Direccion { public $ciudad; }
class Usuario { public $direccion; }
$usuario = new Usuario();
// $usuario->direccion es null en este caso
$ciudad = null;
if ($usuario !== null && $usuario->direccion !== null) {
$ciudad = $usuario->direccion->ciudad;
}
echo $ciudad ?? 'Sin ciudad';
echo "\n";
Ahora:
class Direccion { public $ciudad; }
class Usuario { public $direccion; }
$usuario = new Usuario();
$ciudad = $usuario?->direccion?->ciudad;
echo $ciudad ?? 'Sin ciudad';
echo "\n";
Antes, recorrer una cadena de propiedades opcionalmente nulas requería un if por cada nivel. Ahora se resuelve en una sola línea.
7. Enumeraciones (Enums)
Antes: los “enums” se simulaban con constantes de clase, sin ninguna garantía real de que un valor perteneciera al conjunto permitido:
class Estado
{
const PENDIENTE = 'pendiente';
const PAGADO = 'pagado';
}
function marcarComo(string $estado) { /* cualquier string pasa aquí */ }
Ahora:
enum Estado: string
{
case Pendiente = 'pendiente';
case Pagado = 'pagado';
}
function marcarComo(Estado $estado): string
{
return "Pedido marcado como: {$estado->value}";
}
echo marcarComo(Estado::Pagado) . "\n";
Esto es algo que antes no se podía hacer: PHP ahora rechaza en tiempo de ejecución cualquier valor que no sea un caso real del enum.
8. Tipos de retorno estricto y never
Antes: una función que siempre lanzaba una excepción (y nunca retornaba) no tenía forma de declararlo; PHP y los IDEs no sabían que el código después de la llamada era inalcanzable.
Ahora:
function redirigirOModal(): never
{
header('Location: /login');
exit;
}
function validarEdad(int $edad): string
{
if ($edad < 0) {
throw new RuntimeException('La edad no puede ser negativa');
}
return "Edad válida: {$edad}";
}
echo validarEdad(25) . "\n";
El tipo never le indica a PHP —y a herramientas de análisis estático— que la función jamás retorna, algo que antes de PHP 8.1 no existía como concepto del lenguaje.
9. Fibers (fibras)
Antes: manejar código asíncrono o concurrente en PHP puro, sin extensiones externas, no era posible. Cualquier operación bloqueaba el hilo completo hasta terminar.
Ahora:
$fiber = new Fiber(function (): void {
$valor = Fiber::suspend('pausado');
echo "Recibido: $valor";
});
$fiber->start();
$fiber->resume('continuar');
Las fibras permiten pausar y reanudar la ejecución de una función manualmente, la base sobre la que hoy se construyen librerías de concurrencia en PHP puro.
10. Property Hooks
Antes: controlar qué pasa al leer o escribir una propiedad obligaba a esconderla y crear un getter y un setter manuales:
class Producto
{
private float $precio;
public function getPrecio(): float
{
return $this->precio;
}
public function setPrecio(float $precio): void
{
$this->precio = max(0, $precio);
}
}
Ahora (PHP 8.4):
class Producto
{
public float $precio {
set => max(0, $value);
}
}
$producto = new Producto();
$producto->precio = -50.0;
echo $producto->precio . "\n"; // 0, no -50
La propiedad sigue comportándose como una propiedad pública normal ($producto->precio = -10), pero PHP ejecuta la lógica del hook automáticamente — algo que antes solo se lograba con getters y setters explícitos.
11. Propiedades readonly
Antes: no existía forma nativa de declarar que una propiedad, una vez asignada, no podía volver a modificarse. Se dependía de la disciplina del equipo o de comentarios.
Ahora:
class Producto
{
public readonly string $nombre;
public function __construct(string $nombre)
{
$this->nombre = $nombre;
}
}
$p = new Producto('Teclado');
$p->nombre = 'Mouse'; // Error: no se puede modificar una propiedad readonly
12. Argumentos con nombre (Named Arguments)
Antes: al llamar una función con muchos parámetros opcionales, había que respetar el orden exacto o pasar todos los anteriores aunque no se necesitaran:
function crearUsuario($nombre, $email, $activo = true, $rol = 'empleado')
{
return "{$nombre} ({$email}) — rol: {$rol}";
}
echo crearUsuario('Ana', 'ana@demo.com', true, 'admin');
echo "\n";
Ahora:
function crearUsuario($nombre, $email, $activo = true, $rol = 'empleado')
{
return "{$nombre} ({$email}) — rol: {$rol}";
}
echo crearUsuario(nombre: 'Ana', email: 'ana@demo.com', rol: 'admin');
echo "\n";
Los parámetros se pasan por nombre, en cualquier orden, saltando los que tienen valor por defecto.
13. Funciones flecha (Arrow Functions)
Antes:
$productos = [
(object) ['nombre' => 'Teclado', 'precio' => 899.99],
(object) ['nombre' => 'Mouse', 'precio' => 349.50],
];
$precios = array_map(function ($producto) {
return $producto->precio;
}, $productos);
print_r($precios);
Ahora:
$precios = array_map(fn($producto) => $producto->precio, $productos); print_r($precios);
Además de ser más corta, la función flecha captura automáticamente las variables del entorno exterior, sin necesidad de un use (...) explícito.
14. Operador de fusión de null (?? y ??=)
Antes:
$datos = ['email' => 'ana@demo.com'];
$config = [];
$nombre = isset($datos['nombre']) ? $datos['nombre'] : 'Invitado';
if (!isset($config['tema'])) {
$config['tema'] = 'claro';
}
echo "{$nombre} / {$config['tema']}\n";
Ahora:
$datos = ['email' => 'ana@demo.com'];
$config = [];
$nombre = $datos['nombre'] ?? 'Invitado';
$config['tema'] ??= 'claro';
echo "{$nombre} / {$config['tema']}\n";
15. str_contains, str_starts_with, str_ends_with
Antes: buscar una subcadena obligaba a usar strpos() y recordar que devuelve 0 (no false) cuando la coincidencia está al inicio, una fuente clásica de bugs:
$texto = 'Error de conexión a la base de datos';
if (strpos($texto, 'conexión') !== false) {
echo "El texto contiene 'conexión'\n";
}Ahora:
$texto = 'Error de conexión a la base de datos'; var_dump(str_contains($texto, 'conexión')); // true var_dump(str_starts_with($texto, 'Error')); // true var_dump(str_ends_with($texto, 'datos')); // true
Simple, legible, y sin la trampa del !== false.
16. Errores de tipo como excepciones, no como warnings silenciosos
Antes: pasar un tipo incorrecto a muchas funciones internas de PHP generaba un warning que, si no se revisaban los logs, pasaba completamente desapercibido y la ejecución continuaba con datos corruptos.
Ahora: desde PHP 8.0, la mayoría de esos errores lanzan un TypeError real, que detiene la ejecución igual que cualquier otra excepción:
strlen([1, 2, 3]); // Antes: warning silencioso. Ahora: TypeError
Esto es algo que antes no se podía “atrapar” con un try/catch porque ni siquiera era una excepción — ahora sí.
17. new en inicializadores de parámetros
Antes: no era posible usar new directamente como valor por defecto de un parámetro; había que hacer el objeto nulo y crearlo dentro del cuerpo de la función.
class Logger
{
public function __construct(?Writer $writer = null)
{
$this->writer = $writer ?? new ConsoleWriter();
}
}
Ahora (PHP 8.1):
class Logger
{
public function __construct(
private Writer $writer = new ConsoleWriter(),
) {}
public function log(string $mensaje): void
{
$this->writer->escribir($mensaje);
}
}
$logger = new Logger(); // usa ConsoleWriter por defecto, sin pasar nada
$logger->log('Sistema iniciado');18. array_is_list()
Antes: verificar si un array tenía llaves secuenciales desde el 0 (es decir, si era una “lista” y no un array asociativo) requería una función manual o una comparación con array_values():
function esLista(array $arr): bool
{
return $arr === array_values($arr);
}
Ahora:
$lista = ['rojo', 'verde', 'azul']; $asociativo = ['color' => 'rojo', 'talla' => 'M']; var_dump(array_is_list($lista)); // true var_dump(array_is_list($asociativo)); // false
19. json_validate()
Antes: para saber si un string era JSON válido, la única forma era decodificarlo por completo y revisar si falló, gastando memoria en construir una estructura que ni siquiera se iba a usar:
$texto = '{"nombre": "Ana", "activo": true}';
json_decode($texto);
$esValido = json_last_error() === JSON_ERROR_NONE;
var_dump($esValido); // true
Ahora (PHP 8.3):
$json = '{"nombre": "Ana", "activo": true}';
$esValido = json_validate($json);
var_dump($esValido); // true
Valida la sintaxis sin construir el árbol de datos completo en memoria, algo que antes de PHP 8.3 no existía como función nativa.
20. Operador spread en arrays
Antes: combinar dos arrays requería array_merge():
$base = ['a', 'b']; $extra = ['c', 'd']; $todos = array_merge($base, $extra);
Ahora:
$todos = [...$base, ...$extra]; print_r($todos);
El operador spread (...) también funciona para expandir argumentos al llamar funciones, evitando call_user_func_array().
21. First-class callable syntax
Antes:
$fn = 'strlen';
$len = $fn('hola');
$callback = Closure::fromCallable('strtoupper');Ahora:
$fn = strlen(...);
$len = $fn('hola');
echo "Longitud: {$len}\n";
$callback = strtoupper(...);
echo $callback('php moderno') . "\n";La sintaxis nombre(...) convierte cualquier función o método en un closure real, sin strings mágicos ni Closure::fromCallable().
22. Enums con interfaces y métodos propios
Antes: los “enums” simulados con constantes no podían implementar una interfaz ni tener lógica propia; la lógica vivía afuera, en un switch separado:
class Estado
{
const PENDIENTE = 'pendiente';
const PAGADO = 'pagado';
public static function label($valor)
{
switch ($valor) {
case self::PENDIENTE: return 'Pendiente de pago';
case self::PAGADO: return 'Pagado';
}
}
}Ahora:
interface TieneColor
{
public function color(): string;
}
enum Estado: string implements TieneColor
{
case Pendiente = 'pendiente';
case Pagado = 'pagado';
public function label(): string
{
return match ($this) {
self::Pendiente => 'Pendiente de pago',
self::Pagado => 'Pagado',
};
}
public function color(): string
{
return match ($this) {
self::Pendiente => 'amarillo',
self::Pagado => 'verde',
};
}
}
echo Estado::Pagado->label(); // "Pagado"Un enum moderno puede implementar interfaces y tener métodos propios — algo que antes simplemente no existía como concepto del lenguaje.
23. Clases readonly completas
Antes: había que repetir la palabra readonly en cada propiedad, una por una:
class Punto
{
public readonly float $x;
public readonly float $y;
public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $y;
}
}Ahora (PHP 8.2):
readonly class Punto
{
public function __construct(
public float $x,
public float $y,
) {}
}
$punto = new Punto(10.5, 20.3);
echo "({$punto->x}, {$punto->y})\n";Al marcar la clase completa como readonly, todas sus propiedades quedan protegidas automáticamente, sin repetir la palabra clave en cada una.
24. Tipos de intersección
Antes: no existía forma de exigir que un valor cumpliera dos interfaces al mismo tiempo; solo quedaba documentarlo o validarlo a mano:
function procesar($coleccion)
{
if (!($coleccion instanceof Countable) || !($coleccion instanceof Iterator)) {
throw new InvalidArgumentException();
}
}
Ahora:
class ColeccionProductos implements Countable, Iterator
{
private int $posicion = 0;
public function __construct(private array $items = []) {}
public function count(): int { return count($this->items); }
public function current(): mixed { return $this->items[$this->posicion]; }
public function key(): mixed { return $this->posicion; }
public function next(): void { $this->posicion++; }
public function rewind(): void { $this->posicion = 0; }
public function valid(): bool { return isset($this->items[$this->posicion]); }
}
function procesar(Countable&Iterator $coleccion): void
{
echo "Elementos: {$coleccion->count()}\n";
foreach ($coleccion as $item) {
echo "- {$item}\n";
}
}
procesar(new ColeccionProductos(['Teclado', 'Mouse', 'Monitor']));
25. array_find, array_any, array_all
Antes: buscar o validar elementos de un array requería escribir el foreach manualmente cada vez:
$usuarios = [
['nombre' => 'Ana', 'rol' => 'empleado'],
['nombre' => 'Luis', 'rol' => 'admin'],
];
$hayAdmin = false;
foreach ($usuarios as $usuario) {
if ($usuario['rol'] === 'admin') {
$hayAdmin = true;
break;
}
}
echo "¿Hay admin? " . ($hayAdmin ? 'sí' : 'no') . "\n";
Ahora (PHP 8.4):
$usuarios = [
['nombre' => 'Ana', 'rol' => 'empleado'],
['nombre' => 'Luis', 'rol' => 'admin'],
];
$hayAdmin = array_any($usuarios, fn($u) => $u['rol'] === 'admin');
$primerAdmin = array_find($usuarios, fn($u) => $u['rol'] === 'admin');
$todosActivos = array_all($usuarios, fn($u) => $u['rol'] !== null);
echo "¿Hay admin? " . ($hayAdmin ? 'sí' : 'no') . "\n";
echo "Primer admin: {$primerAdmin['nombre']}\n";26. Interfaz Stringable
Antes: aceptar tanto un string como un objeto con __toString() en un mismo parámetro no tenía forma limpia de tiparse.
function imprimir($valor)
{
echo $valor; // podía ser string u objeto, sin ninguna garantía
}Ahora:
function imprimir(string|Stringable $valor): void
{
echo $valor;
}
class Dinero
{
public function __construct(private float $monto) {}
public function __toString(): string
{
return number_format($this->monto, 2);
}
}
imprimir(new Dinero(99.5)); // funcionaCualquier clase con __toString() implementa Stringable automáticamente, sin declararlo de forma explícita.
27. Atributo #[\Override]
Antes: si renombrabas un método en la clase padre y olvidabas actualizar el hijo, el “override” dejaba de funcionar en silencio, sin ningún aviso:
class Base
{
public function procesar() { /* ... */ }
}
class Hijo extends Base
{
public function procesa() { /* typo: esto no sobrescribe nada */ }
}
Ahora (PHP 8.3):
class Hijo extends Base
{
#[\Override]
public function procesa() { /* Error: procesa() no existe en Base */ }
}Si en cambio el nombre del método sí coincide con el del padre, todo funciona con normalidad:
class ReporteBase
{
public function generar(): string
{
return 'Reporte base generado';
}
}
class ReporteVentas extends ReporteBase
{
#[\Override]
public function generar(): string
{
return 'Reporte de ventas generado';
}
}
$reporte = new ReporteVentas();
echo $reporte->generar() . "\n";PHP lanza un error en tiempo de compilación si el método marcado con #[\Override] no existe realmente en la clase padre — un typo que antes pasaba completamente desapercibido.
28. Nueva API de números aleatorios
Antes: mt_rand() dependía de un estado global, lo que hacía casi imposible aislar o repetir una secuencia aleatoria en pruebas unitarias:
mt_srand(1234); $dado = mt_rand(1, 6);
Ahora (PHP 8.2):
$random = new \Random\Randomizer(new \Random\Engine\Mt19937(1234));
$dado = $random->getInt(1, 6);
echo "Resultado del dado: {$dado}\n";
Cada instancia de Randomizer tiene su propio estado interno, permitiendo generadores independientes y fácilmente testeables.
29. WeakMap
Antes: asociar datos extra a un objeto (por ejemplo, para cachear un cálculo) usando un array normal mantenía una referencia fuerte a ese objeto, impidiendo que el recolector de basura lo liberara aunque ya no se usara en ningún otro lugar:
class Producto { public function __construct(public string $nombre) {} }
$objeto = new Producto('Teclado');
$cache = [];
$cache[spl_object_id($objeto)] = 'datos calculados';
// $objeto nunca se libera de memoria mientras $cache exista
Ahora:
class Producto { public function __construct(public string $nombre) {} }
$objeto = new Producto('Teclado');
$cache = new WeakMap();
$cache[$objeto] = 'datos calculados';
echo $cache[$objeto] . "\n";
// Si $objeto deja de usarse en cualquier otro lugar,
// PHP lo libera de memoria y la entrada desaparece del WeakMap automáticamente
30. Constantes en traits
Antes: los traits solo podían compartir propiedades y métodos; una constante debía repetirse manualmente en cada clase que la necesitara.
trait ConLimite
{
// No era posible declarar una constante aquí
}
class Producto
{
const LIMITE_STOCK = 100; // repetido en cada clase que lo necesite
}
Ahora (PHP 8.2):
trait ConLimite
{
const LIMITE_STOCK = 100;
}
class Producto
{
use ConLimite;
}
echo Producto::LIMITE_STOCK; // 100
El ecosistema también maduró
No solo cambió el lenguaje. El ecosistema alrededor de PHP dio un salto igual de grande:
- Composer es hoy el estándar para gestionar dependencias, reemplazando la práctica de copiar librerías manualmente a una carpeta
/libs. - PSR (PHP Standards Recommendations) estandarizó cosas tan básicas como el autoloading (PSR-4) o el estilo de código (PSR-12).
- PHPUnit y testing automatizado pasaron de ser una rareza a una práctica esperada en cualquier proyecto serio.
Descargar los ejemplos
Los 30 ejemplos de esta guía están disponibles como archivos .php independientes y listos para ejecutar, uno por cada característica:
⬇ Descargar ejemplos de código (.zip)
La mayoría funciona desde PHP 8.0. Algunos ejemplos (Property Hooks,
array_find,#[\Override],json_validate) requieren PHP 8.3 o 8.4 — el detalle de requisitos está incluido en elREADME.mddel zip.
Cómo ejecutar los ejemplos
Opción 1 — Desde la terminal:
cd super-guia-php-moderno php 07-enums.php
Cambia 07-enums.php por el archivo que quieras probar. Verás el resultado directo en la terminal.
Opción 2 — Desde el navegador:
Debes copiar la carpeta de ejemplos a tu carpeta c:\xampp\htdocs
Abre en tu navegador la siguiente URL donde super-guia-php-moderno es la carpeta donde estan los ejemplos:
http://localhost/super-guia-php-moderno/07-enums.php
Requiere PHP 8.0 o superior. Algunos ejemplos (Property Hooks,
array_find,#[\Override],json_validate) necesitan PHP 8.3 o 8.4 — ver requisitos en elREADME.mddel zip.
Conclusión
El PHP de hoy tiene tipado opcional pero real, manejo de errores basado en excepciones, sintaxis moderna para casos comunes (match, ?->, readonly, enum), funciones nativas que antes no existían (json_validate, array_is_list, array_find, str_contains), mejor rendimiento con JIT, herramientas de seguridad en tiempo de compilación (#[\Override]), gestión de memoria más inteligente (WeakMap), y hasta las bases para concurrencia con Fibers. La imagen de “lenguaje inseguro y desordenado” describe código mal escrito de hace 10 años, no al lenguaje en sí.
Si quieres profundizar en varias de las funciones mencionadas aquí, tenemos guías dedicadas por ejemplo RBAC plano en PHP y MySQL, donde se usa buena parte de esta sintaxis moderna en código real y funcional.