src/Service/DoceboLearnerSync.php line 147

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Entity\Apprenant;
  4. use App\Entity\Client;
  5. use App\Entity\Inscription;
  6. use App\Entity\Projet;
  7. use App\Entity\Statistique;
  8. use App\Entity\StatProjet;
  9. use Doctrine\ORM\EntityManagerInterface;
  10. use Psr\Log\LoggerInterface;
  11. /**
  12. * Synchronisation d'un apprenant suite aux webhooks Docebo
  13. * (branch.user.added / branch.user.removed / user.updated / user.created / branch.created)
  14. */
  15. class DoceboLearnerSync
  16. {
  17. const CATEGORIE_ACTIVE = 0;
  18. const CATEGORIE_RETIRE = 5;
  19. private $em;
  20. private $logger;
  21. private $doceboApi;
  22. public function __construct(EntityManagerInterface $em, LoggerInterface $logger, NewDoceboApi $doceboApi)
  23. {
  24. $this->em = $em;
  25. $this->logger = $logger;
  26. $this->doceboApi = $doceboApi;
  27. }
  28. /**
  29. * Apprenant ajouté à une branche Docebo : création / mise à jour + inscription au(x) projet(s)
  30. */
  31. public function addToBranch(int $userId, int $branchId, ?string $username = null, ?array $details = null): array
  32. {
  33. $projets = $this->em->getRepository(Projet::class)->findBy(['idProjet' => $branchId]);
  34. if (!count($projets)) {
  35. return $this->result('branch.user.added', $userId, $branchId, ['status' => 'no_projet']);
  36. }
  37. $apprenant = $this->em->getRepository(Apprenant::class)->findOneBy(['idUser' => $userId]);
  38. if (!$apprenant) {
  39. $apprenant = new Apprenant();
  40. $apprenant->setIdUser($userId);
  41. $apprenant->setUsername($username);
  42. $apprenant->setPermissionAppel(0);
  43. $apprenant->setType(0);
  44. $apprenant->setPriority(0);
  45. $apprenant->setCategorie(0);
  46. $this->em->persist($apprenant);
  47. }
  48. $details = $details ?? $this->fetchUser($userId);
  49. if ($details) {
  50. $this->hydrate($apprenant, $details['user_data'], $details['additional_fields'] ?? []);
  51. } else {
  52. $this->logger->warning('[sync-learners] Détails Docebo indisponibles, apprenant synchronisé sans détails', [
  53. 'user_id' => $userId,
  54. 'branch_id' => $branchId,
  55. ]);
  56. }
  57. $apprenant->setDateModif(date("Y-m-d\TH:i:s", strtotime("+1 hour")));
  58. $projetIds = [];
  59. foreach ($projets as $projet) {
  60. $apprenant->setProjet($projet);
  61. $inscription = $apprenant->getId()
  62. ? $this->em->getRepository(Inscription::class)->findOneBy(['projet' => $projet, 'apprenant' => $apprenant])
  63. : null;
  64. if (!$inscription) {
  65. $inscription = new Inscription();
  66. $inscription->setApprenant($apprenant);
  67. $inscription->setProjet($projet);
  68. $inscription->setCategorie(self::CATEGORIE_ACTIVE);
  69. $this->em->persist($inscription);
  70. } elseif ($inscription->getCategorie() == self::CATEGORIE_RETIRE) {
  71. $inscription->setCategorie(self::CATEGORIE_ACTIVE);
  72. }
  73. $projetIds[] = $projet->getId();
  74. }
  75. $this->em->flush();
  76. return $this->result('branch.user.added', $userId, $branchId, [
  77. 'status' => 'added',
  78. 'apprenant' => $apprenant->getId(),
  79. 'projets' => $projetIds,
  80. ]);
  81. }
  82. /**
  83. * Apprenant retiré d'une branche Docebo : inscription(s) en catégorie 5
  84. */
  85. public function removeFromBranch(int $userId, int $branchId): array
  86. {
  87. $projets = $this->em->getRepository(Projet::class)->findBy(['idProjet' => $branchId]);
  88. if (!count($projets)) {
  89. return $this->result('branch.user.removed', $userId, $branchId, ['status' => 'no_projet']);
  90. }
  91. $apprenant = $this->em->getRepository(Apprenant::class)->findOneBy(['idUser' => $userId]);
  92. if (!$apprenant) {
  93. return $this->result('branch.user.removed', $userId, $branchId, ['status' => 'unknown_user']);
  94. }
  95. $projetIds = [];
  96. foreach ($projets as $projet) {
  97. $inscription = $this->em->getRepository(Inscription::class)->findOneBy(['projet' => $projet, 'apprenant' => $apprenant]);
  98. if ($inscription) {
  99. $inscription->setCategorie(self::CATEGORIE_RETIRE);
  100. }
  101. if ($apprenant->getProjet() === $projet) {
  102. $apprenant->setProjet(null);
  103. }
  104. $projetIds[] = $projet->getId();
  105. }
  106. $apprenant->setDateModif(date("Y-m-d\TH:i:s", strtotime("+1 hour")));
  107. $this->em->flush();
  108. return $this->result('branch.user.removed', $userId, $branchId, [
  109. 'status' => 'removed',
  110. 'apprenant' => $apprenant->getId(),
  111. 'projets' => $projetIds,
  112. ]);
  113. }
  114. /**
  115. * Utilisateur modifié dans Docebo : mise à jour de l'apprenant (sans toucher aux projets / inscriptions)
  116. */
  117. public function updateUser(int $userId, array $payload = []): array
  118. {
  119. $apprenant = $this->em->getRepository(Apprenant::class)->findOneBy(['idUser' => $userId]);
  120. if (!$apprenant) {
  121. return $this->result('user.updated', $userId, null, ['status' => 'unknown_user']);
  122. }
  123. $details = $this->fetchUser($userId);
  124. if ($details) {
  125. $this->hydrate($apprenant, $details['user_data'], $details['additional_fields'] ?? []);
  126. } else {
  127. // Fallback : uniquement les champs de base présents dans le payload
  128. $this->logger->warning('[sync-learners] Détails Docebo indisponibles, mise à jour depuis le payload', ['user_id' => $userId]);
  129. if (!empty($payload['username'])) {
  130. $apprenant->setUsername($payload['username']);
  131. }
  132. if (!empty($payload['firstname'])) {
  133. $apprenant->setFirstname($payload['firstname']);
  134. }
  135. if (!empty($payload['lastname'])) {
  136. $apprenant->setLastname($payload['lastname']);
  137. }
  138. if (!empty($payload['email'])) {
  139. $apprenant->setEmail($payload['email']);
  140. }
  141. }
  142. $apprenant->setDateModif(date("Y-m-d\TH:i:s", strtotime("+1 hour")));
  143. $this->em->flush();
  144. return $this->result('user.updated', $userId, null, [
  145. 'status' => 'updated',
  146. 'apprenant' => $apprenant->getId(),
  147. ]);
  148. }
  149. /**
  150. * Utilisateur créé dans Docebo : le payload ne contient pas de branche,
  151. * on lit ses branches via l'API et on l'inscrit aux projets correspondants
  152. */
  153. public function createUser(int $userId, array $payload = []): array
  154. {
  155. $details = $this->fetchUser($userId);
  156. if (!$details) {
  157. return $this->result('user.created', $userId, null, ['status' => 'details_unavailable']);
  158. }
  159. $projetRepository = $this->em->getRepository(Projet::class);
  160. $branches = [];
  161. foreach (array_unique(array_column($details['branches'] ?? [], 'id')) as $branchId) {
  162. if ($projetRepository->findOneBy(['idProjet' => (int) $branchId])) {
  163. $branches[] = $this->addToBranch($userId, (int) $branchId, $payload['username'] ?? null, $details);
  164. }
  165. }
  166. if (!count($branches)) {
  167. return $this->result('user.created', $userId, null, [
  168. 'status' => 'no_projet',
  169. 'docebo_branches' => array_column($details['branches'] ?? [], 'id'),
  170. ]);
  171. }
  172. return $this->result('user.created', $userId, null, [
  173. 'status' => 'created',
  174. 'apprenant' => $branches[0]['apprenant'] ?? null,
  175. 'branches' => $branches,
  176. ]);
  177. }
  178. /**
  179. * Branche créée dans Docebo :
  180. * - parent_id = 0 : nouveau client (comme /update/clients)
  181. * - parent_id = client : nouveau projet (comme /update/projets)
  182. */
  183. public function branchCreated(int $branchId, int $parentId, ?string $code, ?string $name): array
  184. {
  185. $label = trim((string) $name) !== '' ? trim($name) : (trim((string) $code) !== '' ? trim($code) : 'Branche ' . $branchId);
  186. $now = date("Y-m-d\TH:i:s", strtotime("+1 hour"));
  187. if ($parentId === 0) {
  188. $client = $this->em->getRepository(Client::class)->findOneBy(['idOrg' => $branchId]);
  189. if ($client) {
  190. return $this->result('branch.created', null, $branchId, ['status' => 'client_exists', 'client' => $client->getId()]);
  191. }
  192. $stat = new Statistique();
  193. $this->initStats($stat);
  194. $stat->setDateMaj($now);
  195. $this->em->persist($stat);
  196. $client = new Client();
  197. $client->setIdOrg($branchId);
  198. $client->setCode($code);
  199. $client->setNom($label);
  200. $client->setNombreProjet(0);
  201. $client->setStatus(2);
  202. $client->setStatistique($stat);
  203. $client->setDateMaj($now);
  204. $this->em->persist($client);
  205. $this->em->flush();
  206. return $this->result('branch.created', null, $branchId, ['status' => 'client_created', 'client' => $client->getId()]);
  207. }
  208. $client = $this->em->getRepository(Client::class)->findOneBy(['idOrg' => $parentId]);
  209. if (!$client) {
  210. return $this->result('branch.created', null, $branchId, ['status' => 'parent_not_client', 'parent_id' => $parentId]);
  211. }
  212. $projet = $this->em->getRepository(Projet::class)->findOneBy(['idProjet' => $branchId]);
  213. if ($projet) {
  214. return $this->result('branch.created', null, $branchId, ['status' => 'projet_exists', 'projet' => $projet->getId()]);
  215. }
  216. $stat = new StatProjet();
  217. $this->initStats($stat);
  218. $this->em->persist($stat);
  219. $projet = new Projet();
  220. $projet->setIdProjet($branchId);
  221. $projet->setCode($label);
  222. $projet->setNom($label);
  223. $projet->setClient($client);
  224. $projet->setStatProjet($stat);
  225. $projet->setActif(1);
  226. $projet->setMajApprenants(1);
  227. $projet->setDateMaj($now);
  228. $this->em->persist($projet);
  229. $client->setNombreProjet((int) $client->getNombreProjet() + 1);
  230. $this->em->flush();
  231. return $this->result('branch.created', null, $branchId, [
  232. 'status' => 'projet_created',
  233. 'projet' => $projet->getId(),
  234. 'client' => $client->getId(),
  235. ]);
  236. }
  237. /**
  238. * Statistiques à 0 pour une branche qui vient d'être créée
  239. * @param Statistique|StatProjet $stat
  240. */
  241. private function initStats($stat): void
  242. {
  243. $stat->setTotalUsers(0);
  244. $stat->setCourseEnrollments(0);
  245. $stat->setCourseEnrollmentsNotStarted(0);
  246. $stat->setCourseEnrollmentsInProgress(0);
  247. $stat->setCourseEnrollmentsCompleted(0);
  248. $stat->setCourseEnrollmentsExpired(0);
  249. }
  250. private function fetchUser(int $userId): ?array
  251. {
  252. try {
  253. $response = $this->doceboApi->useDoceboApi('/manage/v1/user/' . $userId, null);
  254. } catch (\Throwable $e) {
  255. $this->logger->error('[sync-learners] Erreur API Docebo', ['user_id' => $userId, 'error' => $e->getMessage()]);
  256. return null;
  257. }
  258. return isset($response['data']['user_data']) ? $response['data'] : null;
  259. }
  260. /**
  261. * Même mapping que ProjetController::importApprenantsAction
  262. */
  263. private function hydrate(Apprenant $apprenant, array $userData, array $additionalFields): void
  264. {
  265. $fields = [];
  266. foreach ($additionalFields as $additionalField) {
  267. if (isset($additionalField['title'])) {
  268. $fields[$additionalField['title']] = $additionalField['value'] ?? "";
  269. }
  270. }
  271. $gsm = str_replace('(0)', '', (string) ($fields['GSM'] ?? ""));
  272. $gsm2 = (string) ($fields['GSM2'] ?? "");
  273. if (strlen($gsm) == 9) {
  274. $gsm = "0" . $gsm;
  275. }
  276. if (strlen($gsm2) == 9) {
  277. $gsm2 = "0" . $gsm2;
  278. }
  279. if (!empty($userData['username'])) {
  280. $apprenant->setUsername($userData['username']);
  281. }
  282. $apprenant->setFirstname($userData['first_name'] ?? null);
  283. $apprenant->setLastname($userData['last_name'] ?? null);
  284. $apprenant->setEmail($userData['email'] ?? null);
  285. $apprenant->setGsm($gsm);
  286. $apprenant->setGsm2($gsm2);
  287. $apprenant->setDirection($fields['Direction'] ?? "");
  288. $apprenant->setPassword($fields['mdp text'] ?? "");
  289. $apprenant->setProfil($fields['Profil'] ?? "");
  290. $apprenant->setPoste($fields['Poste'] ?? "");
  291. $apprenant->setFonction($fields['Fonction'] ?? "");
  292. $vip = $fields['VIP'] ?? null;
  293. $apprenant->setType(($vip == "1" || $vip == "2") ? (int) $vip : 0);
  294. }
  295. private function result(string $event, ?int $userId, ?int $branchId, array $result): array
  296. {
  297. $context = ['event' => $event, 'user_id' => $userId, 'branch_id' => $branchId] + $result;
  298. if (in_array($result['status'], ['added', 'removed', 'updated', 'created', 'client_created', 'projet_created'])) {
  299. $this->logger->info('[sync-learners] ' . $result['status'], $context);
  300. } else {
  301. $this->logger->warning('[sync-learners] ' . $result['status'], $context);
  302. }
  303. return ['user_id' => $userId, 'branch_id' => $branchId] + $result;
  304. }
  305. }