Aller au contenu
Sylvain Joffraud l4w
Retour

Magic strings de session : un SessionManager typé

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 :

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

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

#php #symfony #session #refactoring #dry


Partager cet article :

Article précédent
Parser une date FR en PHP : le `!` qui force minuit
Article suivant
State machine dans l’agrégat : transitions protégées