<?php
namespace App\Service;
use App\Entity\Apprenant;
use App\Entity\Client;
use App\Entity\Inscription;
use App\Entity\Projet;
use App\Entity\Statistique;
use App\Entity\StatProjet;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
/**
* Synchronisation d'un apprenant suite aux webhooks Docebo
* (branch.user.added / branch.user.removed / user.updated / user.created / branch.created)
*/
class DoceboLearnerSync
{
const CATEGORIE_ACTIVE = 0;
const CATEGORIE_RETIRE = 5;
private $em;
private $logger;
private $doceboApi;
public function __construct(EntityManagerInterface $em, LoggerInterface $logger, NewDoceboApi $doceboApi)
{
$this->em = $em;
$this->logger = $logger;
$this->doceboApi = $doceboApi;
}
/**
* Apprenant ajouté à une branche Docebo : création / mise à jour + inscription au(x) projet(s)
*/
public function addToBranch(int $userId, int $branchId, ?string $username = null, ?array $details = null): array
{
$projets = $this->em->getRepository(Projet::class)->findBy(['idProjet' => $branchId]);
if (!count($projets)) {
return $this->result('branch.user.added', $userId, $branchId, ['status' => 'no_projet']);
}
$apprenant = $this->em->getRepository(Apprenant::class)->findOneBy(['idUser' => $userId]);
if (!$apprenant) {
$apprenant = new Apprenant();
$apprenant->setIdUser($userId);
$apprenant->setUsername($username);
$apprenant->setPermissionAppel(0);
$apprenant->setType(0);
$apprenant->setPriority(0);
$apprenant->setCategorie(0);
$this->em->persist($apprenant);
}
$details = $details ?? $this->fetchUser($userId);
if ($details) {
$this->hydrate($apprenant, $details['user_data'], $details['additional_fields'] ?? []);
} else {
$this->logger->warning('[sync-learners] Détails Docebo indisponibles, apprenant synchronisé sans détails', [
'user_id' => $userId,
'branch_id' => $branchId,
]);
}
$apprenant->setDateModif(date("Y-m-d\TH:i:s", strtotime("+1 hour")));
$projetIds = [];
foreach ($projets as $projet) {
$apprenant->setProjet($projet);
$inscription = $apprenant->getId()
? $this->em->getRepository(Inscription::class)->findOneBy(['projet' => $projet, 'apprenant' => $apprenant])
: null;
if (!$inscription) {
$inscription = new Inscription();
$inscription->setApprenant($apprenant);
$inscription->setProjet($projet);
$inscription->setCategorie(self::CATEGORIE_ACTIVE);
$this->em->persist($inscription);
} elseif ($inscription->getCategorie() == self::CATEGORIE_RETIRE) {
$inscription->setCategorie(self::CATEGORIE_ACTIVE);
}
$projetIds[] = $projet->getId();
}
$this->em->flush();
return $this->result('branch.user.added', $userId, $branchId, [
'status' => 'added',
'apprenant' => $apprenant->getId(),
'projets' => $projetIds,
]);
}
/**
* Apprenant retiré d'une branche Docebo : inscription(s) en catégorie 5
*/
public function removeFromBranch(int $userId, int $branchId): array
{
$projets = $this->em->getRepository(Projet::class)->findBy(['idProjet' => $branchId]);
if (!count($projets)) {
return $this->result('branch.user.removed', $userId, $branchId, ['status' => 'no_projet']);
}
$apprenant = $this->em->getRepository(Apprenant::class)->findOneBy(['idUser' => $userId]);
if (!$apprenant) {
return $this->result('branch.user.removed', $userId, $branchId, ['status' => 'unknown_user']);
}
$projetIds = [];
foreach ($projets as $projet) {
$inscription = $this->em->getRepository(Inscription::class)->findOneBy(['projet' => $projet, 'apprenant' => $apprenant]);
if ($inscription) {
$inscription->setCategorie(self::CATEGORIE_RETIRE);
}
if ($apprenant->getProjet() === $projet) {
$apprenant->setProjet(null);
}
$projetIds[] = $projet->getId();
}
$apprenant->setDateModif(date("Y-m-d\TH:i:s", strtotime("+1 hour")));
$this->em->flush();
return $this->result('branch.user.removed', $userId, $branchId, [
'status' => 'removed',
'apprenant' => $apprenant->getId(),
'projets' => $projetIds,
]);
}
/**
* Utilisateur modifié dans Docebo : mise à jour de l'apprenant (sans toucher aux projets / inscriptions)
*/
public function updateUser(int $userId, array $payload = []): array
{
$apprenant = $this->em->getRepository(Apprenant::class)->findOneBy(['idUser' => $userId]);
if (!$apprenant) {
return $this->result('user.updated', $userId, null, ['status' => 'unknown_user']);
}
$details = $this->fetchUser($userId);
if ($details) {
$this->hydrate($apprenant, $details['user_data'], $details['additional_fields'] ?? []);
} else {
// Fallback : uniquement les champs de base présents dans le payload
$this->logger->warning('[sync-learners] Détails Docebo indisponibles, mise à jour depuis le payload', ['user_id' => $userId]);
if (!empty($payload['username'])) {
$apprenant->setUsername($payload['username']);
}
if (!empty($payload['firstname'])) {
$apprenant->setFirstname($payload['firstname']);
}
if (!empty($payload['lastname'])) {
$apprenant->setLastname($payload['lastname']);
}
if (!empty($payload['email'])) {
$apprenant->setEmail($payload['email']);
}
}
$apprenant->setDateModif(date("Y-m-d\TH:i:s", strtotime("+1 hour")));
$this->em->flush();
return $this->result('user.updated', $userId, null, [
'status' => 'updated',
'apprenant' => $apprenant->getId(),
]);
}
/**
* Utilisateur créé dans Docebo : le payload ne contient pas de branche,
* on lit ses branches via l'API et on l'inscrit aux projets correspondants
*/
public function createUser(int $userId, array $payload = []): array
{
$details = $this->fetchUser($userId);
if (!$details) {
return $this->result('user.created', $userId, null, ['status' => 'details_unavailable']);
}
$projetRepository = $this->em->getRepository(Projet::class);
$branches = [];
foreach (array_unique(array_column($details['branches'] ?? [], 'id')) as $branchId) {
if ($projetRepository->findOneBy(['idProjet' => (int) $branchId])) {
$branches[] = $this->addToBranch($userId, (int) $branchId, $payload['username'] ?? null, $details);
}
}
if (!count($branches)) {
return $this->result('user.created', $userId, null, [
'status' => 'no_projet',
'docebo_branches' => array_column($details['branches'] ?? [], 'id'),
]);
}
return $this->result('user.created', $userId, null, [
'status' => 'created',
'apprenant' => $branches[0]['apprenant'] ?? null,
'branches' => $branches,
]);
}
/**
* Branche créée dans Docebo :
* - parent_id = 0 : nouveau client (comme /update/clients)
* - parent_id = client : nouveau projet (comme /update/projets)
*/
public function branchCreated(int $branchId, int $parentId, ?string $code, ?string $name): array
{
$label = trim((string) $name) !== '' ? trim($name) : (trim((string) $code) !== '' ? trim($code) : 'Branche ' . $branchId);
$now = date("Y-m-d\TH:i:s", strtotime("+1 hour"));
if ($parentId === 0) {
$client = $this->em->getRepository(Client::class)->findOneBy(['idOrg' => $branchId]);
if ($client) {
return $this->result('branch.created', null, $branchId, ['status' => 'client_exists', 'client' => $client->getId()]);
}
$stat = new Statistique();
$this->initStats($stat);
$stat->setDateMaj($now);
$this->em->persist($stat);
$client = new Client();
$client->setIdOrg($branchId);
$client->setCode($code);
$client->setNom($label);
$client->setNombreProjet(0);
$client->setStatus(2);
$client->setStatistique($stat);
$client->setDateMaj($now);
$this->em->persist($client);
$this->em->flush();
return $this->result('branch.created', null, $branchId, ['status' => 'client_created', 'client' => $client->getId()]);
}
$client = $this->em->getRepository(Client::class)->findOneBy(['idOrg' => $parentId]);
if (!$client) {
return $this->result('branch.created', null, $branchId, ['status' => 'parent_not_client', 'parent_id' => $parentId]);
}
$projet = $this->em->getRepository(Projet::class)->findOneBy(['idProjet' => $branchId]);
if ($projet) {
return $this->result('branch.created', null, $branchId, ['status' => 'projet_exists', 'projet' => $projet->getId()]);
}
$stat = new StatProjet();
$this->initStats($stat);
$this->em->persist($stat);
$projet = new Projet();
$projet->setIdProjet($branchId);
$projet->setCode($label);
$projet->setNom($label);
$projet->setClient($client);
$projet->setStatProjet($stat);
$projet->setActif(1);
$projet->setMajApprenants(1);
$projet->setDateMaj($now);
$this->em->persist($projet);
$client->setNombreProjet((int) $client->getNombreProjet() + 1);
$this->em->flush();
return $this->result('branch.created', null, $branchId, [
'status' => 'projet_created',
'projet' => $projet->getId(),
'client' => $client->getId(),
]);
}
/**
* Statistiques à 0 pour une branche qui vient d'être créée
* @param Statistique|StatProjet $stat
*/
private function initStats($stat): void
{
$stat->setTotalUsers(0);
$stat->setCourseEnrollments(0);
$stat->setCourseEnrollmentsNotStarted(0);
$stat->setCourseEnrollmentsInProgress(0);
$stat->setCourseEnrollmentsCompleted(0);
$stat->setCourseEnrollmentsExpired(0);
}
private function fetchUser(int $userId): ?array
{
try {
$response = $this->doceboApi->useDoceboApi('/manage/v1/user/' . $userId, null);
} catch (\Throwable $e) {
$this->logger->error('[sync-learners] Erreur API Docebo', ['user_id' => $userId, 'error' => $e->getMessage()]);
return null;
}
return isset($response['data']['user_data']) ? $response['data'] : null;
}
/**
* Même mapping que ProjetController::importApprenantsAction
*/
private function hydrate(Apprenant $apprenant, array $userData, array $additionalFields): void
{
$fields = [];
foreach ($additionalFields as $additionalField) {
if (isset($additionalField['title'])) {
$fields[$additionalField['title']] = $additionalField['value'] ?? "";
}
}
$gsm = str_replace('(0)', '', (string) ($fields['GSM'] ?? ""));
$gsm2 = (string) ($fields['GSM2'] ?? "");
if (strlen($gsm) == 9) {
$gsm = "0" . $gsm;
}
if (strlen($gsm2) == 9) {
$gsm2 = "0" . $gsm2;
}
if (!empty($userData['username'])) {
$apprenant->setUsername($userData['username']);
}
$apprenant->setFirstname($userData['first_name'] ?? null);
$apprenant->setLastname($userData['last_name'] ?? null);
$apprenant->setEmail($userData['email'] ?? null);
$apprenant->setGsm($gsm);
$apprenant->setGsm2($gsm2);
$apprenant->setDirection($fields['Direction'] ?? "");
$apprenant->setPassword($fields['mdp text'] ?? "");
$apprenant->setProfil($fields['Profil'] ?? "");
$apprenant->setPoste($fields['Poste'] ?? "");
$apprenant->setFonction($fields['Fonction'] ?? "");
$vip = $fields['VIP'] ?? null;
$apprenant->setType(($vip == "1" || $vip == "2") ? (int) $vip : 0);
}
private function result(string $event, ?int $userId, ?int $branchId, array $result): array
{
$context = ['event' => $event, 'user_id' => $userId, 'branch_id' => $branchId] + $result;
if (in_array($result['status'], ['added', 'removed', 'updated', 'created', 'client_created', 'projet_created'])) {
$this->logger->info('[sync-learners] ' . $result['status'], $context);
} else {
$this->logger->warning('[sync-learners] ' . $result['status'], $context);
}
return ['user_id' => $userId, 'branch_id' => $branchId] + $result;
}
}