Les hooks de propriété, également connus sous le nom d'"accesseurs de propriété" dans d'autres langages, sont une façon d'intercepter et de remplacer le comportement de lecture et d'écriture d'une propriété. Cette fonctionnalité sert à deux fins :
Il existe deux hooks disponibles sur les propriétés non-statiques : get
et set
.
Ils permettent de remplacer le comportement de lecture et d'écriture d'une propriété, respectivement.
Les hooks sont disponibles pour les propriétés typées et non typées.
Un objet peut être "backed" ou "virtuel". Une propriété "backed" est une propriété qui stocke effectivement une valeur. Toute propriété qui n'a pas de hooks est "backed". Une propriété virtuelle est une propriété qui a des hooks et ces hooks n'interagissent pas avec la propriété elle-même. Dans ce cas, les hooks sont effectivement les mêmes que les méthodes, et l'objet n'utilise pas d'espace pour stocker une valeur pour cette propriété.
Les hooks de propriété sont incompatibles avec les propriétés readonly
.
S'il est nécessaire de restreindre l'accès à une opération get
ou set
en plus de modifier son comportement, utilisez
la visibilité asymétrique de propriété.
La syntaxe générale pour déclarer un hook est la suivante.
Exemple #1 Hooks de propriété (version complète)
<?php
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get {
if ($this->modified) {
return $this->foo . ' (modified)';
}
return $this->foo;
}
set(string $value) {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
$example = new Example();
$example->foo = 'changed';
print $example->foo;
?>
La propriété $foo se termine par {}
, plutôt qu'un point-virgule.
Cela indique la présence de hooks.
Un hook get
et un hook set
sont définis,
bien qu'il soit possible de n'en définir qu'un seul.
Les deux hooks ont un corps, indiqué par {}
, qui peut contenir du code arbitraire.
Le hook set
permet également de spécifier le type et le nom d'une valeur entrante,
en utilisant la même syntaxe qu'une méthode.
Le type doit être soit le même que le type de la propriété,
ou contravariant (plus large) que celui-ci.
Par exemple, une propriété de type string pourrait avoir
un hook set
qui accepte un string|Stringable,
mais pas un qui n'accepte que array.
Au moins un des hooks fait référence à $this->foo
, la propriété elle-même.
Cela signifie que la propriété sera "backed".
Lorsque vous appelez $example->foo = 'changed'
,
la chaîne fournie sera d'abord convertie en minuscules, puis enregistrée dans la valeur de sauvegarde.
Lors de la lecture de la propriété, la valeur précédemment enregistrée peut conditionnellement être complétée
avec du texte supplémentaire.
Il y a plusieurs variantes de syntaxe abrégée pour gérer les cas courants.
Si le hook get
est une simple expression,
alors les {}
peuvent être omis et remplacés par une expression fléchée.
Exemple #2 Expression de propriété get
Cet exemple est équivalent à l'exemple précédent.
<?php
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get => $this->foo . ($this->modified ? ' (modified)' : '');
set(string $value) {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
?>
Si le type du paramètre du hook set
est le même que le type de la propriété (ce qui est typique),
il peut être omis. Dans ce cas, la valeur à définir est automatiquement nommée $value.
Exemple #3 Paramètres par défaut de propriété
Cet exemple est équivalent à l'exemple précédent.
<?php
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get => $this->foo . ($this->modified ? ' (modified)' : '');
set {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
?>
Si le hook set
ne fait que définir une version modifiée de la valeur passée,
il peut également être simplifié en une expression fléchée.
La valeur à laquelle l'expression est évaluée sera définie sur la valeur de sauvegarde.
Exemple #4 Expression de propriété set
<?php
class Example
{
public string $foo = 'default value' {
get => $this->foo . ($this->modified ? ' (modified)' : '');
set => strtolower($value);
}
}
?>
Cet exemple n'est pas tout à fait équivalent au précédent,
car il ne modifie pas non plus $this->modified
.
Si plusieurs instructions sont nécessaires dans le corps du hook, utiliser la version avec des accolades.
Une propriété peut implémenter zéro, un ou les deux hooks selon la situation. Toutes les versions abrégées sont mutuellement indépendantes. C'est-à-dire qu'utiliser un raccourci pour obtenir une longue définition, ou un raccourci pour définir un type explicite, etc., est valide.
Sur une propriété "backed", l'omission d'un hook get
ou set
signifie que le comportement de lecture ou d'écriture par défaut sera utilisé.
Note: Les hooks peuvent être définis lors de l'utilisation de la promotion de propriétés dans le constructeur. Cependant, dans ce cas, les valeurs fournies au constructeur doivent correspondre au type associé à la propriété, indépendamment de ce que le hook
set
pourrait autoriser. Considérez l'exemple suivant :En interne, le moteur décompose cela de la manière suivante :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;
}
},
) {
}
}Toute tentative de définir la propriété en dehors du constructeur autorisera soit une string soit une valeur de type DateTimeInterface, mais le constructeur n'autorisera que DateTimeInterface. Cela s'explique par le fait que le type défini pour la propriété (DateTimeInterface) est utilisé comme type de paramètre dans la signature du constructeur, indépendamment de ce que le hookclass 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;
}
}set
permet. Si ce type de comportement est nécessaire depuis le constructeur, la promotion de propriétés dans le constructeur ne peut pas être utilisée.
Les propriétés virtuelles sont des propriétés qui n'ont pas de valeur de sauvegarde.
Une propriété est virtuelle si ni son hook get
ni son hook set
ne fait référence à la propriété elle-même en utilisant une syntaxe exacte.
C'est-à-dire qu'une propriété nommée $foo
dont le hook contient $this->foo
sera sauvegardée.
Mais la propriété suivante n'est pas une propriété sauvegardée, et générera une erreur :
Exemple #5 Propriété virtuelle invalide
<?php
class Example
{
public string $foo {
get {
$temp = __PROPERTY__;
return $this->$temp; // Doesn't refer to $this->foo, so it doesn't count.
}
}
}
?>
Pour les propriétés virtuelles, si un hook est omis, alors cette opération n'existe pas et essayer de l'utiliser produira une erreur. Les propriétés virtuelles n'occupent pas d'espace mémoire dans un objet. Les propriétés virtuelles sont adaptées pour les propriétés "dérivées", telles que celles qui sont la combinaison de deux autres propriétés.
Exemple #6 Propriété virtuelle
<?php
readonly class Rectangle
{
// Une propriété virtuelle.
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; // affiche 20
$s->area = 30; // Erreur, car il n'y a pas d'opération de définition.
?>
Définir à la fois un hook get
et un hook set
sur une propriété virtuelle est également autorisé.
Tous les hooks fonctionnent dans la portée de l'objet modifié. Cela signifie qu'ils ont accès à toutes les méthodes publiques, privées ou protégées de l'objet, ainsi qu'à toutes les propriétés publiques, privées ou protégées, y compris les propriétés qui peuvent avoir leurs propres hooks de propriété. Accéder à une autre propriété depuis un hook ne contourne pas les hooks définis sur cette propriété.
La conséquence la plus notable de cela est que les hooks non triviaux peuvent appeler une méthode arbitrairement complexe s'ils le souhaitent.
Exemple #7 Appel d'une méthode depuis 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;
}
}
?>
Parce que la présence de hooks intercepte le processus de lecture et d'écriture des propriétés,
ils posent des problèmes lors de l'acquisition d'une référence à une propriété ou avec une modification indirecte,
telle que $this->arrayProp['key'] = 'value';
.
C'est parce que toute tentative de modification de la valeur par référence contournerait un hook de définition
s'il en existe un.
Dans le cas rare où il est nécessaire d'obtenir une référence à une propriété pour laquelle des hooks sont définis,
le hook get
peut être préfixé par &
pour qu'il retourne par référence.
Définir à la fois get
et &get
sur la
même propriété est une erreur de syntaxe.
Définir à la fois les hooks &get
et set
sur une propriété "backed" n'est pas autorisé.
Comme indiqué ci-dessus, écrire dans la valeur retournée par référence contournerait le hook set
.
Sur les propriétés virtuelles, il n'y a pas de valeur commune nécessaire partagée entre les deux hooks, donc définir les deux est autorisé.
Ecrire dans un index d'une propriété de tableau implique également une référence implicite.
Pour cette raison, écrire dans une propriété de tableau "backed" avec des hooks définis est autorisé si et seulement si
il ne définit qu'un hook &get
.
Sur une propriété virtuelle, écrire dans le tableau retourné par
get
ou &get
est légal,
mais si cela a un impact sur l'objet dépend de l'implémentation du hook.
Surcharger l'intégralité de la propriété de tableau est autorisé, et se comporte de la même manière que toute autre propriété. Ne travailler qu'avec des éléments du tableau nécessite une attention particulière.
Les hooks peuvent également être déclarés final, auquel cas ils ne peuvent pas être remplacés.
Exemple #8 Hook finals
<?php
class Utiliser
{
public string $Utilisername {
final set => strtolower($value);
}
}
class Manager extends Utiliser
{
public string $Utilisername {
// Ceci est autorisé
get => strtoupper($this->Utilisername);
// Ceci n'est PAS autorisé, car set est final dans le parent.
set => strtoupper($value);
}
}
?>
Une propriété peut également être déclarée final. Une propriété finale ne peut pas être redéclarée par une classe enfant de quelque manière que ce soit, ce qui exclut la modification des hooks ou l'élargissement de son accès.
Déclarer des hooks finaux sur une propriété qui est déclarée finale est redondant, et sera silencieUtilisement ignoré. C'est le même comportement que pour les méthodes finales.
Une classe enfant peut déclarer ou changer des hooks individuels sur une propriété en redéfinissant la propriété et uniquement les hooks qu'elle souhaite remplacer. Une classe enfant peut également ajouter des hooks à une propriété qui n'en avait pas. C'est essentiellement la même chose que si les hooks étaient des méthodes.
Exemple #9 Héritage 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;
}
}
}
?>
Chaque hook remplace les implémentations parentes indépendamment les unes des autres. Si une classe enfant ajoute des hooks, toute valeur par défaut définie sur la propriété est supprimée, et doit être redéclarée. C'est la même cohérence avec le fonctionnement de l'héritage sur les propriétés sans hooks.
Un hook dans une classe enfant peut accéder à la propriété de la classe parente en utilisant le mot-clé
parent::$prop
, suivi du hook désiré.
Par exemple, parent::$propName::get()
.
Cela peut être lu comme "accéder à la prop définie sur la classe parente,
puis exécuter son opération get" (ou set, selon le cas).
Si ce n'est pas accédé de cette manière, le hook de la classe parente est ignoré. Ce comportement est cohérent avec le fonctionnement de toutes les méthodes. Cela offre également un moyen d'accéder au stockage de la classe parente, le cas échéant. S'il n'y a pas de hook sur la propriété parente, son comportement par défaut get/set sera utilisé. Les hooks ne peuvent pas accéder à un autre hook que leur propre parent sur leur propre propriété.
L'exemple ci-dessus pourrait être réécrit de manière plus efficace comme suit.
Exemple #10 Accès aux hooks parentaux (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');
}
$this->x = $value;
}
}
}
?>
Un exemple de remplacement uniquement d'un hook get pourrait être :
Exemple #11 Accès aux hooks parentaux (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());
}
}
?>
PHP a plusieurs façons différentes de sérialiser un objet, que ce soit pour la consommation publique ou à des fins de débogage. Le comportement des hooks varie en fonction de l'utilisation. Dans certains cas, la valeur de sauvegarde brute d'une propriété sera utilisée, contournant tout hook. Dans d'autres, la propriété sera lue ou écrite "à travers" le hook, comme toute autre action de lecture/écriture normale.