src/Controller/BlogController.php line 394

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Blog;
  4. use App\Entity\Publication;
  5. use App\Entity\SousThematique;
  6. use App\Entity\PublicationImage;
  7. use App\Entity\Thematique;
  8. use App\Form\BlogType;
  9. use App\Form\SousThematiqueType;
  10. use App\Form\ThematiqueType;
  11. use App\Repository\BlogRepository;
  12. use App\Repository\SousThematiqueRepository;
  13. use App\Repository\ThematiqueRepository;
  14. use App\Repository\UserRepository;
  15. use Doctrine\ORM\EntityManagerInterface;
  16. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  17. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  18. use Symfony\Component\HttpFoundation\JsonResponse;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\HttpFoundation\Response;
  21. use Symfony\Component\Routing\Annotation\Route;
  22. class BlogController extends AbstractController
  23. {
  24.     /**
  25.      * @Route("/admin/blog",name="blog_admin")
  26.      * @IsGranted("ROLE_ADMIN")
  27.      */
  28.     public function admin_show(
  29.         BlogRepository $blogRepository
  30.     ) {
  31.         try {
  32.             $publications $blogRepository->findAll();
  33.             return $this->render('admin/blog/index.html.twig', [
  34.                 'publications' => $publications
  35.             ]);
  36.         } catch (\Throwable $exception) {
  37.             // Afficher le message d'erreur dans la réponse HTTP
  38.             return new Response('An error occurred: ' $exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
  39.         }
  40.         // return $this->render('admin/blog/index.html.twig', [
  41.         //     'publications' => $blogRepository->findAll()
  42.         // ]);
  43.     }
  44.     /**
  45.      * @Route("/admin/thematique/blog/",name="blog_thematique")
  46.      * @IsGranted("ROLE_ADMIN")
  47.      */
  48.     public function admin_add_thematique(
  49.         EntityManagerInterface $entityManager,
  50.         Request                $request,
  51.         ThematiqueRepository   $thematiqueRepository
  52.     ) {
  53.         $thematique = new Thematique();
  54.         $form $this->createForm(ThematiqueType::class, $thematique);
  55.         $form->handleRequest($request);
  56.         if ($form->isSubmitted() && $form->isValid()) {
  57.             $slug trim($thematique->getSlug());
  58.             $slug str_replace(' ''-'$slug);
  59.             $thematique->setSlug($slug);
  60.             $entityManager->persist($thematique);
  61.             $entityManager->flush();
  62.             return $this->redirectToRoute('blog_admin', [], Response::HTTP_SEE_OTHER);
  63.         }
  64.         return $this->render('admin/blog/thematique.html.twig', [
  65.             'thematiques' => $thematiqueRepository->findAll(),
  66.             'form' => $form->createView(),
  67.         ]);
  68.     }
  69.     /**
  70.      * @Route ("/admin/thematique/delete/blog/{id}",name="delete_thematique", methods={"POST"})
  71.      * @IsGranted("ROLE_ADMIN")
  72.      */
  73.     public function admin_delete_thematique(
  74.         $id,
  75.         EntityManagerInterface $entityManager,
  76.         Thematique $thematique,
  77.         Request $request
  78.     ) {
  79.         if ($this->isCsrfTokenValid('delete' $thematique->getId(), $request->request->get('_token'))) {
  80.             $entityManager->remove($thematique);
  81.             $entityManager->flush();
  82.         }
  83.         return $this->redirectToRoute('blog_thematique', [], Response::HTTP_SEE_OTHER);
  84.     }
  85.     /**
  86.      * @Route("/admin/thematique/update/{id}", name="update_thematique")
  87.      * @IsGranted("ROLE_ADMIN")
  88.      */
  89.     public function admin_update_thematique(
  90.         EntityManagerInterface $entityManager,
  91.         Thematique             $thematique,
  92.         Request                $request,
  93.         ThematiqueRepository   $thematiqueRepository
  94.     ) {
  95.         $form $this->createForm(ThematiqueType::class, $thematique);
  96.         $form->handleRequest($request);
  97.         if ($form->isSubmitted() && $form->isValid()) {
  98.             $slug trim($thematique->getSlug());
  99.             $slug str_replace(' ''-'$slug);
  100.             $thematique->setSlug($slug);
  101.             $entityManager->flush();
  102.             return $this->redirectToRoute('blog_thematique', [], Response::HTTP_SEE_OTHER);
  103.         }
  104.         return $this->render('admin/blog/thematique_update.html.twig', [
  105.             'thematiques' => $thematiqueRepository->findAll(),
  106.             'form' => $form->createView(),
  107.         ]);
  108.     }
  109.     /**
  110.      * @Route("/admin/ssthematique/blog/",name="blog_ssthematique")
  111.      * @IsGranted("ROLE_ADMIN")
  112.      */
  113.     public function admin_add_ss_thematique(
  114.         EntityManagerInterface   $entityManager,
  115.         Request                  $request,
  116.         SousThematiqueRepository $sousThematiqueRepository
  117.     ) {
  118.         $ss_thematique = new SousThematique();
  119.         $form $this->createForm(SousThematiqueType::class, $ss_thematique);
  120.         $form->handleRequest($request);
  121.         if ($form->isSubmitted() && $form->isValid()) {
  122.             foreach ($ss_thematique->getThematiques() as $thematique) {
  123.                 $thematique->addSousThematique($ss_thematique);
  124.             }
  125.             $slug trim($ss_thematique->getSlug());
  126.             $slug str_replace(' ''-'$slug);
  127.             $ss_thematique->setSlug($slug);
  128.             $entityManager->persist($ss_thematique);
  129.             $entityManager->flush();
  130.             return $this->redirectToRoute('blog_admin', [], Response::HTTP_SEE_OTHER);
  131.         }
  132.         return $this->render('admin/blog/ss_thematique.html.twig', [
  133.             'sousthematiques' => $sousThematiqueRepository->findAll(),
  134.             'form' => $form->createView(),
  135.         ]);
  136.     }
  137.     /**
  138.      * @Route ("/admin/sousthematique/delete/blog/{id}",name="delete_sousthematique", methods={"POST"})
  139.      * @IsGranted("ROLE_ADMIN")
  140.      */
  141.     public function admin_delete_sousthematique(
  142.         $id,
  143.         EntityManagerInterface $entityManager,
  144.         SousThematique $sousThematique,
  145.         Request $request
  146.     ) {
  147.         if ($this->isCsrfTokenValid('delete' $sousThematique->getId(), $request->request->get('_token'))) {
  148.             $entityManager->remove($sousThematique);
  149.             $entityManager->flush();
  150.         }
  151.         return $this->redirectToRoute('blog_ssthematique', [], Response::HTTP_SEE_OTHER);
  152.     }
  153.     /**
  154.      * @Route("/admin/sousthematique/update/{id}", name="update_sousthematique")
  155.      * @IsGranted("ROLE_ADMIN")
  156.      */
  157.     public function admin_update_sousthematique(
  158.         $id,
  159.         EntityManagerInterface $entityManager,
  160.         SousThematique $sousThematique,
  161.         Request $request,
  162.         SousThematiqueRepository $sousThematiqueRepository,
  163.         ThematiqueRepository $thematiqueRepository
  164.     ) {
  165.         $form $this->createForm(SousThematiqueType::class, $sousThematique);
  166.         $form->handleRequest($request);
  167.         if ($form->isSubmitted() && $form->isValid()) {
  168.             // Récupérer les entités Thematique associées actuellement
  169.             $currentThematiques $thematiqueRepository->findBySousThematique($sousThematique);
  170.             // Récupérer les identifiants des Thematiques sélectionnées dans la requête
  171.             $newThematiquesIds $form->get('thematiques')->getData()->map(fn($thematique) => $thematique->getId())->toArray();
  172.             foreach ($currentThematiques as $currentThematique) {
  173.                 // Vérifier si la Thematique actuelle n'est pas sélectionnée dans la requête
  174.                 if (!in_array($currentThematique->getId(), $newThematiquesIds)) {
  175.                     // Si la Thematique actuelle n'est pas sélectionnée dans la requête, la supprimer de la SousThematique
  176.                     $sousThematique->removeThematique($currentThematique);
  177.                     $currentThematique->removeSousThematique($sousThematique);
  178.                 }
  179.             }
  180.             // Ajouter les nouvelles Thematiques sélectionnées dans la requête qui ne sont pas déjà associées à la SousThematique
  181.             foreach ($newThematiquesIds as $newThematiqueId) {
  182.                 // Charger l'entité Thematique depuis la base de données
  183.                 $newThematique $entityManager->getRepository(Thematique::class)->find($newThematiqueId);
  184.                 $sousThematique $sousThematiqueRepository->findOneBy(['id' => $id]);
  185.                 $sousThematique->addThematique($newThematique);
  186.                 $newThematique->addSousThematique($sousThematique);
  187.             }
  188.             $slug trim($sousThematique->getSlug());
  189.             $slug str_replace(' ''-'$slug);
  190.             $sousThematique->setSlug($slug);
  191.             $entityManager->flush();
  192.             return $this->redirectToRoute('blog_ssthematique', [], Response::HTTP_SEE_OTHER);
  193.         }
  194.         return $this->render('admin/blog/ss_thematique_update.html.twig', [
  195.             'sousthematiques' => $sousThematiqueRepository->findAll(),
  196.             'form' => $form->createView(),
  197.         ]);
  198.     }
  199.     /**
  200.      * @Route("/admin/delete/blog/{id}", name="blog_delete", methods={"POST"})
  201.      * @IsGranted("ROLE_ADMIN")
  202.      */
  203.     public function delete(Request $requestBlog $blogEntityManagerInterface $entityManager): Response
  204.     {
  205.         if ($this->isCsrfTokenValid('delete' $blog->getId(), $request->request->get('_token'))) {
  206.             $entityManager->remove($blog);
  207.             $entityManager->flush();
  208.         }
  209.         return $this->redirectToRoute('blog_admin', [], Response::HTTP_SEE_OTHER);
  210.     }
  211.     /**
  212.      * @Route("/admin/blog/new",name="blog_new",methods={"GET", "POST"})
  213.      * @IsGranted("ROLE_ADMIN")
  214.      */
  215.     public function admin_new(
  216.         UserRepository         $userRepository,
  217.         Request                $request,
  218.         EntityManagerInterface $entityManager
  219.     ) {
  220.         $blog = new Blog();
  221.         $blog->setCreatedAt(new \DateTimeImmutable());
  222.         $blog->setUpdatedAt(new \DateTimeImmutable());
  223.         $form $this->createForm(BlogType::class, $blog);
  224.         $form->handleRequest($request);
  225.         if ($form->isSubmitted() && $form->isValid()) {
  226.             try {
  227.                 // dd($blog);
  228.                 // Persister et flusher l'entité Blog
  229.                 $entityManager->persist($blog);
  230.                 $entityManager->flush();
  231.                 return $this->redirectToRoute('blog_admin', [], Response::HTTP_SEE_OTHER);
  232.             } catch (\Throwable $exception) {
  233.                 // Afficher le message d'erreur dans la réponse HTTP
  234.                 return new Response('An error occurred: ' $exception->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
  235.             }
  236.             return $this->redirectToRoute('blog_admin', [], Response::HTTP_SEE_OTHER);
  237.         }
  238.         return $this->renderForm('admin/blog/new.html.twig', [
  239.             'publication' => $blog,
  240.             'form' => $form,
  241.             'nb_not_verify' => $userRepository->countUserNotVerify()
  242.         ]);
  243.     }
  244.     /**
  245.      * @Route("/admin/blog/{id}/edit", name="blog_edit", methods={"GET", "POST"})
  246.      * @IsGranted("ROLE_ADMIN")
  247.      */
  248.     public function blog_edit(Request $requestBlog $blogEntityManagerInterface $entityManagerUserRepository $userRepository): Response
  249.     {
  250.         $form $this->createForm(BlogType::class, $blog, ['is_edit' => true]);
  251.         $form->handleRequest($request);
  252.         if ($form->isSubmitted() && $form->isValid()) {
  253.             $blog->setUpdatedAt(new \DateTimeImmutable());
  254.             $entityManager->flush();
  255.             return $this->redirectToRoute('blog_admin', [], Response::HTTP_SEE_OTHER);
  256.         }
  257.         return $this->renderForm('admin/blog/edit.html.twig', [
  258.             'publication' => $blog,
  259.             'nb_not_verify' => $userRepository->countUserNotVerify(),
  260.             'form' => $form,
  261.         ]);
  262.     }
  263.     /**
  264.      * @Route("/get-sous-thematiques/blog", name="get_sous_thematiques")
  265.      */
  266.     public function getSousThematiques(
  267.         Request                  $request,
  268.         SousThematiqueRepository $thematiqueRepository
  269.     ) {
  270.         // Récupérez les IDs des thématiques sélectionnées
  271.         $thematiqueIds $request->query->get('thematiques');
  272.         if ($thematiqueIds === null) {
  273.             // Utiliser findAll si thematiqueIds est null
  274.             $sousThematiques $thematiqueRepository->findAll();
  275.         } else {
  276.             // Utilisez ces IDs pour récupérer les sous-thématiques correspondantes
  277.             $sousThematiques $thematiqueRepository->findByThematiquesIds($thematiqueIds);
  278.         }
  279.         // Formatez les données pour les envoyer au format JSON
  280.         $formattedData = [];
  281.         foreach ($sousThematiques as $sousThematique) {
  282.             $formattedData[] = [
  283.                 'id' => $sousThematique->getId(),
  284.                 'text' => $sousThematique->getName() // Ou tout autre champ approprié
  285.             ];
  286.         }
  287.         // Retournez les données au format JSON
  288.         return new JsonResponse($formattedData);
  289.     }
  290.     /**
  291.      * @Route("/get-thematiques-preselectionnees/blog", name="get_thematiques_preselectionnees")
  292.      */
  293.     public function getThematiquesPreselectionnees(
  294.         Request              $request,
  295.         ThematiqueRepository $thematiqueRepository
  296.     ): JsonResponse {
  297.         // Récupérer les sous-thématiques sélectionnées depuis la requête
  298.         $sousThematiquesIds $request->query->get('sousThematiques', []);
  299.         // Si aucune sous-thématique n'est sélectionnée, retourner une réponse vide
  300.         if (empty($sousThematiquesIds)) {
  301.             return new JsonResponse([]);
  302.         }
  303.         // Récupérer les thèmes pré-sélectionnés en fonction des sous-thématiques sélectionnées
  304.         $thematiques $thematiqueRepository->findBySousThematiquesIds($sousThematiquesIds);
  305.         // Formater les données pour la réponse JSON
  306.         $formattedThematiques = [];
  307.         foreach ($thematiques as $thematique) {
  308.             $formattedThematiques[] = [
  309.                 'id' => $thematique->getId(),
  310.                 'text' => $thematique->getName() // Ou tout autre champ approprié
  311.             ];
  312.         }
  313.         // Retourner les thèmes pré-sélectionnés au format JSON
  314.         return new JsonResponse($formattedThematiques);
  315.     }
  316.     /**
  317.      * @Route("/blog/", name="blog", methods={"GET"})
  318.      */
  319.     public function index(
  320.         BlogRepository       $blogRepository,
  321.         ThematiqueRepository $thematiqueRepository
  322.     ) {
  323.         $dernierBlogs $blogRepository->findBy([], ['created_at' => 'DESC'], 9);
  324.         $thematiques $thematiqueRepository->findAll();
  325.         return $this->render('blog/index.html.twig', [
  326.             'blogs' => $dernierBlogs,
  327.             'thematiques' => $thematiques
  328.         ]);
  329.     }
  330.     /**
  331.      * @Route("/blog/{thematique}/{sousThematique}", name="blog_show", methods={"GET"},defaults={"thematique"=null, "sousThematique"=null})
  332.      */
  333.     public function show(
  334.         $thematique,
  335.         $sousThematique,
  336.         BlogRepository $blogRepository,
  337.         SousThematiqueRepository $sousThematiqueRepository,
  338.         ThematiqueRepository $thematiqueRepository,
  339.         EntityManagerInterface $entityManager
  340.     ): Response {
  341.         $countResultBySlugFromThematique $thematiqueRepository->countBySlug($thematique);
  342.         if (empty($sousThematique)) {
  343.             // le paramètre est donc un thematique quand le comptage est positif
  344.             if ($countResultBySlugFromThematique 0) {
  345.                 $blogsByThematiqueWithSousThematiqueNull $blogRepository->findByThematiqueSlug($thematique);
  346.                 $sousThematiqueByThematique $sousThematiqueRepository->findByThematiqueSlug($thematique);
  347.                 //dump($sousThematiqueByThematique);
  348.                 //dd($blogsByThematiqueWithSousThematiqueNull);
  349.                 return $this->render('blog/thematique/index.html.twig', [
  350.                     'blogs' => $blogsByThematiqueWithSousThematiqueNull ?? [],
  351.                     'sousThematiques' => $sousThematiqueByThematique ?? [],
  352.                     'thematique' => $thematiqueRepository->findOneBy(['slug' => $thematique]),
  353.                 ]);
  354.             } else {
  355.                 // sinon c'est un article si le slug existe dans la bdd
  356.                 $blogsBySlug $blogRepository->findBy(['slug' => $thematique]);
  357.                 if (empty($blogsBySlug)) {
  358.                     throw $this->createNotFoundException('La page que vous recherchez n\'existe pas.');
  359.                 }
  360.                 //dd($blogsBySlug);
  361.                 foreach ($blogsBySlug as $blog) {
  362.                     // Récupérer la valeur actuelle de la vue
  363.                     $currentViews $blog->getView();
  364.                     // Incrémenter le nombre de vues
  365.                     $newViews $currentViews 1;
  366.                     // Mettre à jour la valeur de la vue
  367.                     $blog->setView($newViews);
  368.                     // Enregistrer les modifications dans la base de données
  369.                     $entityManager->flush();
  370.                 }
  371.                 return $this->render('blog/article/index.html.twig', [
  372.                     'blogs' => $blogsBySlug
  373.                 ]);
  374.             }
  375.         } else {
  376.             if ($countResultBySlugFromThematique 0) {
  377.                 $countResultBySlugFromSousThematique $sousThematiqueRepository->countBySlug($sousThematique);
  378.                 if ($countResultBySlugFromSousThematique 0) {
  379.                     $blogsByThematiqueAndSousThematique $blogRepository->findByThematiqueAndSousThematique($thematique$sousThematique);
  380.                     $sousThematiqueByThematique $sousThematiqueRepository->findByThematiqueNameNotSsthematiqueEncours($thematique$sousThematique);
  381.                     //dd($blogsByThematiqueAndSousThematique);
  382.                     return $this->render(
  383.                         'blog/ss_thematique/index.html.twig',
  384.                         [
  385.                             'blogs' => $blogsByThematiqueAndSousThematique ?? [],
  386.                             'sousThematique' => $sousThematiqueRepository->findOneBy(['slug' => $sousThematique]),
  387.                             'thematique' => $thematiqueRepository->findOneBy(['slug' => $thematique]),
  388.                             'listeSousThematique' => $sousThematiqueByThematique
  389.                         ]
  390.                     );
  391.                 } else {
  392.                     throw $this->createNotFoundException('La page que vous recherchez n\'existe pas.');
  393.                 }
  394.             } else {
  395.                 throw $this->createNotFoundException('La page que vous recherchez n\'existe pas.');
  396.             }
  397.         }
  398.     }
  399.     /**
  400.      * @Route("/admin/blog/update-content", name="update_blog_content", methods={"POST"})
  401.      */
  402.     public function updateBlogContent(Request $requestBlogRepository $blogRepositoryEntityManagerInterface $em)
  403.     {
  404.         // Récupérer l'ID du blog envoyé via la requête
  405.         $blogId $request->request->get('blogId');
  406.         // Récupérer le blog à partir de son ID
  407.         $blog $blogRepository->find($blogId);
  408.         if (!$blog) {
  409.             return new JsonResponse(['error' => 'Blog non trouvé'], 404);
  410.         }
  411.         $currentBigTitle $blog->getBigtitle();
  412.         // Récupérer le contenu actuel du blog
  413.         $content $blog->getContent();  // Assure-toi d'avoir une méthode getContent() dans ton entité Blog
  414.         // Récupérer les mots-clés et les URL associés
  415.         $keywords $this->getKeywords($currentBigTitle); // Fonction pour récupérer les mots-clés et leurs URL
  416.         // Appliquer les modifications sur le contenu
  417.         $modifiedContent $this->applyKeywords($content$keywords);
  418.         // Mettre à jour le contenu du blog dans la base de données
  419.         $blog->setContent($modifiedContent);
  420.         $em->flush();
  421.         // Retourner une réponse JSON pour indiquer que tout a bien fonctionné
  422.         return new JsonResponse(['message' => 'Contenu mis à jour avec succès']);
  423.     }
  424.     /**
  425.      * Fonction pour récupérer les mots-clés et leurs URL
  426.      * Ici on récupère tous les blogs pour avoir leur titre et URL
  427.      */
  428.     private function getKeywords($currentBigTitle)
  429.     {
  430.         // Récupère les blogs
  431.         $blogRepository $this->getDoctrine()->getRepository(Blog::class);
  432.         $blogs $blogRepository->createQueryBuilder('b')
  433.             ->select('b.bigtitle''b.slug')
  434.             ->where('b.bigtitle IS NOT NULL')
  435.             ->andWhere('b.bigtitle != :currentBigTitle'// Ajout de la condition pour exclure le titre actuel
  436.             ->setParameter('currentBigTitle'$currentBigTitle// Assurez-vous de définir le paramètre
  437.             ->getQuery()
  438.             ->getResult();
  439.         // Récupère les publications
  440.         $publicationRepository $this->getDoctrine()->getRepository(Publication::class);
  441.         $publications $publicationRepository->createQueryBuilder('p')
  442.             ->select('p.bigtitle''p.slug')
  443.             ->where('p.bigtitle IS NOT NULL')
  444.             ->andWhere('p.bigtitle != :currentBigTitle'// Ajout de la condition pour exclure le titre actuel
  445.             ->setParameter('currentBigTitle'$currentBigTitle// Assurez-vous de définir le paramètre
  446.             ->getQuery()
  447.             ->getResult();
  448.         $keywords = [];
  449.         // Ajoute les blogs à la liste des mots-clés
  450.         foreach ($blogs as $blog) {
  451.             $normalized $this->normalize($blog['bigtitle']);
  452.             $keywords[$normalized] = [
  453.                 'title' => $blog['bigtitle'],
  454.                 'url' => '/blog/' $blog['slug'],
  455.             ];
  456.         }
  457.         // Ajoute les publications à la liste des mots-clés
  458.         foreach ($publications as $publication) {
  459.             $normalized $this->normalize($publication['bigtitle']);
  460.             // Utilisation de '/page/' pour les publications
  461.             $keywords[$normalized] = [
  462.                 'title' => $publication['bigtitle'],
  463.                 'url' => '/' $publication['slug'],
  464.             ];
  465.         }
  466.         // dd($keywords);
  467.         return $keywords;
  468.     }
  469.     /**
  470.      * Normaliser le titre pour faciliter la recherche dans le texte
  471.      * (ici on fait juste une conversion en minuscules)
  472.      */
  473.     private function normalize($text)
  474.     {
  475.         if (class_exists('Normalizer')) {
  476.             $text \Normalizer::normalize($text\Normalizer::FORM_D);
  477.         }
  478.         // Supprime les accents
  479.         $text preg_replace('/\p{Mn}/u'''$text);
  480.         return strtolower(trim($text));
  481.     }
  482.     private function applyKeywords($content$keywords)
  483.     {
  484.         $dom = new \DOMDocument();
  485.         libxml_use_internal_errors(true);
  486.         $dom->loadHTML(mb_convert_encoding('<div>' $content '</div>''HTML-ENTITIES''UTF-8'));
  487.         libxml_clear_errors();
  488.         $forbiddenTags = ['h1''h2''h3''h4''h5''h6''a'];
  489.         // Récupère les mots déjà entourés par une balise <a>
  490.         $linkedKeywords = [];
  491.         $existingLinks $dom->getElementsByTagName('a');
  492.         foreach ($existingLinks as $link) {
  493.             $linkedKeywords[] = $this->normalize(trim($link->nodeValue));
  494.         }
  495.         // Trier les mots-clés du plus long au plus court
  496.         uksort($keywords, function ($a$b) {
  497.             return mb_strlen($b) - mb_strlen($a);
  498.         });
  499.         foreach ($keywords as $keyword => $data) {
  500.             $normalizedKeyword $this->normalize($keyword);
  501.             $xpath = new \DOMXPath($dom);
  502.             $textNodes $xpath->query('//text()');
  503.             foreach ($textNodes as $textNode) {
  504.                 $parent $textNode->parentNode;
  505.                 // Vérifie si un des ancêtres est une balise interdite
  506.                 $isForbidden false;
  507.                 $current $parent;
  508.                 while ($current !== null && $current->nodeName !== 'div') {
  509.                     if (in_array(strtolower($current->nodeName), $forbiddenTags)) {
  510.                         $isForbidden true;
  511.                         break;
  512.                     }
  513.                     $current $current->parentNode;
  514.                 }
  515.                 if ($isForbidden) {
  516.                     continue;
  517.                 }
  518.                 if (in_array($keyword$linkedKeywords)) {
  519.                     break;
  520.                 }
  521.                 $text $textNode->nodeValue;
  522.                 $normalizedText $this->normalize($text);
  523.                 $pos mb_stripos($normalizedText$normalizedKeyword);
  524.                 if ($pos !== false) {
  525.                     $beforeChar $pos mb_substr($normalizedText$pos 11) : '';
  526.                     $afterPos $pos mb_strlen($keyword'UTF-8');
  527.                     $nextChar mb_substr($normalizedText$afterPos1);
  528.                     $isBeforeOk $beforeChar === '' || preg_match('/[\s.,;:(!?\[{<"\']/'$beforeChar);
  529.                     $isAfterOk $nextChar === '' || preg_match('/[\s.,;:!?)\]}>"\']/'$nextChar);
  530.                     if (!$isBeforeOk || !$isAfterOk) {
  531.                         continue;
  532.                     }
  533.                     // Vérifie que le caractère après le mot est une limite valide (ponctuation, espace, fin de texte)
  534.                     $afterPos $pos mb_strlen($keyword'UTF-8');
  535.                     $nextChar mb_substr($normalizedText$afterPos1);
  536.                     // Si le mot est collé à un autre mot, on ne fait rien
  537.                     if ($nextChar !== '' && !preg_match('/[\s.,;:!?)\]}-]/u'$nextChar)) {
  538.                         continue;
  539.                     }
  540.                     $caractPlus 0;
  541.                     // Ajuster la position pour ignorer un espace avant le mot (si nécessaire)
  542.                     if (mb_substr($text$pos1) === ' ') {
  543.                         $caractPlus++; // Ajouter 1 pour commencer après l'espace
  544.                     }
  545.                     $match mb_substr($text$posmb_strlen($keyword'UTF-8'));
  546.                     if ($this->normalize($match) !== $normalizedKeyword) {
  547.                         $match mb_substr($text$pos 1mb_strlen($keyword'UTF-8'));
  548.                         $after mb_substr($text$afterPos 1);
  549.                         $before mb_substr($text0$pos 1);
  550.                     } else {
  551.                         $after mb_substr($text$afterPos);
  552.                         $before mb_substr($text0$pos);
  553.                     }
  554.                     // Crée le lien
  555.                     $link $dom->createElement('a'$match);
  556.                     $link->setAttribute('href'$data['url']);
  557.                     $link->setAttribute('class''suggested-link');
  558.                     // Recompose
  559.                     $fragment $dom->createDocumentFragment();
  560.                     if ($before !== '') {
  561.                         $fragment->appendChild($dom->createTextNode($before));
  562.                     }
  563.                     $fragment->appendChild($link);
  564.                     if ($after !== '') {
  565.                         $fragment->appendChild($dom->createTextNode($after));
  566.                     }
  567.                     $textNode->parentNode->replaceChild($fragment$textNode);
  568.                     $linkedKeywords[] = $keyword;
  569.                     break;
  570.                 }
  571.             }
  572.         }
  573.         // Récupère le contenu de notre div temporaire
  574.         $body $dom->getElementsByTagName('div')->item(0);
  575.         $newContent '';
  576.         foreach ($body->childNodes as $child) {
  577.             $newContent .= $dom->saveHTML($child);
  578.         }
  579.         return $newContent;
  580.     }
  581. }