Dans la vidéo, je crée et je déploie une application e-commerce avec Symfony, React et Stripe. La vidéo date de 2023 : les versions et les commandes de cet article sont à jour, la démarche reste la même. Le point le plus important, celui qui décide si ta boutique est fiable, c’est le traitement des webhooks — la partie que tout le monde remet au lendemain.

L’architecture en une page

Trois responsabilités, trois outils :

  • Symfony porte le catalogue, les commandes et les prix.
  • Stripe Checkout encaisse : c’est Stripe qui affiche le formulaire de carte, donc tu ne manipules jamais de numéro de carte et ton périmètre PCI se réduit au questionnaire le plus léger, le SAQ A (sécurité).
  • React, monté dans les pages Twig, s’occupe des zones interactives. Ce n’est pas un front séparé.

Cette séparation évite la double peine du SPA e-commerce : deux déploiements, deux jeux de routes, et le référencement à reconstruire.

Installer Symfony et modéliser le catalogue

La documentation actuelle exige PHP 8.4 ou plus et Composer, avec les extensions Ctype, iconv, PCRE, Session, SimpleXML et Tokenizer (setup) :

composer create-project symfony/skeleton:"8.1.*" boutique
cd boutique
composer require webapp

Le pack webapp embarque déjà Doctrine, le MakerBundle en développement et le reste des composants courants. La connexion à la base passe par DATABASE_URL dans le .env (doctrine). Crée tes entités avec make:entity, puis :

php bin/console make:migration
php bin/console doctrine:migrations:migrate

Deux réflexes qui évitent des bugs pénibles : stocke les prix en entiers, en centimes — c’est aussi le format attendu par Stripe (devises) — et joue les migrations au déploiement, jamais à la main sur le serveur.

Monter React dans les pages Twig

Symfony UX React rend des composants React depuis Twig, avec transfert de propriétés (UX React). Trois points à retenir :

  1. Tout fichier placé dans assets/react/controllers/ est rendable, après enregistrement par registerReactControllerComponents().
  2. Si tu enregistres les composants avec import.meta.glob(), passe l’option { eager: true } : le bundle ne gère pas les composants chargés à la demande. Avec Webpack Encore, l’enregistrement passe par require.context() et la recette active .enableReactPreset() ; avec AssetMapper, précompile les fichiers .jsx en JavaScript.
  3. Le rendu se fait dans Twig avec react_component(). Les propriétés sont sérialisées en JSON : passe des scalaires préparés dans le contrôleur, jamais une collection d’entités Doctrine.
{# lignesPanier = [{ id, nom, quantite, prixUnitaire }, …] #}
<div {{ react_component('Panier', { items: lignesPanier }) }}></div>

Encaisser avec Stripe Checkout

La règle de base : la session de paiement se crée côté serveur, parce qu’elle exige une clé secrète qui ne doit jamais atteindre le navigateur (quickstart PHP). Installe la bibliothèque officielle, puis injecte la clé par variable d’environnement :

composer require stripe/stripe-php
public function payer(Produit $produit, StripeClient $stripe): Response
{
    $session = $stripe->checkout->sessions->create([
        'mode' => 'payment',
        'line_items' => [[
            'quantity' => 1,
            'price_data' => [
                'currency' => 'eur',
                'unit_amount' => $produit->getPrix(),   // en centimes
                'product_data' => ['name' => $produit->getNom()],
            ],
        ]],
        'success_url' => $url . '?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => $urlAnnulation,
    ]);

    return $this->redirect($session->url, 303);
}

price_data évite d’avoir deux sources de vérité pour le prix : ta base décide, Stripe encaisse. Le mode accepte payment pour un achat ponctuel, subscription pour un abonnement et setup pour enregistrer un moyen de paiement sans débiter. Dans ce mode hébergé, tu rediriges l’acheteur vers $session->url ; le client_secret ne concerne que les modes intégrés (embedded, custom) (API Checkout).

Côté clés : la clé publique porte le préfixe pk, la secrète sk, la restreinte rk, et le segment test ou live dit l’environnement (clés). Le sandbox se teste avec la carte 4242 4242 4242 4242 ; en code, préfère pm_card_visa (tests).

Les webhooks : ce qui rend la boutique fiable

Le navigateur de l’acheteur peut fermer l’onglet, perdre le réseau ou revenir en arrière après le paiement. La seule source de vérité est le webhook : checkout.session.completed quand payment_status vaut paid, sinon c’est checkout.session.async_payment_succeeded — ou checkout.session.async_payment_failed — qui tranche pour les moyens asynchrones (fulfillment).

#[Route('/webhook/stripe', methods: ['POST'])]
public function webhook(Request $request, string $secret): Response
{
    try {
        $event = Webhook::constructEvent(
            $request->getContent(),                          // corps brut
            $request->headers->get('Stripe-Signature', ''),
            $secret,
        );
    } catch (SignatureVerificationException) {
        return new Response('Signature invalide', 400);
    }

    // traiter l'événement, puis répondre vite
    return new Response('ok', 200);
}

Quatre règles, toutes documentées :

  1. Vérifie la signature avec le corps brut, l’en-tête Stripe-Signature et le secret de l’endpoint : un corps JSON analysé puis ré-encodé ne valide plus (signature).
  2. Laisse la route accessible sans authentification : elle ne doit pas tomber sous un access_control qui exige un utilisateur connecté. La signature remplace l’authentification (sécurité Symfony).
  3. Sois idempotent : un événement peut arriver plusieurs fois et l’ordre n’est pas garanti. Journalise son identifiant et ignore ce que tu as déjà traité (webhooks).
  4. Réponds vite et traite le reste en asynchrone, avec Symfony Messenger.

En local, la CLI Stripe fait le travail — en adaptant le port à ton serveur Symfony, 8000 par défaut : stripe listen --forward-to localhost:8000/webhook/stripe, puis stripe trigger checkout.session.completed pour provoquer l’événement (CLI).

Déployer

La page de déploiement de Symfony donne les étapes : envoyer le code, installer les dépendances sans les paquets de développement, jouer les migrations, vider le cache (deployment). L’ordre compte, et l’environnement doit être en production avant les commandes console :

composer install --no-dev --optimize-autoloader
composer dump-env prod
APP_ENV=prod APP_DEBUG=0 php bin/console doctrine:migrations:migrate --no-interaction
APP_ENV=prod APP_DEBUG=0 php bin/console asset-map:compile   # ou npm run build avec Encore
APP_ENV=prod APP_DEBUG=0 php bin/console cache:clear

Les variables d’environnement doivent venir du serveur, et var/cache/ comme var/log/ doivent être accessibles en écriture au serveur web (permissions). Nginx doit être configuré pour Symfony : la documentation fournit les blocs à recopier (web server).

Sur un VPS Hostinger, tu te connectes en SSH ou par la console web du hPanel, dont la documentation explique l’accès (SSH). Pour le TLS, la méthode recommandée par Certbot reste la référence (certbot.eff.org) — vérifie la procédure en vigueur pour ton image, les tutoriels d’hébergeur vieillissent vite. Si tu préfères les conteneurs, le Docker Manager du hPanel déploie un fichier compose depuis l’interface (Docker), ce qui rejoint la mise en production d’un projet Symfony avec Docker et FrankenPHP.

Si tu débutes sur le framework, la série de tutoriels Symfony part de Symfony 5 : les concepts tiennent toujours, mais vérifie les versions des composants avant de recopier une commande.

Dans la vidéo, je parcours le projet complet, du modèle de données au paiement encaissé.