## Hooks de propriété

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 :

1. Elle permet d'utiliser directement des propriétés, sans avoir besoin de méthodes get et set, tout en laissant la possibilité d'ajouter un comportement supplémentaire à l'avenir. Cela rend la plupart des méthodes get/set inutiles, même sans utiliser de hooks.
2. Elle permet de définir des propriétés qui décrivent un objet sans stocker directement de valeur.

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.

Une propriété peut être "backed" ou "virtuelle". 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, il convient d'utiliser la visibilité asymétrique de propriété.

> Note: Information de version
> Les hooks de propriété ont été introduits en PHP 8.4.

### Syntaxe de base des hooks

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". Lors de l'appel `$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
    {
        private bool $modified = false;

        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 :

>     <?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;
>                 }
>             },
>         ) {
>         }
>     }

> En interne, le moteur décompose cela de la manière suivante :

>     <?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;
>         }
>     }

> 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 hook `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.

### Propriétés virtuelles

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; // Ne fait pas référence à $this->foo, donc cela ne compte pas.
            }
        }
    }
    ?>

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
    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é.

### Portée

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;
        }
    }
    ?>

### Références

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é.

Écrire 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.

### Héritage

#### Hooks finaux

Les hooks peuvent également être déclarés final, auquel cas ils ne peuvent pas être remplacés.

Exemple #8 Hooks finaux

    <?php
    class Utilisateur
    {
        public string $identifiant {
            final set => strtolower($value);
        }
    }

    class Responsable extends Utilisateur
    {
        public string $identifiant {
            // Ceci est autorisé
            get => strtoupper($this->identifiant);

            // 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 silencieusement ignoré. C'est le même comportement que pour les méthodes finales.

Une classe enfant peut définir ou redéfinir 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. Cela est cohérent avec le fonctionnement de l'héritage sur les propriétés sans hooks.

#### Accès aux hooks parentaux

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 propriété 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 comme suit, ce qui permettrait à la classe `Point` d'ajouter son propre hook `set` à l'avenir sans problèmes (dans l'exemple précédent, un hook ajouté à la classe parente serait ignoré dans la classe enfant).

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');
                }
                parent::$x::set($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());
        }
    }
    ?>

### Sérialisation

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.

- var_dump(): Utilise la valeur brute
- serialize(): Utilise la valeur brute
- unserialize(): Utilise la valeur brute
- __serialize()/__unserialize(): Logique propre, utilise les hooks get/set
- Conversion en tableau : Utilise la valeur brute
- var_export(): Utilise le hook get
- json_encode(): Utilise le hook get
- JsonSerializable: Logique propre, utilise le hook get
- get_object_vars(): Utilise le hook get
- get_mangled_object_vars(): Utilise la valeur brute

`↑` and `↓` to navigate • `Enter` to select • `Esc` to close • `/` to open