Hooks de propiedad
Los hooks de propiedad, también conocidos como "métodos de acceso de propiedad" en otros lenguajes,
son una forma de interceptar y reemplazar el comportamiento de lectura y escritura de una propiedad.
Esta funcionalidad sirve a dos fines:
-
Permite utilizar propiedades directamente, sin necesidad de métodos get y set,
dejando la posibilidad de añadir un comportamiento adicional en el futuro.
Esto hace que la mayoría de los métodos get/set sean innecesarios,
incluso sin usar hooks.
-
Permite definir propiedades que describen un objeto sin almacenar
directamente un valor.
Existen dos hooks disponibles en las propiedades no estáticas: get
y set
.
Permiten reemplazar el comportamiento de lectura y escritura de una propiedad, respectivamente.
Los hooks están disponibles para propiedades tipadas y no tipadas.
Un objeto puede ser "backed" o "virtual".
Una propiedad "backed" es una propiedad que almacena efectivamente un valor.
Cualquier propiedad que no tenga hooks es "backed".
Una propiedad virtual es una propiedad que tiene hooks y estos hooks no interactúan con la propiedad misma.
En este caso, los hooks son efectivamente los mismos que los métodos,
y el objeto no utiliza espacio para almacenar un valor para esta propiedad.
Los hooks de propiedad son incompatibles con las propiedades readonly
.
Si es necesario restringir el acceso a una operación get
o set
además de modificar su comportamiento, utilice
la visibilidad asimétrica de propiedad.
Nota:
Información de versión
Los hooks de propiedad fueron introducidos en PHP 8.4.
Sintaxis básica de los hooks
La sintaxis general para declarar un hook es la siguiente.
Ejemplo #1 Hooks de propiedad (versión completa)
<?php
class Example
{
private bool $modified = false;
public string $foo = 'valor por omisión' {
get {
if ($this->modified) {
return $this->foo . ' (modificado)';
}
return $this->foo;
}
set(string $value) {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
$example = new Example();
$example->foo = 'cambiado';
print $example->foo;
?>
La propiedad $foo termina con {}
, en lugar de un punto y coma.
Esto indica la presencia de hooks.
Se definen un hook get
y un hook set
,
aunque es posible definir solo uno.
Ambos hooks tienen un cuerpo, indicado por {}
, que puede contener código arbitrario.
El hook set
también permite especificar el tipo y el nombre de un valor entrante,
utilizando la misma sintaxis que una método.
El tipo debe ser el mismo que el tipo de la propiedad,
o contravariante (más amplio) que este.
Por ejemplo, una propiedad de tipo string podría tener
un hook set
que acepte un string|Stringable,
pero no uno que acepte solo array.
Al menos uno de los hooks hace referencia a $this->foo
, la propiedad misma.
Esto significa que la propiedad será "backed".
Cuando se llama a $example->foo = 'cambiado'
,
la cadena proporcionada se convertirá primero en minúsculas y luego se guardará en el valor de respaldo.
Al leer la propiedad, el valor previamente guardado puede ser condicionalmente complementado
con texto adicional.
Hay varias variantes de sintaxis abreviada para manejar los casos comunes.
Si el hook get
es una simple expresión,
entonces los {}
pueden ser omitidos y reemplazados por una expresión flecha.
Ejemplo #2 Expresión de propiedad get
Este ejemplo es equivalente al ejemplo anterior.
<?php
class Example
{
private bool $modified = false;
public string $foo = 'valor por omisión' {
get => $this->foo . ($this->modified ? ' (modificado)' : '');
set(string $value) {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
?>
Si el tipo del parámetro del hook set
es el mismo que el tipo de la propiedad (lo cual es típico),
puede ser omitido. En este caso, el valor a definir se nombra automáticamente $value.
Ejemplo #3 Parámetros por omisión de propiedad
Este ejemplo es equivalente al ejemplo anterior.
<?php
class Example
{
private bool $modified = false;
public string $foo = 'valor por omisión' {
get => $this->foo . ($this->modified ? ' (modificado)' : '');
set {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
?>
Si el hook set
solo define una versión modificada del valor pasado,
también puede simplificarse en una expresión flecha.
El valor al que se evalúa la expresión se definirá en el valor de respaldo.
Ejemplo #4 Expresión de propiedad set
<?php
class Example
{
public string $foo = 'valor por omisión' {
get => $this->foo . ($this->modified ? ' (modificado)' : '');
set => strtolower($value);
}
}
?>
Este ejemplo no es completamente equivalente al anterior,
ya que tampoco modifica $this->modified
.
Si se necesitan múltiples instrucciones en el cuerpo del hook, utilice la versión con llaves.
Una propiedad puede implementar cero, uno o ambos hooks según la situación.
Todas las versiones abreviadas son mutuamente independientes.
Es decir, utilizar un atajo para obtener una definición larga,
o un atajo para definir un tipo explícito, etc., es válido.
En una propiedad "backed", omitir un hook get
o set
significa que se utilizará el comportamiento de lectura o escritura por omisión.
Nota:
Los hooks pueden ser definidos al utilizar la
promoción de propiedades en el constructor.
Sin embargo, en este caso, los valores proporcionados
al constructor deben coincidir con el tipo asociado a la propiedad,
independientemente de lo que el hook set
podría permitir.
Considere el siguiente ejemplo:
<?php
class Example
{
public function __construct(
public private(set) DateTimeInterface $created {
set (string|DateTimeInterface $value) {
if (is_string($value)) {
$value = new DateTimeImmutable($value);
}
$this->created = $value;
}
},
) {
}
}
Internamente, el motor descompone esto de la siguiente manera:
<?php
class Example
{
public private(set) DateTimeInterface $created {
set (string|DateTimeInterface $value) {
if (is_string($value)) {
$value = new DateTimeImmutable($value);
}
$this->created = $value;
}
}
public function __construct(
DateTimeInterface $created,
) {
$this->created = $created;
}
}
Cualquier intento de definir la propiedad fuera del constructor
permitirá un string o un valor de tipo DateTimeInterface,
pero el constructor solo permitirá DateTimeInterface.
Esto se debe a que el tipo definido para la propiedad (DateTimeInterface)
se utiliza como tipo de parámetro en la firma del constructor, independientemente de lo que
el hook set
permita.
Si este tipo de comportamiento es necesario desde el constructor, la promoción
de propiedades en el constructor no puede ser utilizada.
Propiedades virtuales
Las propiedades virtuales son propiedades que no tienen un valor de respaldo.
Una propiedad es virtual si ni su hook get
ni su hook set
hace referencia a la propiedad misma utilizando una sintaxis exacta.
Es decir, una propiedad nombrada $foo
cuyo hook contiene $this->foo
será respaldada.
Pero la siguiente propiedad no es una propiedad respaldada, y generará un error:
Ejemplo #5 Propiedad virtual inválida
<?php
class Example
{
public string $foo {
get {
$temp = __PROPERTY__;
return $this->$temp; // No se refiere a $this->foo, por lo que no cuenta.
}
}
}
?>
Para las propiedades virtuales, si se omite un hook, entonces esa operación
no existe y tratar de utilizarla producirá un error.
Las propiedades virtuales no ocupan espacio de memoria en un objeto.
Las propiedades virtuales son adecuadas para las propiedades "derivadas",
tales como las que son la combinación de dos otras propiedades.
Ejemplo #6 Propiedad virtual
<?php
class Rectangle
{
// Una propiedad virtual.
public int $area {
get => $this->h * $this->w;
}
public function __construct(public int $h, public int $w) {}
}
$s = new Rectangle(4, 5);
print $s->area; // muestra 20
$s->area = 30; // Error, ya que no hay operación de definición.
?>
Definir tanto un hook get
como un hook set
en una propiedad virtual también está permitido.
Alcance
Todos los hooks funcionan en el alcance del objeto modificado.
Esto significa que tienen acceso a todos los métodos públicos, privados o protegidos del objeto,
así como a todas las propiedades públicas, privadas o protegidas,
incluyendo las propiedades que pueden tener sus propios hooks de propiedad.
Acceder a otra propiedad desde un hook no elude los hooks definidos en esa propiedad.
La consecuencia más notable de esto es que los hooks no triviales pueden
llamar a un método arbitrariamente complejo si lo desean.
Ejemplo #7 Llamada a un método desde un hook
<?php
class Person {
public string $phone {
set => $this->sanitizePhone($value);
}
private function sanitizePhone(string $value): string {
$value = ltrim($value, '+');
$value = ltrim($value, '1');
if (!preg_match('/\d\d\d\-\d\d\d\-\d\d\d\d/', $value)) {
throw new \InvalidArgumentException();
}
return $value;
}
}
?>
Referencias
Debido a que la presencia de hooks intercepta el proceso de lectura y escritura de las propiedades,
plantean problemas al adquirir una referencia a una propiedad o con una modificación indirecta,
tal como $this->arrayProp['key'] = 'value';
.
Esto se debe a que cualquier intento de modificar el valor por referencia eludiría un hook de definición
si existiera uno.
En el caso raro en que sea necesario obtener una referencia a una propiedad para la cual se definen hooks,
el hook get
puede ser prefijado por &
para que devuelva por referencia.
Definir tanto get
como &get
en la
misma propiedad es un error de sintaxis.
Definir tanto los hooks &get
como set
en una propiedad "backed" no está permitido.
Como se indicó anteriormente, escribir en el valor devuelto por referencia eludiría el hook set
.
En las propiedades virtuales, no hay un valor común necesario compartido entre los dos hooks, por lo que definir ambos está permitido.
Escribir en un índice de una propiedad de array implica también una referencia implícita.
Por esta razón, escribir en una propiedad de array "backed" con hooks definidos está permitido si y solo si
define solo un hook &get
.
En una propiedad virtual, escribir en el array devuelto por
get
o &get
es legal,
pero si esto tiene un impacto en el objeto depende de la implementación del hook.
Sobrecargar la propiedad de array completa está permitido, y se comporta de la misma manera que cualquier otra propiedad.
Trabajar solo con elementos del array requiere atención especial.
Herencia
Hooks finales
Los hooks también pueden ser declarados final,
en cuyo caso no pueden ser reemplazados.
Ejemplo #8 Hooks finales
<?php
class Utiliser
{
public string $Utilisername {
final set => strtolower($value);
}
}
class Manager extends Utiliser
{
public string $Utilisername {
// Esto está permitido
get => strtoupper($this->Utilisername);
// Esto NO está permitido, ya que set es final en el padre.
set => strtoupper($value);
}
}
?>
Una propiedad también puede ser declarada final.
Una propiedad final no puede ser redeclarada por una clase hija de ninguna manera,
lo que excluye la modificación de los hooks o la ampliación de su acceso.
Declarar hooks finales en una propiedad que es declarada final es redundante,
y será silenciosamente ignorado.
Es el mismo comportamiento que para los métodos finales.
Una clase hija puede declarar o cambiar hooks individuales en una propiedad
redefiniendo la propiedad y solo los hooks que desea reemplazar.
Una clase hija también puede añadir hooks a una propiedad que no los tenía.
Esto es esencialmente lo mismo que si los hooks fueran métodos.
Ejemplo #9 Herencia de hook
<?php
class Point
{
public int $x;
public int $y;
}
class PositivePoint extends Point
{
public int $x {
set {
if ($value < 0) {
throw new \InvalidArgumentException('Too small');
}
$this->x = $value;
}
}
}
?>
Cada hook reemplaza las implementaciones parentales de manera independiente.
Si una clase hija añade hooks, cualquier valor por omisión definido en la propiedad es eliminado, y debe ser redeclarado.
Es la misma coherencia con el funcionamiento de la herencia en las propiedades sin hooks.
Acceso a los hooks parentales
Un hook en una clase hija puede acceder a la propiedad de la clase padre utilizando la palabra clave
parent::$prop
, seguida del hook deseado.
Por ejemplo, parent::$propName::get()
.
Esto puede ser leído como "acceder a la prop definida en la clase padre,
luego ejecutar su operación get" (o set, según el caso).
Si no se accede de esta manera, el hook de la clase padre es ignorado.
Este comportamiento es coherente con el funcionamiento de todos los métodos.
Esto también proporciona una manera de acceder al almacenamiento de la clase padre, si es necesario.
Si no hay un hook en la propiedad padre,
su comportamiento por omisión get/set será utilizado.
Los hooks no pueden acceder a otro hook que no sea su propio padre en su propia propiedad.
El ejemplo anterior podría ser reescrito de la siguiente manera, lo que permitiría a la
clase Point
añadir su propio hook set
en el futuro sin problemas (en el ejemplo anterior, un hook añadido a la
clase padre sería ignorado en la clase hija).
Ejemplo #10 Acceso a los hooks parentales (set)
<?php
class Point
{
public int $x;
public int $y;
}
class PositivePoint extends Point
{
public int $x {
set {
if ($value < 0) {
throw new \InvalidArgumentException('Too small');
}
parent::$x::set($value);
}
}
}
?>
Un ejemplo de reemplazo de solo un hook get podría ser:
Ejemplo #11 Acceso a los hooks parentales (get)
<?php
class Strings
{
public string $val;
}
class CaseFoldingStrings extends Strings
{
public bool $uppercase = true;
public string $val {
get => $this->uppercase
? strtoupper(parent::$val::get())
: strtolower(parent::$val::get());
}
}
?>
Serialización
PHP tiene varias formas diferentes de serializar un objeto,
ya sea para consumo público o para fines de depuración.
El comportamiento de los hooks varía según el uso.
En algunos casos, el valor bruto de respaldo de una propiedad será utilizado,
eludiendo cualquier hook.
En otros, la propiedad será leída o escrita "a través" del hook,
como cualquier otra acción de lectura/escritura normal.