🔎 Buscar

🐘 PHP 8 moderno

Match, named arguments, attributes, enums, readonly, property promotion, nullsafe, tipos avanzados y Composer con PSR-4 explicados con código real.

Wiki / Apuntes📖 Contenido

PHP 8 moderno

PHP 8 convirtió un lenguaje históricamente criticado en una herramienta moderna de backend: enums de primera clase, attributes (anotaciones), types estrictos y una JIT. Si aún escribes PHP como en la era de PHP 5, te estás perdiendo el 80 % de las mejoras que esta versión aporta. Este artículo recorre cada feature con código real.

match vs switch

match es la evolución de switch: devuelve un valor, usa comparación estricta (===) y es exhaustivo (si no hay caso que coincida, lanza UnhandledMatchError).

function estadoTexto(int $estado): string
{
    return match ($estado) {
        0 => 'pendiente',
        1 => 'procesando',
        2 => 'completado',
        default => 'desconocido',
    };
}

echo estadoTexto(2); // completado
// switch: sin valor de retorno, comparación suelta (==), fácil de olvidar un break
switch ($estado) {
    case 0:
        $r = 'pendiente';
        break;
    case 1:
        $r = 'procesando';
        break;
}
switch match
Devuelve valor ❌ (necesitas asignar) ✅ expresión
Comparación suelta (==) estricta (===)
break obligatorio no aplica
Exhaustivo no sí (sin default → excepción)
Múltiples condiciones case 1: case 2: 1, 2 => ...

💡 match también admite condiciones combinadas: match ($x) { 'a', 'b' => 'algo', default => 'otro' }.

Named arguments

Puedes pasar argumentos por nombre, en el orden que quieras y omitir los opcionales del medio:

function crearUsuario(
    string $nombre,
    string $email,
    bool $activo = true,
    array $roles = []
): void {
    // ...
}

// Sin named args: tienes que recordar el orden y rellenar opcionales
crearUsuario('Ana', 'ana@x.com', true, ['admin']);

// Con named args: claro y con saltos
crearUsuario('Ana', 'ana@x.com', roles: ['admin']);

⚠️ Aplica el principio de último recurso: named args mejoran la legibilidad, pero sobreusarlos acopla tu código a los nombres de parámetros de terceros (si renombran uno, rompes). Úsalos para parámetros opcionales o booleanos confusos.

Attributes: los “decoradores” de PHP

Los attributes (PHP 8.0+) metadatos declarativos que se leen con reflexión. Son la base de cómo Laravel y Symfony configuran rutas, validación y más.

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Ruta
{
    public function __construct(public string $metodo, public string $path) {}
}

#[Ruta('GET', '/usuarios')]
class ListarUsuarios
{
    public function ejecutar(): array { return []; }
}

// Leerlos con reflexión
$ref = new ReflectionClass(ListarUsuarios::class);
$attr = $ref->getAttributes(Ruta::class)[0]->newInstance();
echo $attr->metodo . ' ' . $attr->path; // GET /usuarios

Enums: pure y backed

Los enums (PHP 8.1) son de primera clase: con métodos, constantes y casos:

// Pure enum: sin valor asociado
enum Estado
{
    case Pendiente;
    case Procesando;
    case Completado;

    public function etiqueta(): string
    {
        return match ($this) {
            self::Pendiente  => 'En espera',
            self::Procesando => 'En proceso',
            self::Completado => 'Terminado',
        };
    }
}

// Backed enum: asociado a un valor (int o string)
enum Rol: string
{
    case Admin  = 'admin';
    case Editor = 'editor';
    case Lector = 'lector';

    public function esPrivilegiado(): bool
    {
        return $this === self::Admin;
    }
}

echo Rol::Admin->value;                // 'admin'
echo Rol::from('admin')->name;         // 'Admin'
$rol = Rol::tryFrom('inexistente');    // null (no lanza)

💡 Un enum implementa una interface, puede tener métodos estáticos, y los casos se comparan con ===. Cuando necesites una “lista de constantes con comportamiento”, es un enum.

readonly properties y readonly classes

PHP 8.1 introdujo propiedades readonly; PHP 8.2 añadió clases readonly (todas sus propiedades son readonly). Solo se escriben una vez, y solo desde dentro del constructor:

readonly class Configuracion
{
    public function __construct(
        public string $host,
        public int $puerto,
        public bool $debug = false,
    ) {}
}

$cfg = new Configuracion('localhost', 3306);
// $cfg->host = 'otro';  // ❌ Error: no se puede modificar readonly

Constructor property promotion

Promueve parámetros del constructor a propiedades sin declararlas aparte:

// Antes (PHP 7)
class Usuario {
    private string $nombre;
    private int $edad;
    public function __construct(string $nombre, int $edad) {
        $this->nombre = $nombre;
        $this->edad = $edad;
    }
}

// Con promotion (PHP 8)
class Usuario {
    public function __construct(
        private string $nombre,
        private int $edad,
    ) {}
}

💡 El promotion es azúcar sintáctico: la propiedad se crea, se declara su visibilidad y se asigna en el constructor, todo en una línea. Combinado con readonly y named args elimina mucho boilerplate.

Nullsafe operator

Encadena llamadas que pueden devolver null sin lanzar errores en el camino:

// PHP 7: feo y propenso a errores
$ciudad = null;
if ($usuario !== null) {
    $perfil = $usuario->getPerfil();
    if ($perfil !== null) {
        $ciudad = $perfil->getDireccion()->ciudad ?? null;
    }
}

// PHP 8: nullsafe
$ciudad = $usuario?->getPerfil()?->getDireccion()?->ciudad;

⚠️ Nullsafe corta la cadena devolviendo null en el primer null, pero no valida el tipo final: sigue necesitando ?? si el último eslabón puede ser null.

Union e intersection types

PHP 8.0 añadió unions (int|string); PHP 8.1 añadió intersections (A&B):

// Union: el valor puede ser cualquiera de los listados
function normalizar(int|string $valor): int
{
    return is_string($valor) ? (int) $valor : $valor;
}

// Intersection: el valor DEBE cumplir todos los tipos
interface Guardable {}
interface Serializable {}

function persistir(Guardable&Serializable $entidad): void
{
    // $entidad implementa ambas interfaces
}

⚠️ No puedes mezclar A&B y A|B en un mismo parámetro sin paréntesis; PHP lo rechaza con error de compilación.

mixed y never

  • mixed (8.0): “cualquier tipo” — la alternativa segura a no tipar (equivale a array|bool|callable|int|float|null|object|resource|string).
  • never (8.1): la función no devuelve nada porque siempre lanza o termina el proceso.
function debug(mixed $dato): void { var_dump($dato); }

function abortar(string $msg): never
{
    throw new RuntimeException($msg);
}

First-class callable

Puedes pasar una función como objeto callable sin closures ni strings:

$fn = strlen(...);                 // first-class callable (8.1)
echo $fn('hola');                  // 4

array_map(strtoupper(...), ['a', 'b']); // ['A', 'B']

str_contains, str_starts_with, str_ends_with

PHP 8 quitó por fin la necesidad de strpos(...) !== false:

$email = 'ana@example.com';

str_contains($email, '@');     // true
str_starts_with($email, 'ana'); // true
str_ends_with($email, '.com');  // true
// El clásico de PHP 5 que ya no necesitas
if (strpos($email, '@') !== false) { /* ... */ }

JIT y Opcache

  • Opcache: cachea el bytecode compilado (los .php ya compilados) en memoria. Está activo en producción desde PHP 5.5.
  • JIT (PHP 8.0): compila los hot paths de bytecode a código máquina en runtime. En la práctica beneficia CPU-bound; para backend típico (I/O), Opcache es lo que importa.
; php.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000

; JIT (solo PHP 8+)
opcache.jit=auto
opcache.jit_buffer_size=64M

💡 En un API/CRUD típico el JIT no cambia casi nada (el tiempo se va en I/O). Mide con php -d opcache.jit=on script.php contra =off antes de tocar nada.

Composer y autoload PSR-4

Composer es el gestor de dependencias. El autoload PSR-4 mapea namespaces a directorios sin escanear archivos:

{
  "name": "acme/app",
  "require": {
    "php": "^8.2",
    "guzzlehttp/guzzle": "^7.0"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}
src/
├── Usuario.php        → namespace App;  class Usuario
└── Dominio/
    └── Repositorio.php → namespace App\Dominio;  class Repositorio
composer install          # instala dependencias
composer dump-autoload    # regenera el autoloader (al añadir clases)
composer require laravel/framework
composer update guzzlehttp/guzzle

💡 En PSR-4 el namespace App\Dominio\Repositorio se resuelve a src/Dominio/Repositorio.php automáticamente. Cada clase en su propio archivo, con el namespace que coincida con la ruta.

Cheatsheet

Feature Versión Una línea
match, named args, attributes 8.0 expresión match que devuelve valor
Union types, mixed, ?-> 8.0 int|string, nullsafe
Enums, readonly props, intersections 8.1 enum Rol: string
Readonly classes 8.2 readonly class Config
First-class callable 8.1 strlen(...)
str_contains/starts/ends_with 8.0 adiós a strpos() !== false
JIT 8.0 bytecode → código máquina
Composer + PSR-4 siempre autoload por namespace

Para profundizar

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.