Samuel Arendt

La RFC de readonly corrige una fricción creada por property promotion

Una RFC de PHP en votación aborda la interacción entre propiedades promoted readonly y validación en el constructor. La promoción asigna la propiedad al entrar. Como readonly normalmente impide otra asignación, una clase que necesita normalizar o validar debe abandonar promotion y declarar la propiedad manualmente.

La propuesta de Robert Landers permite exactamente una reasignación de una propiedad promoted readonly dentro del constructor de la clase que la declara, mediante $this. Fuera de esa ventana estrecha, readonly no cambia. Asymmetric visibility sigue funcionando y la implementación reutiliza la flag interna ISPROPREINITABLE.

Es una reparación de ergonomía, no una relajación amplia de la inmutabilidad. DTO y value objects conservan promoción concisa mientras sustituyen el input por su forma validada. La votación estaba abierta hasta el 7 de abril en la fecha del artículo, así que seguía siendo una propuesta.

Una reasignación, solo en la cadena del constructor

La regla propuesta es estrecha. La propiedad tiene que ser readonly y haber sido promovida en el constructor (CPP). La reasignación se permite mientras un constructor de la clase dueña del CPP está en la stack, incluso vía método o closure llamada desde ahí. Una sola vez. Solo en $this. private(set) y protected(set) siguen valiendo dentro de esa cadena.

Llamar __construct() de nuevo en el objeto ya construido no reabre la ventana. Cuando termina la primera construcción, la propiedad ya no está sin inicializar y vuelve el Error de readonly. ReflectionClass::newInstanceWithoutConstructor() seguido del primer __construct() explícito todavía permite la reasignación, porque esa es la primera construcción. Una segunda llamada falla.

El hijo no hereda la ventana del padre. La posesión es de la clase que promovió. Si el hijo redeclarara la misma propiedad sin CPP, el padre todavía puede reasignar una vez. Si el hijo también promueve, el CPP del hijo corre primero y el parent::__construct() que intenta inicializar de nuevo se rompe. Setear en el hijo antes de llamar al padre también se rompe, por el mismo motivo: el slot deja de estar vacío.

Eso cubre el DTO que hace trim, strtolower, abs, o un default con ??. No cubre una inicialización en varios pasos con dos writes. El segundo write sigue siendo Error. ++, --, += y .= cuentan como la única reasignación.

Cuándo la declaración manual sigue siendo más clara

La propia RFC lista casos en los que abandonar promotion sigue siendo la forma más clara. Parámetro nullable que se vuelve propiedad non-nullable. PHPDoc que necesita vivir en la propiedad (non-empty-string, lowercase-string). Inicialización larga, con varias derivaciones a partir del mismo input.

Los property hooks en PHP 8.4 normalizan en cada asignación. Sin un private(set) extra, pierden la garantía write-once de readonly. Semántica distinta, costo distinto. Cambiar readonly por hook solo para conservar el CPP es un cambio de contrato. Útil cuando la normalización debe valer después del constructor. Mal encaje cuando el objeto necesita nacer inmutable.

Si la votación falla, el workaround sigue siendo el de siempre: declarar la propiedad, procesar el parámetro, asignar una vez. Verboso. Legible. Ya funciona en 8.1. Las bibliotecas de dominio que tienen que correr en 8.3 y 8.6 a la vez se quedan en ese diseño aunque la RFC pase, hasta que suba el piso de PHP.

Hay un criterio simple para elegir. Si parámetro y propiedad tienen el mismo tipo y la transformación cabe en una línea, la reasignación propuesta reduce ruido. Si el tipo diverge o la transformación tiene ramas, la declaración tradicional documenta el contrato mejor que un segundo write escondido en el cuerpo.

Analizadores y el hábito del equipo

Las herramientas estáticas y los IDE hoy marcan el segundo write en readonly como error. Si la RFC pasa, necesitan entender la ventana. Hasta entonces, habilitar la sintaxis en un CI con PHP 8.5 rompe el build. Psalm, PHPStan y el parser del editor no votan la RFC. Siguen el runtime que usa la tubería.

El code review necesita una sola pregunta: ¿este write es la normalización de la promoción, o alguien está usando el constructor como setter? La RFC no quiere lo segundo. Un ++ en el contador promovido pasa fácil en una lectura rápida y consume el único permiso.

__clone() ya podía reinicializar readonly vía IS_PROP_REINITABLE. La propuesta no toca eso. La ventana de clone y la de constructor son marcadores distintos a propósito, para que una no se filtre en la otra. Mezclar clone, promotion y reasignación en el mismo objeto pide un test de dominio, no una analogía con "readonly ya era flexible en clone".

Yo pediría un test por clase que use la nueva ventana: construir con input sucio, afirmar el valor normalizado, intentar un segundo write (Error), clonar si el objeto es clonable. Sin eso, el equipo solo descubre el límite cuando un factory llama al constructor dos veces.

Esperar el runtime del equipo, no el texto de la RFC

La votación iba hasta el 7 de abril y pedía dos tercios. El texto aprobado todavía necesita la minor que incorpore el patch. La RFC apunta a PHP 8.6 o posterior. Las bibliotecas de DTO que adopten la sintaxis el día del merge fuerzan el piso de la aplicación. En un vendor compartido entre 8.4 y 8.6, la declaración manual sigue siendo el menor denominador.

Seguir la votación tiene sentido si el equipo ya sufre el roce en value objects y ya tiene un patrón de promotion. Abrir un refactor amplio "porque la RFC existe" gasta el mismo esfuerzo que la declaración manual, con el riesgo extra de que el analizador y el runtime de la tubería rechacen el código.

También está el costo de enseñar la regla. "Readonly no cambia, excepto una vez en el constructor de la clase que promovió, incluso en un método llamado desde ahí, nunca en el hijo que no es dueño del CPP" no cabe en una diapositiva. Cabe en un ejemplo de la guía interna, si la feature existe en el PHP que de verdad corren.

Mientras el voto no se vuelva patch en la imagen de producción, el diseño estable sigue siendo el mismo: promotion cuando el valor llega listo, declaración tradicional cuando el constructor tiene que trabajar.

Si tu empresa está evaluando algo parecido, escríbeme por LinkedIn o por WhatsApp.

Referencias

#Arquitectura, #Calidad de software, #Estrategia, #Radar