Construction du questionnaire template : Explication Détaillée de l'Implémentation
La solution décidée est implémentée en trois couches pour respecter le principe de la Séparation des Responsabilités : le Repository, le Service d'Ordonnancement, et le Contrôleur/Sérialisation.
Étape 1 : Préparation de la Modélisation (Relations Inverses)
Pour que l'Algorithme de Kahn puisse identifier facilement les dépendances (qui est le parent ?), nous devons garantir que la relation inverse est disponible.
-
Ajouter
incomingTransitionssur l'entitéQuestion:// Dans App\Entity\Question
/**
* Liste des transitions qui mènent à cette question (les parents/prérequis)
* @var Collection<int, Transition>
*/
#[ORM\OneToMany(targetEntity: Transition::class, mappedBy: 'childQuestion')]
private Collection $incomingTransitions;
// ...
// Assurez-vous que l'entité Transition a bien une relation inversée vers la Question parent via PossibleAnswer.
Étape 2 : Le Repository (Fetching Efficace en 2 Requêtes)
Nous utilisons deux requêtes pour minimiser la complexité SQL et la charge d'hydratation.
Méthode 1 : Récupérer les Questions (Objets Complets)
// Dans App\Repository\QuestionnaireTemplateRepository
/**
* Récupère les Questions et leurs relations (réponses/transitions) en objets Doctrine.
*/
public function findQuestionsAndAnswers(int $questionnaireId): array
{
return $this->getEntityManager()->createQueryBuilder()
->select('q', 'pa', 't')
->from(Question::class, 'q')
->leftJoin('q.possibleAnswers', 'pa')
->leftJoin('pa.transitions', 't') // Relations sortantes
// Optionnel mais utile pour le tri :
->leftJoin('q.incomingTransitions', 'in_t') // Relations entrantes (parents)
->where('q.questionnaireTemplate = :id')
->setParameter('id', $questionnaireId)
->getQuery()
->getResult(); // Retourne un tableau d'objets Question hydratés
}
Méthode 2 : Récupérer les Transitions (Tableau d'IDs)
// Dans App\Repository\TransitionRepository (ou la même classe si c'est plus pratique)
/**
* Récupère les liens de dépendance (transitions) uniquement pour le tri topologique.
* On utilise un array result pour la légèreté.
*/
public function findFlatTransitionLinks(int $questionnaireId): array
{
return $this->getEntityManager()->createQueryBuilder()
->select('pa.id as source_id', 'cq.id as target_id')
->from(Transition::class, 't')
->join('t.possibleAnswer', 'pa') // La réponse (Question parent) qui déclenche la transition
->join('t.childQuestion', 'cq') // La question enfant (cible)
->where('pa.questionnaireTemplate = :id') // Assurez-vous que le lien vers le QuestionnaireTemplate est possible
->setParameter('id', $questionnaireId)
->getQuery()
->getArrayResult(); // Retourne un tableau léger : [['source_id' => 1, 'target_id' => 2], ...]
}
Étape 3 : Le Service d'Ordonnancement (Algorithme de Kahn)
Ce service reçoit les données brutes et produit la liste ordonnée.
// src/Service/QuestionnaireOrderService.php
// Pseudo-code de l'implémentation de Kahn
class QuestionnaireOrderService
{
// ... dépendances (Repos)
public function topologicalSort(int $questionnaireId): array
{
// 1. Récupération des données
$questions = $this->questionRepository->findQuestionsAndAnswers($questionnaireId);
$transitions = $this->transitionRepository->findFlatTransitionLinks($questionnaireId);
// 2. Initialisation du Graphe
$graph = [];
$inDegree = []; // Compteur de dépendances entrantes pour chaque question (vœux)
// Initialisation de V (sommets/questions)
foreach ($questions as $q) {
$graph[$q->getId()] = [];
$inDegree[$q->getId()] = 0;
}
// 3. Construction du Graphe et des degrés entrants
foreach ($transitions as $link) {
$sourceId = $link['source_id'];
$targetId = $link['target_id'];
if (isset($graph[$sourceId]) && isset($inDegree[$targetId])) {
$graph[$sourceId][] = $targetId; // Ajout de l'arête (source -> cible)
$inDegree[$targetId]++; // Incrémentation du degré entrant
}
}
// Trouver la (ou les) première question (celle qui démarre l'arbre)
// Elle doit avoir 0 dépendance entrante OU être la initialTransition
$queue = new \SplQueue();
foreach ($inDegree as $id => $degree) {
if ($degree === 0) {
$queue->enqueue($id);
}
}
// 4. Exécution de Kahn
$sortedOrder = [];
while (!$queue->isEmpty()) {
$vId = $queue->dequeue();
$sortedOrder[] = $vId;
// Pour tous les voisins (enfants) de v
foreach ($graph[$vId] as $uId) {
$inDegree[$uId]--;
// Si le degré entrant devient 0, ajouter à la file
if ($inDegree[$uId] === 0) {
$queue->enqueue($uId);
}
}
}
// 5. Re-mapping des IDs aux objets Question pour le retour
$questionObjects = [];
$questionMap = array_combine(array_map(fn($q) => $q->getId(), $questions), $questions);
foreach ($sortedOrder as $id) {
if (isset($questionMap[$id])) {
$questionObjects[] = $questionMap[$id];
}
}
return $questionObjects;
}
}
Étape 4 : Le Contrôleur (Sérialisation Finale)
Le contrôleur utilise le Serializer pour formater la liste triée d'objets.
// Dans votre Contrôleur API
// ... injections du service de tri et du SerializerInterface
public function getQuestionnaireOrderedAction(int $id): JsonResponse
{
// 1. Obtenir la liste ordonnée d'objets Question
$questionsOrdonnees = $this->questionnaireOrderService->topologicalSort($id);
// 2. Sérialisation de la liste (Groupes de sérialisation requis sur Question/Answer/Transition)
$jsonContent = $this->serializer->serialize($questionsOrdonnees, 'json', [
'groups' => ['questionnaire:read'],
]);
return new JsonResponse($jsonContent, JsonResponse::HTTP_OK, [], true);
}