Ce guide vous explique comment créer une application e-commerce complète avec Symfony 8.0, incluant la gestion des produits, catégories, commandes, authentification utilisateur et interface d'administration.

Prérequis : Avoir Symfony 8.0 installé et configuré.


📋 Vue d'ensemble

Ce guide vous permettra de créer :

  • Catalogue produits avec catégories (relation ManyToMany)
  • Gestion des commandes (Order et OrderItem)
  • Authentification utilisateur (inscription, connexion, vérification email)
  • Interface d'administration (CRUD pour toutes les entités)
  • Sécurité (protection des routes par rôles)

🚀 Étape 1 : Créer le projet Symfony

Créer un nouveau projet

# Créer un nouveau projet Symfony 8.0 avec webapp
symfony new mon-ecommerce --version='8.0.*' --webapp

# Ou avec Composer
composer create-project symfony/skeleton:'8.0.*' mon-ecommerce
cd mon-ecommerce
composer require webapp

Installer les packages nécessaires

cd mon-ecommerce

# ORM Doctrine (base de données)
composer require symfony/orm-pack

# Maker Bundle (générateur de code)
composer require --dev symfony/maker-bundle

# Security Bundle (authentification)
composer require symfony/security-bundle

# Formulaires
composer require symfony/form

# Validation
composer require symfony/validator

# Twig (templates)
composer require symfony/twig-pack

# Mailer (emails)
composer require symfony/mailer

# Vérification d'email
composer require symfonycasts/verify-email-bundle

# Profiler (débogage)
composer require --dev symfony/profiler-pack

🗄️ Étape 2 : Configuration de la base de données

Configurer la connexion

Éditez le fichier .env :

DATABASE_URL='mysql://root:password@127.0.0.1:3306/ecommerce_db?serverVersion=8.0.32&charset=utf8mb4'

Créer la base de données

php bin/console doctrine:database:create

📦 Étape 3 : Créer les entités

Entité Category (Catégorie)

php bin/console make:entity Category

Remplissez les champs suivants :

  • name (string, 255, not null)
  • description (string, 255, nullable)
  • slug (string, 255, not null)
  • createdAt (datetime_immutable, nullable)
  • updatedAt (datetime_immutable, nullable)

Entité Product (Produit)

php bin/console make:entity Product

Remplissez les champs suivants :

  • name (string, 255, not null)
  • description (string, 255, nullable)
  • price (decimal, precision: 10, scale: 2, not null)
  • stock (integer, not null)
  • image (string, 255, nullable)
  • slug (string, 255, not null)
  • createdAt (datetime_immutable, nullable)
  • updatedAt (datetime_immutable, nullable)
  • categories (relation ManyToMany vers Category)

Important : Lors de la création de la relation ManyToMany, choisissez :

  • Type de relation : ManyToMany
  • Entité cible : Category
  • Propriété dans Category : products
  • Voulez-vous ajouter une nouvelle propriété dans Category ? : yes
  • Relation propriétaire : Product (côté Product)

Entité User (Utilisateur)

php bin/console make:user User

Choisissez :

  • Utiliser un email comme identifiant : yes
  • Stocker le mot de passe dans la base de données : yes

Ensuite, ajoutez des champs supplémentaires :

php bin/console make:entity User

Ajoutez :

  • firstName (string, 255, not null)
  • lastName (string, 255, not null)
  • address (string, 255, nullable)
  • city (string, 255, nullable)
  • postalCode (string, 20, nullable)
  • phone (string, 50, nullable)
  • isVerified (boolean, not null, default: false)

Entité Order (Commande)

php bin/console make:entity Order

Remplissez les champs suivants :

  • orderNumber (string, 50, not null)
  • status (string, 50, not null)
  • total (decimal, precision: 10, scale: 2, not null)
  • createdAt (datetime_immutable, nullable)
  • updatedAt (datetime_immutable, nullable)
  • user (relation ManyToOne vers User, not null)

Note : Doctrine créera automatiquement une table nommée `order` (avec backticks car "order" est un mot réservé SQL).

Entité OrderItem (Article de commande)

php bin/console make:entity OrderItem

Remplissez les champs suivants :

  • quantity (integer, not null)
  • price (decimal, precision: 10, scale: 2, not null)
  • orderId (relation ManyToOne vers Order, not null)
  • product (relation ManyToOne vers Product, not null)

Explication de la relation ManyToMany

La relation ManyToMany entre Product et Category permet :

  • Un produit peut appartenir à plusieurs catégories : Un produit peut être classé dans plusieurs catégories simultanément
  • Une catégorie peut contenir plusieurs produits : Une catégorie peut regrouper de nombreux produits
  • Table de jointure automatique : Doctrine crée automatiquement la table product_category pour gérer la relation

Structure de la table de jointure :

  • product_id (clé étrangère vers product)
  • category_id (clé étrangère vers category)
  • Clé primaire composite : (product_id, category_id)

Générer les migrations

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

🔐 Étape 4 : Configuration de l'authentification

Créer l'authentification

php bin/console make:auth

Choisissez :

  • Type d'authentification : Login form authenticator
  • Nom de la classe : AppAuthenticator
  • Route de redirection après connexion : /home

Configurer Security

Le fichier config/packages/security.yaml est automatiquement configuré. Vérifiez qu'il contient :

security:
    password_hashers:
        SymfonyComponentSecurityCoreUserPasswordAuthenticatedUserInterface: 'auto'
    
    providers:
        app_user_provider:
            entity:
                class: AppEntityUser
                property: email
    
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|assets|build)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                csrf_token_id: authenticate
                username_parameter: email
                password_parameter: password
            logout:
                path: app_logout
                target: app_home
    
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/profile, roles: ROLE_USER }

Créer le contrôleur d'authentification

php bin/console make:controller SecurityController

Le contrôleur généré contient les méthodes login() et logout().

Créer le contrôleur d'inscription

php bin/console make:registration-form

Choisissez :

  • Voulez-vous ajouter la vérification d'email ? : yes
  • Classe EmailVerifier : AppSecurityEmailVerifier

Le système génère automatiquement :

  • RegistrationController avec gestion de l'inscription et vérification d'email
  • RegistrationFormType avec les champs : firstName, lastName, email, agreeTerms, plainPassword
  • EmailVerifier pour la vérification d'email
  • Templates Twig pour l'inscription et la confirmation d'email

Adapter le formulaire d'inscription

Le formulaire RegistrationFormType est généré automatiquement avec les champs de base. Il inclut :

  • firstName et lastName (mappés à l'entité User)
  • email (mappé à l'entité User)
  • agreeTerms (non mappé, avec validation IsTrue)
  • plainPassword (non mappé, avec validation NotBlank et Length)

Le mot de passe est automatiquement hashé dans le contrôleur avant la persistance.


🛍️ Étape 5 : Créer les CRUD avec make:crud

CRUD pour Category

php bin/console make:crud Category

Choisissez :

  • Voulez-vous générer les routes avec des préfixes ? : no
  • Classe du contrôleur : CategoryController

Le système génère automatiquement :

  • CategoryController avec les actions : index, new, show, edit, delete
  • CategoryType (formulaire)
  • Templates Twig : index, new, show, edit, _form, _delete_form

CRUD pour Product

php bin/console make:crud Product

Le système génère automatiquement :

  • ProductController avec les actions : index, new, show, edit, delete
  • ProductType (formulaire)
  • Templates Twig : index, new, show, edit, _form, _delete_form

CRUD pour Order

php bin/console make:crud Order

Le système génère automatiquement :

  • OrderController avec les actions : index, new, show, edit, delete
  • OrderType (formulaire)
  • Templates Twig : index, new, show, edit, _form, _delete_form

CRUD pour OrderItem

php bin/console make:crud OrderItem

Le système génère automatiquement :

  • OrderItemController avec les actions : index, new, show, edit, delete
  • OrderItemType (formulaire)
  • Templates Twig : index, new, show, edit, _form, _delete_form

Adapter les formulaires

Les formulaires générés par make:crud incluent tous les champs de l'entité. Pour les relations ManyToMany et ManyToOne, adaptez les formulaires :

Dans ProductType.php :

  • Le champ categories est automatiquement généré en EntityType avec multiple => true
  • Vous pouvez améliorer l'affichage en changeant choice_label de id à name

Dans OrderType.php :

  • Le champ user est automatiquement généré en EntityType
  • Vous pouvez améliorer l'affichage en changeant choice_label de id à email

Dans OrderItemType.php :

  • Les champs orderId et product sont automatiquement générés en EntityType
  • Vous pouvez améliorer l'affichage en changeant choice_label de id à un champ plus lisible

Protéger les routes CRUD

Ajoutez l'attribut #[IsGranted('ROLE_ADMIN')] au-dessus de chaque classe de contrôleur CRUD :

use SymfonyComponentSecurityHttpAttributeIsGranted;

#[IsGranted('ROLE_ADMIN')]
#[Route('/product')]
final class ProductController extends AbstractController
{
    // ...
}

Cela protège toutes les routes du contrôleur et nécessite le rôle ROLE_ADMIN pour y accéder.


🏠 Étape 6 : Créer le HomeController

php bin/console make:controller HomeController

Créez une route protégée par ROLE_USER :

use SymfonyComponentSecurityHttpAttributeIsGranted;

#[IsGranted('ROLE_USER')]
#[Route('/home', name: 'app_home')]
public function index(): Response
{
    return $this->render('home/index.html.twig');
}

🎨 Étape 7 : Créer le template de base

Créez templates/base.html.twig avec une structure de sidebar pour l'interface d'administration, incluant :

  • Navigation vers les différentes sections (Catégories, Produits, Commandes, etc.)
  • Bouton de déconnexion
  • Intégration Bootstrap et Font Awesome

📝 Étape 8 : Migrations supplémentaires

Si vous ajoutez des champs après la création initiale (comme isVerified pour User), créez une nouvelle migration :

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

🗂️ Étape 9 : Générer des données de test avec Doctrine Fixtures

Installer les packages nécessaires

composer require --dev orm-fixtures
composer require --dev fakerphp/faker

Créer les fixtures avec make:fixtures

php bin/console make:fixtures CategoryFixtures
php bin/console make:fixtures ProductFixtures
php bin/console make:fixtures UserFixtures
php bin/console make:fixtures OrderFixtures
php bin/console make:fixtures OrderItemFixtures

Structure des fixtures

Les fixtures sont créées dans src/DataFixtures/ et permettent de générer des données de test pour votre application.

CategoryFixtures : Crée 8 catégories prédéfinies (Électronique, Vêtements, Maison & Jardin, etc.)

ProductFixtures : Génère 50 produits avec Faker

  • Nom, description, prix, stock générés aléatoirement
  • Images optionnelles (70% de chance)
  • Association à 1 catégorie aléatoire
  • Dates de création et mise à jour réalistes

UserFixtures : Génère 21 utilisateurs (1 admin + 20 utilisateurs)

  • Admin : admin@example.com / admin123 avec ROLE_ADMIN
  • Utilisateurs : emails, noms, adresses générés avec Faker
  • Mots de passe hashés avec UserPasswordHasherInterface
  • 80% des utilisateurs vérifiés

OrderFixtures : Génère 30 commandes

  • Numéros de commande au format CMD-####-####
  • Statuts aléatoires : pending, processing, shipped, delivered, cancelled
  • Dates entre les 6 derniers mois
  • Total initialisé à 0 (recalculé dans OrderItemFixtures)

OrderItemFixtures : Génère les articles de commande

  • 1 à 5 articles par commande
  • Quantité entre 1 et 3 par article
  • Prix récupéré du produit
  • Recalcule le total de chaque commande

Gérer les dépendances entre fixtures

Utilisez DependentFixtureInterface pour définir l'ordre de chargement :

use DoctrineCommonDataFixturesDependentFixtureInterface;

class ProductFixtures extends Fixture implements DependentFixtureInterface
{
    public function getDependencies(): array
    {
        return [
            CategoryFixtures::class,
        ];
    }
}

Charger les fixtures

# Charger toutes les fixtures (vide la base avant)
php bin/console doctrine:fixtures:load

# Ajouter les fixtures sans vider la base
php bin/console doctrine:fixtures:load --append

# Charger un groupe spécifique
php bin/console doctrine:fixtures:load --group=CategoryFixtures

Concepts importants

$manager->persist($entity) : Prépare l'entité pour la sauvegarde (mise en file d'attente)

$manager->flush() : Exécute toutes les requêtes SQL en base de données

$this->addReference('name', $entity) : Stocke une référence à une entité pour la récupérer dans d'autres fixtures avec $this->getReference('name')

Faker : Bibliothèque pour générer des données réalistes (noms, adresses, dates, etc.)

Exemple de fixture complète

<?php

namespace AppDataFixtures;

use AppEntityCategory;
use DoctrineBundleFixturesBundleFixture;
use DoctrinePersistenceObjectManager;

class CategoryFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $categories = [
            ['name' => 'Électronique', 'description' => 'Appareils électroniques'],
            ['name' => 'Vêtements', 'description' => 'Mode et habillement'],
        ];

        foreach ($categories as $categoryData) {
            $category = new Category();
            $category->setName($categoryData['name']);
            $category->setDescription($categoryData['description']);
            $category->setSlug(strtolower(str_replace(' ', '-', $categoryData['name'])));
            $category->setCreatedAt(new DateTimeImmutable());
            $category->setUpdatedAt(new DateTimeImmutable());

            $manager->persist($category);
        }

        $manager->flush();
    }
}

🏠 Étape 10 : Créer la page d'accueil publique

Créer le controller Accueil

Créez src/Controller/AccueilController.php :

<?php

namespace AppController;

use AppRepositoryProductRepository;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAttributeRoute;

final class AccueilController extends AbstractController
{
    #[Route('/', name: 'app_accueil')]
    public function index(ProductRepository $productRepository): Response
    {
        $products = $productRepository->findAll();

        return $this->render('accueil/index.html.twig', [
            'products' => $products,
        ]);
    }
}

Points importants :

  • Route / : Page d'accueil accessible à tous (pas de protection)
  • Injection de ProductRepository : Récupère tous les produits
  • Template accueil/index.html.twig : Affiche les produits en grille

Créer le template de base pour l'accueil

Créez templates/base_accueil.html.twig :

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Accueil{% endblock %}</title>
    
    {% block stylesheets %}
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
        <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet">
        <style>
            body.page-accueil {
                min-height: 100vh;
                display: flex;
                flex-direction: column;
                margin: 0;
                background-color: #f8f9fa;
            }
            body.page-accueil .navbar {
                background-color: #fff;
                box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            }
            body.page-accueil .product-card {
                transition: transform 0.3s ease, box-shadow 0.3s ease;
                height: 100%;
            }
            body.page-accueil .product-card:hover {
                transform: translateY(-5px);
                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            }
            body.page-accueil .product-image {
                height: 200px;
                object-fit: cover;
                width: 100%;
            }
            body.page-accueil main {
                flex: 1;
            }
            body.page-accueil .footer {
                background-color: #343a40;
                color: #fff;
                padding: 40px 0;
                margin-top: auto;
            }
        </style>
    {% endblock %}
</head>
<body class="page-accueil">
    <nav class="navbar navbar-expand-lg navbar-light">
        <div class="container">
            <a class="navbar-brand fw-bold" href="{{ path('app_accueil') }}">E-Commerce</a>
            <div class="collapse navbar-collapse">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ path('app_accueil') }}">Accueil</a>
                    </li>
                    {% if app.user %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{ path('app_home') }}">Dashboard</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ path('app_logout') }}">Déconnexion</a>
                        </li>
                    {% else %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{ path('app_login') }}">Connexion</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ path('app_register') }}">Inscription</a>
                        </li>
                    {% endif %}
                </ul>
            </div>
        </div>
    </nav>

    <main>
        {% block body %}{% endblock %}
    </main>

    <footer class="footer">
        <div class="container text-center">
            <p class="mb-0">© 2026 E-Commerce. Tous droits réservés.</p>
        </div>
    </footer>
</body>
</html>

Caractéristiques du template :

  • Classe page-accueil : Scopage des styles CSS pour éviter les conflits avec base.html.twig
  • Flexbox layout : Footer toujours en bas de page même si le contenu est court
  • Navigation conditionnelle : Affiche Dashboard/Déconnexion si connecté, Connexion/Inscription sinon
  • Responsive : Bootstrap pour l'adaptation mobile

Créer la vue accueil

Créez templates/accueil/index.html.twig :

{% extends 'base_accueil.html.twig' %}

{% block title %}Accueil - Produits{% endblock %}

{% block body %}
<div class="container my-5">
    <div class="row mb-4">
        <div class="col-12">
            <h1 class="display-4 text-center mb-4">Nos Produits</h1>
            <p class="text-center text-muted">Découvrez notre sélection de produits</p>
        </div>
    </div>

    <div class="row g-4">
        {% for product in products %}
            <div class="col-md-4 col-lg-3">
                <div class="card product-card">
                    {% if product.image %}
                        <img src="{{ product.image }}" class="card-img-top product-image" alt="{{ product.name }}">
                    {% else %}
                        <div class="card-img-top product-image bg-secondary d-flex align-items-center justify-content-center">
                            <i class="fas fa-image fa-3x text-white"></i>
                        </div>
                    {% endif %}
                    <div class="card-body d-flex flex-column">
                        <h5 class="card-title">{{ product.name }}</h5>
                        <p class="card-text text-muted flex-grow-1">
                            {% if product.description %}
                                {{ product.description|length > 100 ? product.description|slice(0, 100) ~ '...' : product.description }}
                            {% else %}
                                Aucune description disponible
                            {% endif %}
                        </p>
                        <div class="mt-auto">
                            <div class="d-flex justify-content-between align-items-center mb-3">
                                <span class="h5 text-primary mb-0">{{ product.price }} €</span>
                                {% if product.stock > 0 %}
                                    <span class="badge bg-success">En stock ({{ product.stock }})</span>
                                {% else %}
                                    <span class="badge bg-danger">Rupture de stock</span>
                                {% endif %}
                            </div>
                            <div class="d-grid gap-2">
                                <a href="{{ path('app_product_show', {'id': product.id}) }}" class="btn btn-primary">
                                    <i class="fas fa-eye me-2"></i>Voir les détails
                                </a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        {% else %}
            <div class="col-12">
                <div class="alert alert-info text-center">
                    <i class="fas fa-info-circle me-2"></i>
                    Aucun produit disponible pour le moment.
                </div>
            </div>
        {% endfor %}
    </div>
</div>
{% endblock %}

Fonctionnalités de la vue :

  • Grille de produits : Affichage en cartes Bootstrap (4 colonnes sur desktop, responsive)
  • Images produits : Affiche l'image si disponible, sinon icône placeholder
  • Description tronquée : Limite à 100 caractères avec "..."
  • Badge de stock : Vert si en stock, rouge si rupture
  • Lien vers détails : Bouton vers la page de détail du produit
  • Message si vide : Affiche un message si aucun produit

Différences entre base.html.twig et base_accueil.html.twig

base.html.twig (Interface d'administration) :

  • Sidebar fixe à gauche
  • Navigation verticale
  • Protégé par ROLE_USER
  • Utilisé pour les pages CRUD

base_accueil.html.twig (Page publique) :

  • Navigation horizontale en haut
  • Footer en bas
  • Accessible à tous (pas de protection)
  • Utilisé pour la page d'accueil publique

Points importants

  • Scopage CSS : Utilisez body.page-accueil pour éviter les conflits de styles
  • Flexbox pour footer : display: flex + flex-direction: column + margin-top: auto sur le footer
  • Responsive : Utilisez les classes Bootstrap col-md-4 col-lg-3 pour l'adaptation mobile
  • Performance : Considérez la pagination si vous avez beaucoup de produits

✅ Résumé

Vous avez maintenant créé une application e-commerce complète avec :

  • Entités : Category, Product (ManyToMany), User, Order, OrderItem
  • CRUD complets : Générés avec make:crud pour toutes les entités
  • Formulaires adaptés : ProductType avec sélection multiple de catégories
  • Authentification : Login, logout, inscription avec vérification d'email
  • Sécurité : Protection des routes CRUD par ROLE_ADMIN, route home par ROLE_USER
  • Interface d'administration : Templates générés avec sidebar de navigation
  • Fixtures : Génération de données de test avec Doctrine Fixtures et Faker
  • Page d'accueil publique : Controller Accueil avec affichage des produits en grille

Structure des contrôleurs

Tous les contrôleurs CRUD suivent la même structure :

  • index() : Liste toutes les entités
  • new() : Crée une nouvelle entité
  • show() : Affiche une entité
  • edit() : Modifie une entité
  • delete() : Supprime une entité (méthode POST avec vérification CSRF)

Points importants sur la relation ManyToMany

  • Un produit peut avoir plusieurs catégories : Un produit peut être classé dans plusieurs catégories simultanément
  • Une catégorie peut contenir plusieurs produits : Une catégorie peut regrouper de nombreux produits
  • Table de jointure automatique : Doctrine crée automatiquement la table product_category pour gérer la relation
  • Méthodes de gestion : Utilisez addCategory() et removeCategory() pour gérer les catégories d'un produit

Exemple d'utilisation dans un formulaire :

// Dans ProductType.php, le champ categories est automatiquement généré :
->add('categories', EntityType::class, [
    'class' => Category::class,
    'choice_label' => 'name', // Adapté pour afficher le nom
    'multiple' => true,
    'expanded' => false, // ou true pour des checkboxes
])

Prochaines étapes possibles :

  • Ajouter un système de panier (session)
  • Intégration d'un système de paiement (Stripe, PayPal)
  • Gestion des images produits (upload)
  • Système de recherche avancée avec filtres par catégories multiples
  • Avis et notes produits
  • Gestion des stocks en temps réel
  • Export/Import de produits avec leurs catégories

🔗 Ressources