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_categorypour gérer la relation
Structure de la table de jointure :
product_id(clé étrangère versproduct)category_id(clé étrangère verscategory)- 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 :
RegistrationControlleravec gestion de l'inscription et vérification d'emailRegistrationFormTypeavec les champs : firstName, lastName, email, agreeTerms, plainPasswordEmailVerifierpour 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 :
firstNameetlastName(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 :
CategoryControlleravec les actions : index, new, show, edit, deleteCategoryType(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 :
ProductControlleravec les actions : index, new, show, edit, deleteProductType(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 :
OrderControlleravec les actions : index, new, show, edit, deleteOrderType(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 :
OrderItemControlleravec les actions : index, new, show, edit, deleteOrderItemType(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
categoriesest automatiquement généré enEntityTypeavecmultiple => true - Vous pouvez améliorer l'affichage en changeant
choice_labeldeidàname
Dans OrderType.php :
- Le champ
userest automatiquement généré enEntityType - Vous pouvez améliorer l'affichage en changeant
choice_labeldeidàemail
Dans OrderItemType.php :
- Les champs
orderIdetproductsont automatiquement générés enEntityType - Vous pouvez améliorer l'affichage en changeant
choice_labeldeidà 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/admin123avecROLE_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 avecbase.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-accueilpour éviter les conflits de styles - Flexbox pour footer :
display: flex+flex-direction: column+margin-top: autosur le footer - Responsive : Utilisez les classes Bootstrap
col-md-4 col-lg-3pour 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:crudpour 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 parROLE_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ésnew(): 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_categorypour gérer la relation - Méthodes de gestion : Utilisez
addCategory()etremoveCategory()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
