PHP 8.4: property hooks, asymmetric visibility y nuevas funciones de array

PHP 8.4, lanzado en noviembre de 2024, es la actualización más sustancial del lenguaje desde PHP 8.0. Trae dos características que la comunidad lleva años pidiendo —property hooks y asymmetric visibility— más cuatro nuevas funciones de array y soporte nativo para objetos perezosos (lazy objects). Todo sin romper compatibilidad hacia atrás en la inmensa mayoría de los casos.

Property hooks: getters y setters en las propiedades

Hasta PHP 8.3, acceder a una propiedad con lógica extra requería hacerla privada y crear métodos getX() y setX(). Con property hooks, la lógica se declara directamente en la propiedad:

<?php
class Temperatura {
    public float $celsius {
        get => $this->celsius;  // hook get
        set(float $valor) {     // hook set con validación
            if ($valor < -273.15) {
                throw new ValueError("Temperatura bajo el cero absoluto");
            }
            $this->celsius = $valor;
        }
    }

    // Propiedad derivada (solo get — no almacena nada)
    public float $fahrenheit {
        get => $this->celsius * 9/5 + 32;
    }

    public function __construct(float $celsius) {
        $this->celsius = $celsius;  // pasa por el hook set
    }
}

$t = new Temperatura(100.0);
echo $t->celsius;     // 100.0
echo $t->fahrenheit;  // 212.0

$t->celsius = -274;   // ValueError: Temperatura bajo el cero absoluto

Hooks más concisos

<?php
class Usuario {
    private string $_nombre = '';

    // Hook set que normaliza el valor
    public string $nombre {
        get => $this->_nombre;
        set => $this->_nombre = trim(ucfirst(strtolower($value)));
    }

    // Hook get que computa el valor sin almacenarlo
    public string $nombreCompleto {
        get => trim($this->nombre . ' ' . $this->apellidos);
    }

    // Solo get: propiedad de solo lectura con lógica
    public string $slug {
        get {
            return strtolower(str_replace(' ', '-', $this->nombre));
        }
    }

    public function __construct(public string $apellidos = '') {}
}

$u = new Usuario('García');
$u->nombre = '  ana  ';
echo $u->nombre;        // Ana
echo $u->nombreCompleto; // Ana García
echo $u->slug;          // ana

Hooks en interfaces y clases abstractas

<?php
interface Serializable {
    public string $json { get; }  // obliga a implementar el get hook
}

abstract class Modelo {
    abstract public int $id { get; set; }
}

class Producto extends Modelo implements Serializable {
    public int $id {
        get => $this->_id;
        set => $this->_id = max(1, $value);
    }
    private int $_id = 0;

    public string $json {
        get => json_encode(['id' => $this->id, 'nombre' => $this->nombre]);
    }

    public function __construct(public string $nombre) {}
}

Asymmetric visibility: leer público, escribir privado

Un patrón muy habitual: la propiedad puede leerse desde fuera pero solo modificarse desde dentro de la clase (o subclases). Hasta PHP 8.3 requería una propiedad privada con getter público; PHP 8.4 lo resuelve con public private(set):

<?php
class Pedido {
    // Se puede leer desde cualquier lado, solo modificar desde dentro de la clase
    public private(set) string $estado = 'pendiente';
    public private(set) float  $total  = 0.0;
    public protected(set) DateTimeImmutable $creadoEn;

    public function __construct() {
        $this->creadoEn = new DateTimeImmutable();
    }

    public function confirmar(): void {
        $this->estado = 'confirmado';  // OK — desde dentro
    }

    public function añadirLinea(float $precio): void {
        $this->total += $precio;
    }
}

$pedido = new Pedido();
echo $pedido->estado;    // pendiente — lectura OK desde fuera
$pedido->confirmar();
echo $pedido->estado;    // confirmado

$pedido->estado = 'cancelado';  // Error: Cannot modify private(set) property

Combinando con readonly y hooks

<?php
class Articulo {
    // readonly + hook get: la propiedad no se puede modificar tras la construcción,
    // pero se puede añadir lógica al leerla
    public readonly string $slug {
        get => strtolower(str_replace(' ', '-', $this->titulo));
    }

    // public protected(set): subclases pueden modificar, clientes no
    public protected(set) int $vistas = 0;

    public function __construct(public readonly string $titulo) {}
}

class ArticuloDestacado extends Articulo {
    public function registrarVisita(): void {
        $this->vistas++;  // OK — desde subclase
    }
}

Nuevas funciones de array en PHP 8.4

<?php
$numeros = [3, 1, 7, 2, 9, 4, 6];

// array_find(): devuelve el primer elemento que cumple el predicado
$primeroMayorCinco = array_find($numeros, fn($n) => $n > 5);
echo $primeroMayorCinco;  // 7

// array_find_key(): devuelve la clave del primer elemento que cumple el predicado
$clave = array_find_key($numeros, fn($n) => $n > 5);
echo $clave;  // 2 (índice)

// array_any(): true si algún elemento cumple el predicado
$hayPares = array_any($numeros, fn($n) => $n % 2 === 0);
var_dump($hayPares);  // bool(true)

// array_all(): true si TODOS los elementos cumplen el predicado
$todosMayoresQueCero = array_all($numeros, fn($n) => $n > 0);
var_dump($todosMayoresQueCero);  // bool(true)

// Ejemplo real: validar que un array de emails son válidos
$emails = ['[email protected]', '[email protected]', '[email protected]'];
$todosValidos = array_all($emails, fn($e) => filter_var($e, FILTER_VALIDATE_EMAIL) !== false);
var_dump($todosValidos);  // bool(true)

$hayInvalidos = array_any($emails, fn($e) => !filter_var($e, FILTER_VALIDATE_EMAIL));

Lazy objects: objetos perezosos

Los objetos perezosos difieren su inicialización hasta el momento en que realmente se usan. Útil para contenedores de inyección de dependencias, ORMs y cualquier objeto cuya creación es costosa pero que puede no llegar a usarse en la petición:

<?php
class ConexionDB {
    private PDO $pdo;

    public function __construct(private string $dsn, private string $user, private string $pass) {
        echo "Conectando a la base de datos...n";  // costoso
        $this->pdo = new PDO($dsn, $user, $pass);
    }

    public function query(string $sql): array {
        return $this->pdo->query($sql)->fetchAll();
    }
}

// Sin lazy object: la conexión se crea aunque no la uses
$db = new ConexionDB('mysql:host=localhost;dbname=app', 'user', 'pass');
// "Conectando a la base de datos..." — ya!

// Con lazy object: la conexión NO se crea hasta que se usa
$reflector = new ReflectionClass(ConexionDB::class);
$db = $reflector->newLazyGhost(function(ConexionDB $obj) {
    // Este inicializador solo se llama cuando se accede al objeto
    $obj->__construct('mysql:host=localhost;dbname=app', 'user', 'pass');
});

echo "Objeto creado, aún no conectadon";  // sin "Conectando..."

$resultado = $db->query('SELECT 1');  // aquí sí se llama al inicializador
// "Conectando a la base de datos..." — solo ahora

// Verificar si ya está inicializado
var_dump($reflector->isUninitializedLazyObject($db));  // false (ya inicializado)

Otras mejoras de PHP 8.4

<?php
// Llamadas encadenadas sin paréntesis extra
// PHP < 8.4:
$valor = (new MiClase())->metodo();
// PHP 8.4:
$valor = new MiClase()->metodo();  // sin paréntesis extra

// Constantes de clase tipadas
class Config {
    const float PI      = 3.14159;
    const int   MAX_INT = PHP_INT_MAX;
    const string NOMBRE = 'Mi App';
}

// array_repeat() — repetir un array N veces (mencionado en el artículo de arrays)
$semana = ['L', 'M', 'X', 'J', 'V'];
$mes = array_repeat($semana, 4);  // 20 elementos

// BCMath con operadores nativos (PHP 8.4 con extensión bcmath)
// $resultado = $a + $b funciona con objetos BcMathNumber

Compatibilidad y migración

PHP 8.4 es mayoritariamente compatible hacia atrás. Los puntos a revisar al migrar desde PHP 8.3:

  • Deprecaciones: implicitly nullable en parámetros (function f(Type $x = null) debe ser ?Type $x = null). Se emitía un deprecation notice en 8.1/8.2 y en 8.4 es error.
  • E_STRICT eliminado: la constante E_STRICT ya no existe. Si la usas en error_reporting, actualiza.
  • Funciones DOM: algunas funciones DOM tienen nuevas variantes que devuelven objetos en lugar de listas. Los cambios son opt-in.
  • Property hooks y readonly: son incompatibles entre sí en propiedades con almacenamiento. Readonly implica que solo se puede escribir una vez (en el constructor), mientras que un hook set permite escrituras posteriores con validación.

PHP 8.4 consolida la tendencia del lenguaje hacia más expresividad y menos código boilerplate. Los property hooks eliminan uno de los patrones más repetitivos del PHP orientado a objetos, la asymmetric visibility resuelve el dilema de encapsulamiento vs. acceso directo a propiedades, y las nuevas funciones de array siguen rellenando los huecos que obligaban a usar array_filter + reset o count(array_filter(...)) para tareas comunes.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP