# PHP 8.4 : property hooks et leur impact sur les mocks PHPUnit

> PHP 8.4 introduit les property hooks. Sur un projet WordPress migré tôt, plusieurs doublures de test qui interceptaient des accesseurs ont cessé de fonctionner comme prévu.

- Auteur : Clément Hadrot
- Publié le : 2024-12-07
- Mis à jour le : 2024-12-07
- Catégorie : Tests
- URL : https://wpmoderne.dev.wordpress-developpement.fr/tests/php-8-4-property-hooks-mocks-phpunit/

## L’essentiel

- Les hooks get/set remplacent des accesseurs manuels testés par mock
- Un mock sur une propriété hookée ne se comporte plus comme avant
- Prévoir une phase de revue ciblée plutôt qu'une réécriture globale

`public string $status { get => $this->normalizeStatus(); set => $this->status = strtolower($value); }` — cette syntaxe, nouvelle en PHP 8.4, a suffi à faire échouer onze tests sur une extension de gestion de stock que nous maintenons depuis 2021. Rien n'avait changé dans la logique métier. Le coupable : une propriété que nous avions migrée vers un property hook pour éviter deux méthodes `getStatus()` et `setStatus()` devenues redondantes.

Les property hooks permettent d'attacher un comportement `get` et `set` directement à une propriété, sans passer par des méthodes dédiées. C'est élégant, cela réduit le code de plomberie, mais cela change la façon dont PHPUnit — et surtout les doublures de test construites autour des anciens accesseurs — interagissent avec l'objet. Ce billet documente ce que nous avons dû corriger, pas ce qui a changé côté propriétés dynamiques de PHP 8.2, déjà traité par ailleurs.

## Ce que les property hooks changent concrètement

Avant PHP 8.4, une classe qui voulait valider ou transformer une valeur à l'écriture exposait généralement une méthode `setStatus(string $status): void`. Les tests créaient un mock de la classe et vérifiaient que `setStatus` était appelée avec le bon argument, via `$mock->expects($this->once())->method('setStatus')->with('publie')`. Avec un property hook, il n'y a plus de méthode à espionner : l'affectation `$commande->status = 'publie'` déclenche directement le hook `set`, sans passer par un appel de méthode visible pour PHPUnit.

Résultat : les mocks construits avec `createMock()` ou `getMockBuilder()` qui reposaient sur l'interception d'un appel de méthode ne voient plus rien passer. Le test ne plante pas toujours — parfois il passe silencieusement sans avoir vérifié ce qu'il croyait vérifier, ce qui est pire qu'un échec franc.

## Où cela casse dans une suite WordPress typique

Sur nos projets, les property hooks sont surtout utiles dans les classes de domaine — un objet `Commande`, un objet `Adhesion` — qui ne dépendent pas directement des hooks WordPress. Trois zones ont posé problème :

- Les classes qui exposaient un accesseur uniquement pour permettre le test (anti-pattern qu'on croyait avoir éliminé, réapparu sous une autre forme)
- Les traits partagés entre plusieurs types d'objets, où un seul hook modifiait le comportement attendu par des tests écrits pour l'ancienne API
- Les sérialisations vers des tableaux (`toArray()`) qui lisaient directement la propriété plutôt que de passer par le hook `get`, créant une désynchronisation entre la valeur stockée et la valeur exposée

> L'essentiel à retenir : Les hooks get/set remplacent des accesseurs manuels testés par mock ; Un mock sur une propriété hookée ne se comporte plus comme avant ; Prévoir une phase de revue ciblée plutôt qu'une réécriture globale

## La correction que nous avons appliquée

Plutôt que de réécrire les mocks, nous avons choisi de tester le résultat observable plutôt que l'appel intermédiaire. Un test qui vérifiait `$mock->expects($this->once())->method('setStatus')` a été remplacé par une assertion sur l'état final de l'objet après affectation :

```
public function test_status_est_normalise_en_minuscules(): void
{
    $commande = new Commande();
    $commande->status = 'PUBLIE';

    $this->assertSame('publie', $commande->status);
}
```

Cette approche est en réalité plus robuste : elle teste le comportement, pas l'implémentation. Le hook peut changer de forme demain sans casser le test, tant que le contrat — normaliser en minuscules — reste respecté.

### Cas où le mock reste nécessaire

Quand la propriété hookée déclenche un effet de bord externe (écriture en base, appel à une API), le mock garde son intérêt, mais il doit cibler la dépendance injectée dans le hook, pas la propriété elle-même. Nous injectons désormais un service de journalisation dans le constructeur et vérifions ses appels, plutôt que d'essayer d'intercepter l'affectation de propriété.

## Prévenir la régression sur les prochains projets

Nous avons ajouté une règle PHPStan personnalisée qui signale toute propriété hookée testée uniquement via un mock d'accesseur — un signal faible, mais suffisant pour déclencher une relecture manuelle avant fusion. Sur le projet audité, cela représentait quatorze tests sur une centaine, soit une proportion suffisante pour justifier une revue systématique plutôt qu'un correctif au cas par cas.

> Un mock qui passe sans avoir vérifié ce qu'il prétend vérifier est plus dangereux qu'un test qui échoue franchement.

## Notre verdict

Les property hooks de PHP 8.4 sont un gain net pour la lisibilité du code de domaine, mais ils ne sont pas neutres pour une suite de tests construite autour des mocks de méthode. La migration ne demande pas de réécrire la suite, seulement d'identifier les tests qui interceptaient des accesseurs et de les recentrer sur l'état observable de l'objet. Documentation officielle utile pour approfondir : [php.net](https://www.php.net/manual/en/language.oop5.property-hooks.php).
