PHP ha evolucionado hacia un modelo de objetos mas cercano al que se encuentra en Java, C# o Kotlin. Las novedades que llegaron con PHP 8.1, PHP 8.2 y PHP 8.4 permiten expresar invariantes del dominio directamente en la declaracion de la clase, eliminando comprobaciones manuales en setters y metodos. Esta leccion cubre las cuatro piezas que vertebran ese cambio.
Propiedades readonly
Una propiedad readonly, disponible desde PHP 8.1, solo puede asignarse una unica vez dentro del alcance declarativo de la clase. Tras la primera escritura, cualquier intento de modificarla lanza Error.
<?php
final class Usuario
{
public function __construct(
public readonly int $id,
public readonly string $email,
) {}
}
$u = new Usuario(1, "alicia@example.com");
echo $u->email;
// $u->email = "otro@example.com"; // Error: Cannot modify readonly property
El modificador readonly se combina a la perfeccion con la promocion de propiedades en el constructor. El parametro tipado del constructor se convierte directamente en una propiedad de la clase, sin necesidad de declarar y asignar por separado.
Las propiedades readonly se inicializan una sola vez y no tienen que ser obligatoriamente en el constructor. Cualquier metodo de la misma clase puede asignarlas siempre que aun no esten inicializadas.
<?php
final class ResultadoPago
{
public readonly string $referencia;
public function marcarReferencia(string $ref): void
{
$this->referencia = $ref;
}
}
Una propiedad readonly no es una constante. Su valor se fija en tiempo de ejecucion durante la construccion, no en tiempo de compilacion, lo que permite usar valores dinamicos sin perder la garantia de inmutabilidad posterior.
Clases readonly
PHP 8.2 introduce la posibilidad de marcar una clase entera como readonly. Todas sus propiedades heredan automaticamente la restriccion, y la clase no puede declarar propiedades no tipadas ni dinamicas.
<?php
readonly class Coordenada
{
public function __construct(
public float $latitud,
public float $longitud,
) {}
public function distanciaA(self $otro): float
{
return sqrt(
($this->latitud - $otro->latitud) ** 2 +
($this->longitud - $otro->longitud) ** 2
);
}
}
Esta sintaxis es ideal para value objects, esos objetos que solo existen por los datos que contienen y que el dominio compara por valor. Al declarar la clase entera como readonly, cualquier intento de modificar un campo queda detectado en analisis estatico y en tiempo de ejecucion.
Para crear una version modificada de un value object se usa el operador clone with, disponible a partir de PHP 8.3, que devuelve una nueva instancia con las propiedades indicadas actualizadas.
<?php
$origen = new Coordenada(40.4168, -3.7038);
$destino = clone($origen) with { latitud: 41.3851, longitud: 2.1734 };
Este patron respeta la inmutabilidad, devolviendo siempre un objeto nuevo en lugar de mutar el original.
Visibilidad asimetrica
Hasta PHP 8.3, la visibilidad de una propiedad debia ser la misma para lectura y escritura. La visibilidad asimetrica de PHP 8.4 permite declarar un nivel mas restrictivo para la escritura que para la lectura, mediante la sintaxis public private(set).
<?php
final class Cuenta
{
public function __construct(
public private(set) float $saldo = 0.0,
) {}
public function ingresar(float $importe): void
{
if ($importe <= 0) {
throw new InvalidArgumentException("Importe debe ser positivo");
}
$this->saldo += $importe;
}
}
$c = new Cuenta();
$c->ingresar(100);
echo $c->saldo; // 100.0 (lectura publica)
// $c->saldo = 999; // Error: solo la clase puede escribir
La combinacion public private(set) evita el ritual de declarar una propiedad privada y exponerla con un getter explicito, manteniendo al mismo tiempo el control total sobre las mutaciones. Esto reduce boilerplate sin ceder encapsulacion.
Tambien existe la variante public protected(set), que permite a las subclases modificar la propiedad sin exponerla publicamente.
La visibilidad asimetrica tiene su reflejo natural en otros lenguajes: en C# es el patron
public int Saldo { get; private set; }, y en Kotlin equivale avar saldo: Double private set.
Property hooks
Los property hooks son quizas la novedad mas transformadora de PHP 8.4. Permiten interceptar la lectura y escritura de una propiedad directamente en su declaracion, sin declarar metodos getX() y setX() separados. El resultado es un codigo mas cercano al dominio y una API limpia para los consumidores.
<?php
final class Persona
{
public string $nombre = "";
public string $apellidos = "";
public string $nombreCompleto {
get => trim("{$this->nombre} {$this->apellidos}");
}
}
$p = new Persona();
$p->nombre = "Ana";
$p->apellidos = "Gomez";
echo $p->nombreCompleto; // "Ana Gomez"
El hook get convierte $nombreCompleto en una propiedad derivada. No hay almacenamiento para ella, se calcula bajo demanda cada vez que se lee, igual que una propiedad computada en otros lenguajes.
Los hooks tambien admiten interceptores de escritura con validacion incorporada. La variable $value representa el valor entrante y set => permite transformarlo antes de guardar.
<?php
final class Producto
{
public string $slug {
set (string $value) {
$normalizado = strtolower(trim($value));
if ($normalizado === "") {
throw new InvalidArgumentException("Slug vacio");
}
$this->slug = str_replace(" ", "-", $normalizado);
}
}
}
$p = new Producto();
$p->slug = " Mi Producto ";
echo $p->slug; // "mi-producto"
Los hooks se pueden combinar con readonly, con visibilidad asimetrica y con tipos. Esto permite modelar con precision cuando una propiedad admite escritura externa, cuando se calcula, y cuando su valor se almacena tras transformarlo.
El diagrama resume los elementos de modelado de objetos introducidos en las ultimas versiones.
flowchart LR
A[Objeto PHP 8.4] --> B[Constructor property promotion]
A --> C[readonly]
A --> D[Asymmetric visibility]
A --> E[Property hooks]
C --> C1[Propiedad readonly]
C --> C2[Clase readonly]
D --> D1[public private set]
D --> D2[public protected set]
E --> E1[get]
E --> E2[set]
Patron practico: DTO inmutable
La combinacion de todas estas herramientas ofrece un estilo compacto para definir Data Transfer Objects que viajan entre capas. El objeto solo se construye una vez, sus datos no se modifican, y las propiedades derivadas se calculan al vuelo.
<?php
readonly class UsuarioDTO
{
public function __construct(
public int $id,
public string $email,
public string $nombre,
public string $apellidos,
) {}
public string $iniciales {
get => strtoupper($this->nombre[0] . $this->apellidos[0]);
}
}
$dto = new UsuarioDTO(1, "alicia@example.com", "Alicia", "Martinez");
echo $dto->iniciales; // "AM"
Este enfoque sustituye a los antiguos arrays asociativos y a las clases POPO repletas de getters escritos a mano. El resultado es menos codigo, mas seguridad de tipos y una intencion mas clara para quien lo lee.
Cuando preferir mutabilidad controlada
No todo objeto debe ser inmutable. Las entidades del dominio que cambian de estado a lo largo de su ciclo de vida se benefician de la visibilidad asimetrica combinada con metodos que encapsulan las transiciones validas. El readonly total conviene para los value objects, los DTO y los parametros de configuracion que se construyen una vez al arrancar el proceso.
Una regla util: si dos objetos con los mismos valores son intercambiables para el negocio, son value objects y merecen ser readonly. Si tienen identidad propia y un ciclo de vida, son entidades y normalmente exponen metodos mutadores controlados.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, PHP es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de PHP
Explora más contenido relacionado con PHP y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Aprender a usar propiedades readonly, clases readonly de PHP 8.2, la visibilidad asimetrica y los property hooks de PHP 8.4 para expresar invariantes en tiempo de compilacion.