Vous êtes développeur web dans une startup EdTech nommée SkillBoost Academy. L'entreprise souhaite créer une plateforme innovante de formation en ligne permettant aux formateurs de proposer leurs cours et aux apprenants de s'inscrire facilement.
Le système doit gérer :
- Formations avec leurs détails, programmes et tarifs
- Utilisateurs (formateurs et apprenants)
- Inscriptions des apprenants aux formations
- Catégories de formation pour faciliter la recherche
Votre mission : développer les interfaces de saisie avec formulaires et validation en utilisant le système de formulaires Symfony avec des contraintes de validation personnalisées pour garantir la qualité des données éducatives.
Compétences techniques visées :
- Maîtriser le système de formulaires Symfony (FormBuilder, FormType)
- Implémenter la validation avancée avec contraintes personnalisées (relativement simple, juste pour démontrer leur existence et utilisation)
Compétences transversales :
- Concevoir des formulaires intuitifs adaptés au contexte éducatif
- Structurer le code selon les bonnes pratiques Symfony
- Garantir la qualité et la cohérence des données métier
Créez un nouveau projet Symfony et installez les dépendances :
symfony new symfony-tp5-formulaires --webappConfiguration de la base de données dans .env :
DATABASE_URL="mysql://root:@127.0.0.1:3306/skillboost_academy?serverVersion=8.0"
Créez les quatre entités de base avec la commande make:entity.
Entité Categorie :
symfony console make:entity CategorieAjoutez les propriétés suivantes :
nom: string, 100 caractèresslug: string, 100 caractères (version URL-friendly du nom)description: text, nullablecouleur: string, 7 caractères (format hex #RRGGBB)icone: string, 50 caractères
Entité User :
symfony console make:entity UserAjoutez les propriétés suivantes :
email: string, 180 caractères, uniqueroles: json (stocke un tableau de rôles)password: string, 255 caractèresprenom: string, 100 caractèresnom: string, 100 caractèrestelephone: string, 20 caractères, nullabledateNaissance: date, nullableentreprise: string, 255 caractères, nullableposte: string, 100 caractères, nullablebio: text, nullablephotoProfil: string, 255 caractères, nullabledateCreation: datetimeestActif: boolean
Entité Formation :
symfony console make:entity FormationAjoutez les propriétés suivantes :
titre: string, 250 caractèresslug: string, 250 caractèresdescription: textprogramme: textobjectifs: text, nullableprerequis: text, nullableduree: integer (durée en heures)niveau: string, 50 caractères (débutant, intermédiaire, avancé)prix: decimal (10,2)capaciteMax: integermodalite: string, 50 caractères (présentiel, distanciel, hybride)dateDebut: datedateFin: dateestPublie: booleandateCreation: datetime
Entité Inscription :
symfony console make:entity InscriptionAjoutez les propriétés suivantes :
dateInscription: datetimestatut: string, 50 caractères (en_attente, confirmee, annulee, terminee)modePaiement: string, 50 caractères, nullable (carte, virement, cheque)commentaire: text, nullable (besoins spécifiques de l'apprenant)noteSatisfaction: integer, nullable (de 1 à 5)commentaireSatisfaction: text, nullable
Modifiez l'entité Formation pour ajouter les relations :
symfony console make:entity FormationAjoutez les relations bidirectionnelles suivantes :
categorie: relation ManyToOne vers Categorieformateur: relation ManyToOne vers User (le formateur)
Modifiez l'entité Inscription pour ajouter les relations :
symfony console make:entity InscriptionAjoutez les relations bidirectionnelles suivantes :
formation: relation ManyToOne vers Formationapprenant: relation ManyToOne vers User (l'apprenant)
Générez la migration et mettez à jour la base de données :
symfony console make:migration
symfony console doctrine:migrations:migrateCréez des fixtures pour peupler votre base avec des données de test :
composer require --dev orm-fixtures fakerphp/fakerCréez au minimum :
- 5 catégories de formation (Développement web, Data Science, Design, Marketing, Gestion de projet)
- 10 utilisateurs (5 formateurs et 5 apprenants)
- 15 formations variées avec différentes modalités et niveaux
- 25 inscriptions avec différents statuts
Créez le contrôleur pour gérer les catégories :
symfony console make:crud CategorieMission : Sur le modèle du CategorieController, créez un UserController complet avec :
- Route
/user- Index listant tous les utilisateurs - Route
/user/create- Création automatique d'un utilisateur avec Faker - Route
/user/{id}- Affichage des détails d'un utilisateur - Route
/user/{id}/update- Mise à jour aléatoire (email, téléphone, bio) - Route
/user/{id}/delete- Suppression d'un utilisateur
Mission : Sur le modèle du formulaire Categorie, créez un formulaire complet pour les utilisateurs
Créez les éléments suivants :
- FormType :
UserTypeavec tous les champs (email, prenom, nom, telephone, dateNaissance, entreprise, poste, bio) - Contrôleur : Modifiez
UserControllerpour ajouter les actionsnew,edit,deleteutilisant le formulaire - Templates : Créez
user/new.html.twig,user/edit.html.twiget mettez à jouruser/index.html.twigetuser/show.html.twig
Points d'attention :
- Pour les champs : utilisez
EmailType,TextType,TelType,DateType,TextareaType - N'incluez pas le champ
passworddans ce formulaire (on le traitera séparément avec un formulaire d'inscription) - N'incluez pas les champs
roles,photoProfil,dateCreation,estActif(gérés automatiquement) - Ajoutez des labels et placeholders appropriés
Mission : Ajoutez des contraintes de validation dans l'entité Categorie
Modifiez src/Entity/Categorie.php en ajoutant les imports et contraintes :
nom: NotBlank, Length (min: 3, max: 100)slug: NotBlank, Regex (/^[a-z0-9]+(?:-[a-z0-9]+)*$/), UniqueEntitydescription: Length (max: 1000) - optionnelcouleur: NotBlank, Regex (/^#[0-9A-Fa-f]{6}$/)icone: NotBlank, Regex (/^fa-[\w-]+$/)
Testez votre formulaire en essayant de soumettre des données invalides :
- Slug avec majuscules ou espaces
- Couleur mal formatée
- Nom trop court
- Icône invalide
Mission : Ajoutez les contraintes de validation sur l'entité User
email: NotBlank, Email, UniqueEntityprenom: NotBlank, Length (min: 2, max: 100)nom: NotBlank, Length (min: 2, max: 100)telephone: Regex (/^[0-9\-\+\s\(\)\.]+$/) - optionneldateNaissance: LessThan ('today'), GreaterThan ('-120 years') - optionnelentreprise: Length (max: 255) - optionnelposte: Length (max: 100) - optionnelbio: Length (max: 1000) - optionnel
Mission : Créez une contrainte personnalisée pour valider les dates de formation
Créez le fichier src/Validator/FormationDatesValides.php (l'attribut de contrainte) :
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class FormationDatesValides extends Constraint
{
public string $messageFinAvantDebut = 'La date de fin doit être postérieure à la date de début';
public string $messageDebutDansLePasse = 'La date de début ne peut pas être dans le passé';
public string $messageDureeInconsistante = 'La durée de la formation semble incohérente avec les dates fournies';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}Créez le fichier src/Validator/FormationDatesValidesValidator.php (le validateur) :
<?php
namespace App\Validator;
use App\Entity\Formation;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class FormationDatesValidesValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint): void
{
if (!$constraint instanceof FormationDatesValides) {
throw new UnexpectedTypeException($constraint, FormationDatesValides::class);
}
if (!$value instanceof Formation) {
return;
}
$dateDebut = $value->getDateDebut();
$dateFin = $value->getDateFin();
$duree = $value->getDuree();
if (!$dateDebut || !$dateFin) {
return; // Les champs NotBlank se chargeront de ces validations
}
// Vérifier que la date de fin est après la date de début
if ($dateFin <= $dateDebut) {
$this->context->buildViolation($constraint->messageFinAvantDebut)
->atPath('dateFin')
->addViolation();
}
// Vérifier que la date de début n'est pas dans le passé
$aujourdhui = new \DateTime('today');
if ($dateDebut < $aujourdhui) {
$this->context->buildViolation($constraint->messageDebutDansLePasse)
->atPath('dateDebut')
->addViolation();
}
// Vérifier la cohérence de la durée (avec une tolérance)
if ($duree) {
// Calcul du nombre de jours entre les dates
$interval = $dateDebut->diff($dateFin);
$joursFormation = $interval->days;
// On suppose une formation de 7h par jour en moyenne
$heuresEstimees = $joursFormation * 7;
// Tolérance de 50% (la durée réelle peut varier selon l'intensité)
$toleranceBasse = $heuresEstimees * 0.5;
$toleranceHaute = $heuresEstimees * 1.5;
if ($duree < $toleranceBasse || $duree > $toleranceHaute) {
$this->context->buildViolation($constraint->messageDureeInconsistante)
->atPath('duree')
->addViolation();
}
}
}
}Mission : Ajoutez les contraintes sur l'entité Formation
titre: NotBlank, Length (min: 10, max: 250)slug: NotBlank, Regex (/^[a-z0-9]+(?:-[a-z0-9]+)*$/)description: NotBlank, Length (min: 50)programme: NotBlank, Length (min: 50)objectifs: (pas de contraintes) - optionnelprerequis: (pas de contraintes) - optionnelduree: NotBlank, Positive, Range (min: 1, max: 1000) - en heuresniveau: NotBlank, Choice (debutant, intermediaire, avance)prix: NotBlank, PositiveOrZero, Range (max: 10000)capaciteMax: NotBlank, Positive, Range (min: 1, max: 100)modalite: NotBlank, Choice (presentiel, distanciel, hybride)dateDebut: NotBlankdateFin: NotBlankcategorie: NotNull (relation)formateur: NotNull (relation)- Contrainte de classe : FormationDatesValides (personnalisée)
Mission : Créez un formulaire complet pour créer et modifier une formation
Créez les éléments suivants :
- FormType :
FormationTypeavec tous les champs - Contrôleur :
FormationControlleravec actions CRUD complètes (index, new, show, edit, delete) - Templates : Tous les templates nécessaires
Points d'attention particuliers :
- Utilisez
EntityTypepour les relations (categorie, formateur) - Utilisez
DateTypepour les dates avec widget approprié - Utilisez
MoneyTypepour le prix - Utilisez
ChoiceTypepour niveau et modalite - Utilisez
IntegerTypepour duree et capaciteMax - Utilisez
CheckboxTypepour estPublie - Testez que la contrainte personnalisée
FormationDatesValidesfonctionne bien
Cas de test pour la contrainte personnalisée :
- Essayez de créer une formation avec dateFin avant dateDebut
- Essayez de créer une formation avec dateDebut dans le passé
- Essayez de créer une formation où la durée est incohérente avec les dates
Mission : Créez un template personnalisé pour l'affichage des erreurs
Créez le fichier templates/form/custom_errors.html.twig :
{% block form_errors %}
{% if errors|length > 0 %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong><i class="fas fa-exclamation-triangle"></i> Erreur(s) de validation :</strong>
<ul class="mb-0 mt-2">
{% for error in errors %}
<li>{{ error.message }}</li>
{% endfor %}
</ul>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% endblock %}Pour utiliser ce template personnalisé, dans vos templates de formulaires :
{% form_theme form 'form/custom_errors.html.twig' %}# Création de FormType
symfony console make:form
# Création de contrainte de validation personnalisée
symfony console make:validator
# Création d'entité
symfony console make:entity
# Création de contrôleur
symfony console make:controller
# Migration
symfony console make:migration
symfony console doctrine:migrations:migrate
# Chargement des fixtures
symfony console doctrine:fixtures:loadTextType: Champ texte simpleEmailType: Champ email avec validation HTML5TelType: Champ téléphoneIntegerType: Nombre entierMoneyType: Montant monétaireDateType: Sélection de dateTextareaType: Zone de texte multi-lignesChoiceType: Liste déroulante ou boutons radioEntityType: Sélection d'une entité DoctrineCheckboxType: Case à cocherColorType: Sélecteur de couleur
@Assert\NotBlank: Champ non vide@Assert\Length: Longueur min/max@Assert\Email: Format email@Assert\Regex: Expression régulière@Assert\Range: Valeur dans un intervalle@Assert\Positive/@Assert\PositiveOrZero: Nombre positif@Assert\Choice: Valeur parmi une liste@Assert\LessThan/@Assert\GreaterThan: Comparaison@UniqueEntity: Unicité en base de données
