Vous travaillez pour MediConnect, une startup qui développe une plateforme numérique de gestion de consultations médicales pour les maisons de santé pluriprofessionnelles. La plateforme doit permettre aux structures de santé de coordonner leurs consultations (médecine générale, spécialités, téléconsultations, urgences) et aux patients de les trouver facilement.
Le système doit gérer :
- Consultations avec leurs détails et planning
- Cabinets médicaux (centres de santé, cabinets libéraux, cliniques)
- Praticiens (médecins, infirmiers, kinésithérapeutes, psychologues)
- Spécialités médicales pour faciliter la recherche
Votre mission : développer le module de recherche et de manipulation des données en utilisant les Repository Pattern, DQL, Query Builder, et les techniques d'optimisation de performance.
Compétences techniques visées :
- Maîtriser le Repository Pattern et créer des repositories personnalisés
- Construire des requêtes complexes avec DQL et Query Builder
- Optimiser les performances en évitant les problèmes N+1
- Implémenter des systèmes de recherche et de filtrage avancés
- Utiliser les techniques de pagination et de cache
- Analyser et optimiser les performances avec le Profiler Doctrine
Compétences transversales :
- Concevoir des interfaces de recherche intuitives
- Structurer le code selon les bonnes pratiques du Repository Pattern
- Diagnostiquer et résoudre les problèmes de performance
Créez un nouveau projet Symfony et installez les dépendances :
symfony new symfony-tp4-repository-doctrine --webappCréez les quatre entités de base avec la commande make:entity. Pour cette phase, nous allons nous concentrer sur Cabinet et Praticien.
Entité Cabinet :
symfony console make:entity CabinetAjoutez les propriétés suivantes :
nom: string, 200 caractèresadresse: string, 255 caractèresville: string, 100 caractèrescapaciteAccueil: integertypeCabinet: string, 50 caractères (centres de santé, cabinets libéraux, cliniques)description: text, nullable
Entité Praticien :
symfony console make:entity PraticienAjoutez les propriétés suivantes :
nom: string, 200 caractèresemail: string, 180 caractèrestelephone: string, 14 caractères, nullable (format souhaite '01-02-03-04-05')numeroOrdre: string, 255 caractères, nullabledescription: text, nullable
Entité SpecialiteMedicale :
symfony console make:entity SpecialiteMedicaleAjoutez les propriétés suivantes :
nom: string, 100 caractèrescouleur: string, 7 caractèresicone: string, 50 caractèresdescription: text, nullable
Entité Consultation :
symfony console make:entity ConsultationAjoutez les propriétés suivantes :
titre: string, 250 caractèresdescription: textdateDebut: datetimedateFin: datetimetarif: decimal (10,2), nullablecreneauxDisponibles: integerestPublie: booleandateCreation: datetime
Modifiez l'entité Consultation pour ajouter les relations :
symfony console make:entity ConsultationAjoutez les relations bidirectionnelles suivantes :
cabinet: relation ManyToOne vers Cabinetpraticien: relation ManyToOne vers Praticienspecialite: relation ManyToOne vers SpecialiteMedicale
Modifiez l'entité Praticien pour ajouter la relation :
symfony console make:entity PraticienAjoutez les relations bidirectionnelles suivantes :
specialite: relation ManyToOne vers SpecialiteMedicale
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 cabinets différents dans 3 villes
- 3 praticiens de types différents
- 6 spécialités médicales
- 20 consultations variées (passées, présentes, futures)
Créez le contrôleur pour gérer les cabinets :
symfony console make:controller CabinetControllerImplémenter les fonctions suivantes :
<?php
namespace App\Controller;
use App\Entity\Cabinet;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Faker\Factory;
#[Route('/cabinet')]
class CabinetController extends AbstractController
{
#[Route('/', name: 'cabinet_index')]
public function index(EntityManagerInterface $em): Response
{
// Récupération de tous les cabinets avec la méthode findAll()
// retourne la vue twig associée avec les cabinets
}
#[Route('/create', name: 'cabinet_create')]
public function create(EntityManagerInterface $em): Response
{
// Utilisation de Faker pour générer des données aléatoires
// Persist et flush pour sauvegarder en base
// Message flash pour confirmer la création
// Redirection vers l'index
}
#[Route('/{id}', name: 'cabinet_show', requirements: ['id' => '\d+'])]
public function show(int $id, EntityManagerInterface $em): Response
{
// Récupération d'un cabinet par son ID avec find()
// Gestion de l'ID n'existant pas
// Retourne la vue twig associée au detail d'un cabinet
}
#[Route('/{id}/update', name: 'cabinet_update', requirements: ['id' => '\d+'])]
public function update(int $id, EntityManagerInterface $em): Response
{
$faker = Factory::create('fr_FR');
// Récupération du cabinet (l'entité devient automatiquement "Managed")
// Gestion de l'ID n'existant pas
// Modification aléatoire de certaines propriétés
// PAS BESOIN de persist() pour une entité existante !
// Le dirty checking de Doctrine détecte automatiquement les modifications, mais il faut quand les flush
// Message flash pour confirmer la modification
// Redirection vers la page de visualisation
}
#[Route('/{id}/delete', name: 'cabinet_delete', requirements: ['id' => '\d+'])]
public function delete(int $id, EntityManagerInterface $em): Response
{
// Récupération du cabinet
// Gestion de l'ID n'existant pas
// Extraction du nom du cabinet pour le message flash
// Suppression de l'entité
// Message flash pour confirmer la suppression
// Redirection vers l'index
}
}Mission : Sur le modèle du CabinetController, créez un PraticienController complet avec :
- Route
/praticien- Index listant tous les praticiens - Route
/praticien/create- Création automatique d'un praticien avec Faker - Route
/praticien/{id}- Affichage des détails d'un praticien - Route
/praticien/{id}/update- Mise à jour aléatoire (email, téléphone, description) - Route
/praticien/{id}/delete- Suppression d'un praticien
Créez un repository personnalisé pour l'entité Consultation avec les méthodes suivantes :
class ConsultationRepository extends ServiceEntityRepository
{
/**
* Trouve les consultations publiées et à venir
*/
public function findConsultationsAvenir(): array
{
// À implémenter avec QueryBuilder
}
/**
* Trouve les consultations par spécialité
*/
public function findBySpecialite(SpecialiteMedicale $specialite): array
{
// À implémenter
}
/**
* Trouve les consultations dans une ville donnée
*/
public function findByVille(string $ville): array
{
// À implémenter avec jointure sur Cabinet
}
/**
* Compte le nombre de consultations publiées
*/
public function countConsultationsPubliees(): int
{
// À implémenter
}
}Créez un repository pour l'entité Cabinet :
class CabinetRepository extends ServiceEntityRepository
{
/**
* Trouve les cabinets par type avec le nombre de consultations
*/
public function findCabinetsAvecNombreConsultations(): array
{
// À implémenter avec LEFT JOIN et GROUP BY
}
/**
* Trouve les cabinets les plus actifs (avec le plus de consultations)
*/
public function findCabinetsLesPlusActifs(int $limit = 5): array
{
// À implémenter
}
}Mission autonome : Implémentez ces méthodes en utilisant le QueryBuilder. Testez-les avec des données factices créées manuellement.
Implémentez une méthode de recherche avancée dans ConsultationRepository :
/**
* Recherche de consultations avec filtres multiples
*
* @param array $criteres [
* 'terme' => string|null, // Recherche dans titre et description
* 'specialite' => SpecialiteMedicale|null,
* 'ville' => string|null,
* 'dateDebut' => DateTime|null, // À partir de cette date
* 'dateFin' => DateTime|null, // Jusqu'à cette date
* 'tarifMax' => float|null, // Tarif maximum
* 'gratuit' => bool|null // Seulement les consultations gratuites
* ]
*/
public function rechercherConsultations(array $criteres = []): array
{
$qb = $this->createQueryBuilder('c')
->leftJoin('c.cabinet', 'cab')
->leftJoin('c.specialite', 's')
->leftJoin('c.praticien', 'p');
// Ajouter les conditions dynamiquement selon les critères
// Utiliser addSelect() pour optimiser les jointures
return $qb->getQuery()->getResult();
}Afin d'accéder à certaines fonctions liées aux dates, il est necéssaire d'installer une extension doctrine ajoutant des fonctions au DQL de base:
https://github.com/beberlei/DoctrineExtensions
composer require beberlei/doctrineextensionsModifier ensuite le fichier config/packages/doctrine.yaml afin d'ajouter vos fonctions:
doctrine:
# ...
orm:
# ...
dql:
datetime_functions:
month: DoctrineExtensions\Query\Mysql\Month
year: DoctrineExtensions\Query\Mysql\YearCréez des requêtes pour générer des statistiques :
/**
* Statistiques des consultations par mois
*/
public function getStatistiquesParMois(int $annee): array
{
// Utiliser les fonctions DATE() de DQL
// Retourner un tableau avec mois => nombre de consultations
}
/**
* Top 10 des praticiens les plus actifs
*/
public function getTopPraticiens(int $limit = 10): array
{
// Jointure avec GROUP BY et ORDER BY COUNT
}Implémentez une méthode qui évite le problème N+1 :
/**
* Récupère les consultations avec toutes leurs relations
* Optimisé pour éviter le problème N+1
*/
public function findConsultationsAvecRelations(): array
{
// Utiliser plusieurs addSelect() pour charger :
// - cabinet, praticien, specialite en une seule requête
}Créez une route /api/recherche qui retourne du JSON pour une utilisation AJAX :
/**
* @Route("/api/recherche", name="api_recherche", methods={"GET"})
*/
public function apiRecherche(Request $request, ConsultationRepository $repo): JsonResponse
{
$terme = $request->query->get('q', '');
$ville = $request->query->get('ville');
$specialite = $request->query->get('specialite');
$criteres = array_filter([
'terme' => $terme ?: null,
'ville' => $ville ?: null,
'specialite' => $specialite ? $this->getSpecialiteById($specialite) : null
]);
$consultations = $repo->rechercherConsultations($criteres);
// Sérialiser en JSON avec les données essentielles
$data = array_map(function($consultation) {
return [
'id' => $consultation->getId(),
'titre' => $consultation->getTitre(),
'date' => $consultation->getDateDebut()->format('Y-m-d H:i'),
'cabinet' => $consultation->getCabinet()->getNom(),
'ville' => $consultation->getCabinet()->getVille(),
'tarif' => $consultation->getTarif()
];
}, $consultations);
return new JsonResponse($data);
}