Un tunnel multi-étapes stocke son état d’avancement en session : les données de chaque étape, le jeton de signature, le motif de paiement, etc. Au fil du temps et des contrôleurs, les clés se sont multipliées sous forme de chaînes nues :
$session->set('tunnel_step_' . $step, $data);
$session->get('tunnel_step_5_uploads', []);
$session->set('tunnel_signature_signed', true);
$session->get('tunnel_payment_amount_cents');
// … 49 occurrences, dispersées sur 4 contrôleurs
Un grep a compté 49 occurrences de ces magic strings 'tunnel_*' réparties sur 4 contrôleurs (le tunnel principal, le paiement, l’upload, et un contrôleur de dev). Chacune est une typo silencieuse en puissance.
Pourquoi c’est un piège
Une clé de session écrite en dur n’a aucun filet :
- Typo = bug muet.
$session->get('tunnel_signature_signd')(uneen moins) ne lève rien. Il renvoienull/le défaut, et l’utilisateur se retrouve renvoyé à l’étape signature sans qu’aucune erreur ne s’affiche. On le découvre en prod. - Refacto pénible. Renommer une clé = rechercher-remplacer une chaîne dans tout le code, en espérant n’en oublier aucune et ne pas attraper de faux positif.
- Aucune vue d’ensemble. Quelles clés ce tunnel utilise-t-il ? Impossible à répondre sans grep. La « forme » de l’état en session n’est écrite nulle part.
- Types perdus. Chaque
get()renvoie dumixed. L’appelant doit se souvenir quepayment_amount_centsest unintetuploadsunarray.
Un manager qui possède les clés
La parade : une classe qui détient toutes les clés (en constantes privées) et n’expose que des accesseurs typés. Aucune méthode ne renvoie de magic string nu ; le reste du code ne connaît plus jamais la chaîne.
final class TunnelSessionManager
{
private const STEP_PREFIX = 'tunnel_step_';
private const KEY_UPLOADS = 'tunnel_step_5_uploads';
private const KEY_SIGNATURE_TX = 'tunnel_signature_tx';
private const KEY_SIGNATURE_SIGNED = 'tunnel_signature_signed';
private const KEY_PAYMENT_CENTS = 'tunnel_payment_amount_cents';
public function __construct(private readonly RequestStack $requestStack)
{
}
private function session(): SessionInterface
{
return $this->requestStack->getSession();
}
/** @return array<string, mixed> */
public function getStep(int $step, array $default = []): array
{
return $this->session()->get(self::STEP_PREFIX . $step, $default);
}
/** @param array<string, mixed> $data */
public function saveStep(int $step, array $data): void
{
$this->session()->set(self::STEP_PREFIX . $step, $data);
}
public function hasStep(int $step): bool
{
return $this->session()->has(self::STEP_PREFIX . $step);
}
public function isSignatureSigned(): bool
{
return (bool) $this->session()->get(self::KEY_SIGNATURE_SIGNED, false);
}
public function markSignatureSigned(string $transactionId): void
{
$this->session()->set(self::KEY_SIGNATURE_TX, $transactionId);
$this->session()->set(self::KEY_SIGNATURE_SIGNED, true);
}
public function getPaymentAmountCents(): ?int
{
$cents = $this->session()->get(self::KEY_PAYMENT_CENTS);
return $cents === null ? null : (int) $cents;
}
}
Les 49 magic strings deviennent un seul point de vérité. Côté contrôleur :
// Avant : magic string + cast à la main + risque de typo
if ($session->get('tunnel_signature_signed', false)) { /* … */ }
// Après : intention typée, autocomplétion, zéro chaîne nue
if ($this->tunnelSession->isSignatureSigned()) { /* … */ }
Les bénéfices concrets
- La typo redevient une erreur de compilation. Se tromper sur
KEY_SIGNATURE_SIGNED(une constante) est attrapé par l’IDE et l’analyse statique. Se tromper sur'tunnel_signature_signed'(une chaîne) ne l’est jamais. - Renommer une clé = changer une constante. Une ligne, un seul endroit. Le reste du code ne référence que l’accesseur.
- L’état du tunnel est documenté par la classe. Lire les constantes et les méthodes du
SessionManager, c’est lire la liste exhaustive de ce que le tunnel met en session — et avec quels types. - Les types sont posés une fois.
getPaymentAmountCents(): ?intcaste au bon endroit ; les appelants n’ont plus à se souvenir de rien.
Détail d’architecture qui s’est révélé utile : comme le projet avait deux tunnels (souscription et devis), j’ai créé un manager par tunnel, symétriques. Chacun possède son propre préfixe de clés — pas de collision possible entre les deux, et chaque tunnel reste maître de son namespace de session.
Lessons learned
- Une clé de session écrite en dur est une typo silencieuse en attente. Aucune erreur, juste un comportement faux (utilisateur renvoyé en arrière, donnée perdue). Centralisez les clés dans des constantes ; la typo redevient détectable.
- Exposez des accesseurs typés, jamais la chaîne.
isSignatureSigned(): boolplutôt queget('...'). Le reste du code ne doit jamais voir la magic string ni lemixed. - Le SessionManager documente la forme de l’état. Ses constantes + méthodes sont la spec de ce que le tunnel stocke. Plus besoin de grep pour répondre à « quelles clés utilise ce tunnel ? ».
- Un manager par contexte de session, avec son propre préfixe de clés, évite les collisions quand plusieurs tunnels cohabitent. Symétrie et isolation.
- Le casting vit dans l’accesseur, une fois.
?int,bool,array<string,mixed>posés dans le manager déchargent tous les appelants de cette gymnastique.
#php #symfony #session #refactoring #dry