Sylius15 min

Migration Frontend Sylius 2.0 : De Semantic UI + jQuery a Bootstrap 5 + Stimulus

Par Pierre-Arthur Demengel
SyliusFrontendBootstrapStimulusjQueryMigrationWebpack

La migration frontend est souvent le chantier le plus sous-estime lors du passage a Sylius 2.0. Passer de Semantic UI + jQuery a Bootstrap 5 + Symfony UX (Stimulus + Turbo) ne se resume pas a remplacer des classes CSS : c'est une refonte complete de l'architecture frontend. Ce guide technique couvre chaque etape, avec des exemples concrets avant/apres.

Etat des lieux : ce qui change

Voici la matrice de correspondance entre l'ancien et le nouveau stack :

Sylius 1.xSylius 2.0
Semantic UI 2.4Bootstrap 5.3
jQuery 3.xStimulus 3.x
jQuery AJAXFetch API / Turbo Frames
Semantic UI modules (modal, dropdown, tab, accordion)Composants Bootstrap (modal, dropdown, tabs, accordion)
Gulp / custom WebpackWebpack Encore (standardise)
Fichiers JS monolithiquesControleurs Stimulus modulaires

Inventaire des classes Semantic UI a remplacer

Avant de commencer, faites un inventaire exhaustif. Voici les commandes pour quantifier le travail :

# Compter les occurrences de classes Semantic UI dans vos templates
grep -r "ui button" templates/ --include="*.twig" -c
grep -r "ui form" templates/ --include="*.twig" -c
grep -r "ui modal" templates/ --include="*.twig" -c
grep -r "ui segment" templates/ --include="*.twig" -c
grep -r "ui grid" templates/ --include="*.twig" -c
grep -r "ui menu" templates/ --include="*.twig" -c
grep -r "ui dropdown" templates/ --include="*.twig" -c
grep -r "ui tab" templates/ --include="*.twig" -c
grep -r "ui card" templates/ --include="*.twig" -c
grep -r "ui table" templates/ --include="*.twig" -c
grep -r "ui message" templates/ --include="*.twig" -c
grep -r "ui label" templates/ --include="*.twig" -c
grep -r "ui container" templates/ --include="*.twig" -c

# Total des fichiers Twig concernes
grep -rl "ui " templates/ --include="*.twig" | wc -l

# Compter les fichiers jQuery custom
find assets/ -name "*.js" -exec grep -l "\$(" {} \; | wc -l

Mapping des composants : Semantic UI vers Bootstrap 5

Grille et mise en page

<!-- AVANT : Semantic UI grid -->
<div class="ui grid">
    <div class="eight wide column">Gauche</div>
    <div class="eight wide column">Droite</div>
</div>

<!-- APRES : Bootstrap 5 grid -->
<div class="row">
    <div class="col-md-6">Gauche</div>
    <div class="col-md-6">Droite</div>
</div>

Mapping des colonnes : Semantic UI utilise des mots (one wide a sixteen wide sur une grille de 16). Bootstrap utilise des nombres (col-1 a col-12 sur une grille de 12). Attention a la conversion : eight wide (8/16 = 50%) = col-md-6 (6/12 = 50%), mais five wide (5/16 = 31.25%) n'a pas d'equivalent exact dans Bootstrap.

Boutons

<!-- AVANT -->
<button class="ui primary button">Valider</button>
<button class="ui red basic button">Annuler</button>
<button class="ui large green labeled icon button">
    <i class="cart icon"></i> Ajouter au panier
</button>

<!-- APRES -->
<button class="btn btn-primary">Valider</button>
<button class="btn btn-outline-danger">Annuler</button>
<button class="btn btn-success btn-lg">
    <i class="bi bi-cart"></i> Ajouter au panier
</button>

Formulaires

<!-- AVANT : Semantic UI form -->
<form class="ui form">
    <div class="field">
        <label>Email</label>
        <input type="email" placeholder="email@exemple.com">
    </div>
    <div class="field error">
        <label>Mot de passe</label>
        <input type="password">
        <div class="ui pointing red basic label">Mot de passe requis</div>
    </div>
    <button class="ui submit button">Connexion</button>
</form>

<!-- APRES : Bootstrap 5 form -->
<form>
    <div class="mb-3">
        <label class="form-label">Email</label>
        <input type="email" class="form-control" placeholder="email@exemple.com">
    </div>
    <div class="mb-3">
        <label class="form-label">Mot de passe</label>
        <input type="password" class="form-control is-invalid">
        <div class="invalid-feedback">Mot de passe requis</div>
    </div>
    <button type="submit" class="btn btn-primary">Connexion</button>
</form>

Modales

<!-- AVANT : Semantic UI modal + jQuery -->
<div class="ui modal" id="confirmDelete">
    <div class="header">Confirmation</div>
    <div class="content">
        <p>Supprimer cet element ?</p>
    </div>
    <div class="actions">
        <div class="ui cancel button">Non</div>
        <div class="ui red ok button">Oui, supprimer</div>
    </div>
</div>

<script>
$('#confirmDelete').modal('show');
$('#confirmDelete').modal({
    onApprove: function() {
        $.ajax({ url: '/api/delete/42', method: 'DELETE' });
    }
});
</script>

<!-- APRES : Bootstrap 5 modal + Stimulus -->
<div class="modal fade" id="confirmDelete" tabindex="-1"
     data-controller="confirm-delete"
     data-confirm-delete-url-value="/api/delete/42">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Confirmation</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <p>Supprimer cet element ?</p>
            </div>
            <div class="modal-footer">
                <button class="btn btn-secondary" data-bs-dismiss="modal">Non</button>
                <button class="btn btn-danger" data-action="confirm-delete#delete">Oui, supprimer</button>
            </div>
        </div>
    </div>
</div>

Reecrire les interactions jQuery en controleurs Stimulus

Pattern AJAX classique

// AVANT : jQuery AJAX
$(document).ready(function() {
    $('.add-to-cart').on('click', function(e) {
        e.preventDefault();
        var $btn = $(this);
        var url = $btn.data('url');
        var quantity = $btn.closest('form').find('input[name="quantity"]').val();

        $.ajax({
            url: url,
            method: 'POST',
            data: { quantity: quantity },
            success: function(response) {
                $('#cart-count').text(response.itemCount);
                $btn.text('Ajoute !').addClass('green');
                setTimeout(function() {
                    $btn.text('Ajouter au panier').removeClass('green');
                }, 2000);
            },
            error: function() {
                alert('Erreur lors de l\'ajout au panier');
            }
        });
    });
});
// APRES : Stimulus controller
// assets/controllers/add_to_cart_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ['button', 'quantity', 'cartCount'];
    static values = { url: String };

    async add(event) {
        event.preventDefault();
        const button = this.buttonTarget;
        const originalText = button.textContent;

        try {
            const response = await fetch(this.urlValue, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest',
                },
                body: JSON.stringify({
                    quantity: parseInt(this.quantityTarget.value, 10),
                }),
            });

            if (!response.ok) throw new Error('Erreur serveur');

            const data = await response.json();

            // Mise a jour du compteur panier
            this.cartCountTarget.textContent = data.itemCount;

            // Feedback visuel
            button.textContent = 'Ajoute !';
            button.classList.add('btn-success');
            button.classList.remove('btn-primary');

            setTimeout(() => {
                button.textContent = originalText;
                button.classList.remove('btn-success');
                button.classList.add('btn-primary');
            }, 2000);
        } catch (error) {
            button.textContent = 'Erreur...';
            setTimeout(() => { button.textContent = originalText; }, 2000);
        }
    }
}
<!-- Template Twig associe -->
<div data-controller="add-to-cart"
     data-add-to-cart-url-value="{{ path('sylius_shop_ajax_cart_add_item', {'productId': product.id}) }}">
    <input type="number" value="1" min="1" data-add-to-cart-target="quantity" class="form-control">
    <button data-add-to-cart-target="button"
            data-action="add-to-cart#add"
            class="btn btn-primary">
        Ajouter au panier
    </button>
    <span data-add-to-cart-target="cartCount">{{ cart_item_count }}</span>
</div>

Dropdowns et navigation

// AVANT : Semantic UI dropdown init
$('.ui.dropdown').dropdown();

// APRES : Bootstrap 5 — aucun JS necessaire !
// Les dropdowns Bootstrap fonctionnent via data attributes
// <div class="dropdown">
//   <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Menu</button>
//   <ul class="dropdown-menu">...</ul>
// </div>

C'est un gain majeur : la plupart des composants Bootstrap 5 fonctionnent sans ecrire de JavaScript, grace au systeme de data attributes. Semantic UI imposait une initialisation jQuery explicite pour chaque composant.

Onglets

<!-- AVANT : Semantic UI tabs -->
<div class="ui top attached tabular menu">
    <a class="item active" data-tab="description">Description</a>
    <a class="item" data-tab="reviews">Avis</a>
    <a class="item" data-tab="specs">Specifications</a>
</div>
<div class="ui bottom attached tab segment active" data-tab="description">...</div>
<div class="ui bottom attached tab segment" data-tab="reviews">...</div>
<div class="ui bottom attached tab segment" data-tab="specs">...</div>
<script>$('.menu .item').tab();</script>

<!-- APRES : Bootstrap 5 tabs (zero JS custom) -->
<ul class="nav nav-tabs" role="tablist">
    <li class="nav-item">
        <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#description">Description</button>
    </li>
    <li class="nav-item">
        <button class="nav-link" data-bs-toggle="tab" data-bs-target="#reviews">Avis</button>
    </li>
    <li class="nav-item">
        <button class="nav-link" data-bs-toggle="tab" data-bs-target="#specs">Specifications</button>
    </li>
</ul>
<div class="tab-content">
    <div class="tab-pane fade show active" id="description">...</div>
    <div class="tab-pane fade" id="reviews">...</div>
    <div class="tab-pane fade" id="specs">...</div>
</div>

Turbo Frames pour les mises a jour partielles

Turbo remplace le pattern jQuery "charger un fragment HTML via AJAX et l'injecter dans le DOM" :

<!-- AVANT : jQuery load fragment -->
<div id="cart-summary"></div>
<script>$('#cart-summary').load('/cart/summary');</script>

<!-- APRES : Turbo Frame -->
<turbo-frame id="cart-summary" src="{{ path('app_cart_summary') }}" loading="lazy">
    <p>Chargement...</p>
</turbo-frame>

Le Turbo Frame charge automatiquement le contenu depuis l'URL source, extrait le <turbo-frame> correspondant dans la reponse, et remplace le contenu. Pas de JavaScript custom necessaire.

Turbo Frames pour la pagination

<!-- La pagination sans rechargement complet -->
<turbo-frame id="product-list">
    {% for product in products %}
        <div class="col-md-4">
            {{ include('@SyliusShop/Product/_card.html.twig') }}
        </div>
    {% endfor %}

    <nav>
        {{ include('@SyliusShop/_pagination.html.twig') }}
        {# Les liens de pagination ciblent automatiquement le turbo-frame parent #}
    </nav>
</turbo-frame>

Configuration Webpack Encore

Sylius 2.0 standardise Webpack Encore. Voici la configuration type :

// webpack.config.js
const Encore = require('@symfony/webpack-encore');

Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')

    // Shop entry point
    .addEntry('shop', './assets/shop/entry.js')

    // Admin entry point
    .addEntry('admin', './assets/admin/entry.js')

    // Stimulus / Symfony UX
    .enableStimulusBridge('./assets/controllers.json')

    // Sass pour Bootstrap
    .enableSassLoader()

    // Single runtime chunk
    .enableSingleRuntimeChunk()

    .cleanupOutputBeforeBuild()
    .enableSourceMaps(!Encore.isProduction())
    .enableVersioning(Encore.isProduction())
;

module.exports = Encore.getWebpackConfig();

Point d'entree shop

// assets/shop/entry.js

// Bootstrap CSS
import 'bootstrap/scss/bootstrap.scss';

// Votre CSS custom
import './styles/app.scss';

// Bootstrap JS (composants necessaires uniquement)
import 'bootstrap/js/dist/modal';
import 'bootstrap/js/dist/dropdown';
import 'bootstrap/js/dist/tab';
import 'bootstrap/js/dist/collapse';
import 'bootstrap/js/dist/alert';

// Stimulus
import { startStimulusApp } from '@symfony/stimulus-bridge';
export const app = startStimulusApp(
    require.context('./controllers', true, /\.(j|t)sx?$/)
);

// Turbo
import '@hotwired/turbo';

Registre des controleurs Stimulus

// assets/controllers.json
{
    "controllers": {
        "@symfony/ux-turbo": {
            "turbo-core": {
                "enabled": true,
                "fetch": "eager"
            }
        }
    },
    "entrypoints": []
}

Gestion des plugins jQuery tiers

Si votre projet utilise des plugins jQuery tiers (select2, datepicker, slick carousel...), voici les strategies :

  • select2Tom Select (pas de dependance jQuery, controleur Stimulus dispo dans Symfony UX)
  • jQuery UI Datepicker<input type="date"> natif ou Flatpickr
  • Slick / Owl CarouselSwiper (zero jQuery)
  • DataTablesGrid.js ou la pagination Turbo Frame
  • Fancybox / LightboxfsLightbox ou un controleur Stimulus custom

Pour chaque plugin, la demarche est : (1) identifier un equivalent sans jQuery, (2) creer un controleur Stimulus wrapper si necessaire, (3) tester visuellement et fonctionnellement.

Comparaison de performances avant/apres

Voici les gains typiques observes apres migration du stack frontend :

MetriqueSemantic UI + jQueryBootstrap 5 + StimulusGain
CSS total (minifie gzip)~180 Ko~45 Ko-75%
JS total (minifie gzip)~130 Ko (jQuery + Semantic)~35 Ko (Stimulus + Turbo)-73%
LCP (Largest Contentful Paint)~2.8s~1.4s-50%
TBT (Total Blocking Time)~450ms~120ms-73%
Navigations subsequentesFull page reloadTurbo partial (200-500ms)-80%

La reduction massive de la taille des assets s'explique par deux facteurs : Bootstrap 5 est plus leger que Semantic UI, et Stimulus + Turbo pesent une fraction de jQuery. De plus, Turbo elimine les rechargements complets de page pour les navigations internes, ce qui transforme l'experience utilisateur.

Strategie de migration recommandee

  1. Phase 1 — Inventaire : utilisez Sylius Upgrade Analyzer pour quantifier automatiquement vos fichiers Semantic UI, scripts jQuery et overrides de templates
  2. Phase 2 — Admin d'abord : migrez le panel admin vers le nouveau stack Sylius 2.0. L'admin est fourni cle en main, le travail se limite a vos customisations
  3. Phase 3 — Layout shop : migrez le layout principal (header, footer, grille) vers Bootstrap. Testez la navigation et le responsive
  4. Phase 4 — Composants interactifs : reecrivez les modales, dropdowns, onglets et carrousels un par un. Chaque composant est un controleur Stimulus independant
  5. Phase 5 — AJAX vers Turbo/Stimulus : remplacez les appels jQuery AJAX par des Turbo Frames ou des controleurs Stimulus avec Fetch API
  6. Phase 6 — Nettoyage : supprimez jQuery, Semantic UI et leurs dependances de package.json. Verifiez qu'aucun $ ou jQuery ne traine dans le code

Tests frontend

La migration frontend doit etre validee a trois niveaux :

  • Tests visuels : captures d'ecran automatisees (Playwright, Percy, Chromatic) pour detecter les regressions CSS
  • Tests fonctionnels : scenarios Cypress ou Playwright pour les parcours critiques (ajout au panier, checkout, recherche)
  • Tests de performance : Lighthouse CI dans votre pipeline pour verifier que les metriques Core Web Vitals ne regressent pas
// Exemple : test Playwright pour le parcours d'achat
import { test, expect } from '@playwright/test';

test('ajout au panier et checkout', async ({ page }) => {
    await page.goto('/products/t-shirt-symfony');

    // Ajout au panier via Stimulus controller
    await page.fill('[data-add-to-cart-target="quantity"]', '2');
    await page.click('[data-action="add-to-cart#add"]');

    // Verification du compteur panier
    await expect(page.locator('[data-add-to-cart-target="cartCount"]')).toHaveText('2');

    // Navigation vers le checkout (Turbo, pas de full reload)
    await page.click('a[href="/checkout"]');
    await expect(page).toHaveURL('/checkout/address');
});

Conclusion

La migration frontend Sylius est un chantier consequent mais les gains sont considerables : assets divises par 3 ou 4, navigations quasi-instantanees avec Turbo, code modulaire et testable avec Stimulus, et un framework CSS activement maintenu. Commencez par l'inventaire automatise avec Sylius Upgrade Analyzer, procedez par phases (admin, layout, composants, AJAX), et ne sautez pas les tests. Si vous avez un storefront Sylius custom avec de nombreuses interactions jQuery, contactez-moi pour un audit detaille et un plan de migration personnalisé.

Questions fréquentes

13 projets livresGrand-Est & BelgiqueLighthouse >90Disponible immédiatement

Un projet en tete ?

Discutons de votre site web. Réponse garantie sous 24h.

Ou appelez directement :06 95 41 30 25

WhatsApp
Appeler