PHP 8.3.21 Released!

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:

  1. 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.
  2. 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.

add a note

User Contributed Notes 1 note

up
0
malferov at gmail dot com
2 months ago
The json_encode() function returns false when trying to serialize an object that contains a property with only a set hook, but without a get hook, because the function seeks to get the property value from the get hook and returns false if no such hook has been defined.

The following example shows how, when serializing an object with the json_encode() function, to get an empty serialized object, or object with only public initialized properties instead of false; the restriction is—the object must remain empty and not set the uninitialized properties to null, if they are not initialized with other values:

<?php

class Lambic
// Implementing an interface to intercept json_encode() serialization
implements JsonSerializable
{
// Note: The property with the set hook only
/** @var int It must be typed if need to omit an uninitialized property in output object */
public int $age {
set => $value;
}

/**
* Implements the interface method
*/
#[Override]
public function
jsonSerialize(): stdClass
{
// Cast the array to an object to get an empty serialized object when object does not contain initialized properties
return (object) get_mangled_object_vars($this);
}

public function
__construct(?int $age = null)
{
if (
$age !== null) {
$this->age = $age;
}
}
}

// Get the empty serialized object without properties
$noagedLambic = new Lambic();
var_dump(json_encode($noagedLambic)); // {} and not {"age":null}

// ...and with properties if initialized
$agedLambic = new Lambic(20);
var_dump(json_encode($agedLambic)); // {"age":20}

?>
To Top