1. Accueil
  2. Articles
4 min de lecture
2 vues

static $variable en PHP : un piège silencieux avec Laravel Octane

mckenziearts
Arthur Monney

Quand on travaille avec Laravel sans Octane, certaines habitudes de code semblent inoffensives. Mais dès qu'on passe à Octane, elles peuvent provoquer des bugs difficiles à reproduire et encore plus difficiles à débugger en production.

C'est exactement ce qui m'est arrivé avec une simple variable static dans un helper.


Le contexte

Dans mon projet, j'avais un helper current_tax_label() dont le rôle est de retourner le label de taxe applicable à l'utilisateur selon sa zone géographique (TTC ou HT).

Pour éviter de refaire le calcul plusieurs fois dans la même requête, j'avais utilisé une technique classique de memoization :

1function current_tax_label(): string
2{
3 static $label = null;
4 
5 if ($label !== null) {
6 return $label;
7 }
8 
9 $zone = ZoneSessionManager::getSession();
10 
11 if (! $zone instanceof CountryByZoneData) {
12 return $label = '';
13 }
14 
15 $taxZone = TaxZone::query()
16 ->whereHas('country', fn ($q) => $q->where('cca2', $zone->countryCode))
17 ->whereNull('province_code')
18 ->first();
19 
20 return $label = $taxZone?->is_tax_inclusive ? __('TTC') : __('HT');
21}
1function current_tax_label(): string
2{
3 static $label = null;
4 
5 if ($label !== null) {
6 return $label;
7 }
8 
9 $zone = ZoneSessionManager::getSession();
10 
11 if (! $zone instanceof CountryByZoneData) {
12 return $label = '';
13 }
14 
15 $taxZone = TaxZone::query()
16 ->whereHas('country', fn ($q) => $q->where('cca2', $zone->countryCode))
17 ->whereNull('province_code')
18 ->first();
19 
20 return $label = $taxZone?->is_tax_inclusive ? __('TTC') : __('HT');
21}

En PHP classique, ce code est parfaitement correct. La variable static $label est initialisée à null à chaque nouvelle requête, et mémorisée le temps de celle-ci.


Pourquoi ça fonctionne en PHP classique

En PHP standard (Apache, PHP-FPM), chaque requête HTTP démarre un nouveau processus PHP qui repart de zéro. Toute la mémoire est libérée à la fin de la requête. Donc static $label est bien réinitialisée à null à chaque fois.

1Requête 1 → nouveau processus → $label = null → calcul → 'TTC' ✅
2Requête 2 → nouveau processus → $label = null → calcul → 'HT' ✅
1Requête 1 → nouveau processus → $label = null → calcul → 'TTC' ✅
2Requête 2 → nouveau processus → $label = null → calcul → 'HT' ✅

Pourquoi c'est un bug avec Octane

Laravel Octane (avec FrankenPHP ou Swoole) fonctionne différemment. L'application est chargée une seule fois en mémoire et des workers restent actifs en permanence pour traiter les requêtes entrantes. Il n'y a plus de "nouveau processus" à chaque requête.

Le problème : les variables static ne sont jamais réinitialisées entre les requêtes dans le même worker. Elles gardent leur valeur indéfiniment.

1Worker 1 — Requête A (utilisateur en France, zone EUR)
2 → $label est null → calcul → $label = 'TTC'
3
4Worker 1 — Requête B (utilisateur au Cameroun, zone XAF)
5 → $label est déjà 'TTC' (valeur de la requête précédente !)
6 → retourne 'TTC' sans recalculer
7 → ❌ mauvais label affiché
1Worker 1 — Requête A (utilisateur en France, zone EUR)
2 → $label est null → calcul → $label = 'TTC'
3
4Worker 1 — Requête B (utilisateur au Cameroun, zone XAF)
5 → $label est déjà 'TTC' (valeur de la requête précédente !)
6 → retourne 'TTC' sans recalculer
7 → ❌ mauvais label affiché

L'utilisateur camerounais voit le label de l'utilisateur français. Et selon l'ordre d'arrivée des requêtes, ça peut s'inverser. Le bug est aléatoire, silencieux, et ne génère aucune erreur.


La distinction importante

Beaucoup confondent les différents usages du mot-clé static en PHP. Voici ce qui pose problème et ce qui ne pose pas problème :

Syntaxe Comportement Danger pour Octane
static $var = null dans une fonction Variable persistante entre les appels ❌ Oui
Propriété static de classe mutable Persistante dans le process ❌ Oui
public static function foo() Simple méthode appelable sans instance ✅ Non
public const CACHE_TTL = 7200 Valeur immuable, définie à la compilation ✅ Non

La règle : ce n'est pas le mot-clé static sur une méthode qui pose problème, c'est le mot-clé static sur une variable — parce qu'elle est mutable et persiste dans le worker.


Le fix

La solution est simple : supprimer la memoization. Le coût d'une petite requête SQL supplémentaire est négligeable comparé au risque de données incorrectes en production.

1function current_tax_label(): string
2{
3 $zone = ZoneSessionManager::getSession();
4 
5 if (! $zone instanceof CountryByZoneData) {
6 return '';
7 }
8 
9 $taxZone = TaxZone::query()
10 ->whereHas('country', fn ($q) => $q->where('cca2', $zone->countryCode))
11 ->whereNull('province_code')
12 ->first();
13 
14 return $taxZone?->is_tax_inclusive ? __('TTC') : __('HT');
15}
1function current_tax_label(): string
2{
3 $zone = ZoneSessionManager::getSession();
4 
5 if (! $zone instanceof CountryByZoneData) {
6 return '';
7 }
8 
9 $taxZone = TaxZone::query()
10 ->whereHas('country', fn ($q) => $q->where('cca2', $zone->countryCode))
11 ->whereNull('province_code')
12 ->first();
13 
14 return $taxZone?->is_tax_inclusive ? __('TTC') : __('HT');
15}

Si la performance est vraiment un enjeu, la bonne approche avec Octane est d'utiliser le cache Laravel (Cache::remember(...)) keyed sur un identifiant de requête ou de session — pas une variable statique globale.


À retenir

Si tu utilises Laravel Octane, audite ton code à la recherche de :

  • static $variable dans des fonctions ou helpers
  • Propriétés static de classes modifiées à l'exécution
  • Singletons qui stockent un état lié à l'utilisateur ou à la requête

Ces patterns sont courants et fonctionnent parfaitement sans Octane. Avec Octane, ils deviennent des bombes à retardement.


Ce bug a été découvert lors d'une revue architecturale de l'application e-commerce demo multi-zones construite avec Laravel Shopper.