collectDesiredPermissions(); } catch (InvalidArgumentException $e) { $io->error($e->getMessage()); return Command::FAILURE; } // Etape 2 : upsert transactionnel non destructif. $this->em->beginTransaction(); try { // Indexation des permissions existantes par code pour un acces O(1). $existingByCode = []; foreach ($this->permissionRepository->findAll() as $permission) { $existingByCode[$permission->getCode()] = $permission; } $added = 0; $updated = 0; $orphans = 0; // Upsert : chaque entree desiree est creee, revivee ou mise a jour. foreach ($desiredPermissions as $code => $entry) { $label = $entry['label']; $module = $entry['module']; if (isset($existingByCode[$code])) { $existing = $existingByCode[$code]; if ($existing->isOrphan()) { // Revival : le code reapparait dans le source, on // rafraichit ses metadonnees et on retire le flag. $existing->revive($label, $module); ++$updated; } elseif ($existing->getLabel() !== $label || $existing->getModule() !== $module) { // Mise a jour des metadonnees sans toucher au flag orphan. $existing->updateMetadata($label, $module); ++$updated; } // Sinon : strictement identique, no-op. } else { // Creation : on persiste directement via l'EM pour ne // pas declencher un flush par appel (cf. save() repo). $permission = new Permission($code, $label, $module); $this->em->persist($permission); ++$added; } } // Etape 3 : marquage orphelin des permissions absentes du source. foreach ($existingByCode as $code => $existing) { if (isset($desiredPermissions[$code])) { continue; } if (!$existing->isOrphan()) { $existing->markOrphan(); ++$orphans; } } // Un unique flush regroupe toutes les mutations de la transaction. $this->em->flush(); $this->em->commit(); } catch (Throwable $e) { $this->em->rollback(); $io->error(sprintf('Echec de la synchronisation des permissions : %s', $e->getMessage())); return Command::FAILURE; } $totalInDb = count($this->permissionRepository->findAll()); $io->success('Synchronisation des permissions RBAC terminee.'); $io->table( ['Indicateur', 'Valeur'], [ ['Permissions ajoutees', (string) $added], ['Permissions mises a jour ou revivees', (string) $updated], ['Permissions marquees orphelines', (string) $orphans], ['Total en base apres sync', (string) $totalInDb], ], ); return Command::SUCCESS; } /** * Parcourt la liste des modules actifs declares dans `config/modules.php`, * extrait leurs permissions statiques, valide strictement chaque entree * puis renvoie une map indexee par code. * * Regles de validation appliquees : * - chaque entree doit posseder exactement les cles `code` et `label` * - le `code` doit etre prefixe par `::ID . '.'` * - `code` et `label` ne peuvent pas etre des chaines vides * * Les modules ne definissant pas de methode statique `permissions()` sont * ignores silencieusement (compat ascendante pour les modules legacy). * * @return array */ private function collectDesiredPermissions(): array { /** @var array $moduleClasses */ $moduleClasses = require $this->projectDir.'/config/modules.php'; $desired = []; foreach ($moduleClasses as $moduleClass) { if (!method_exists($moduleClass, 'permissions')) { continue; } /** @var array> $entries */ $entries = $moduleClass::permissions(); $moduleId = $moduleClass::ID; foreach ($entries as $entry) { $keys = array_keys($entry); sort($keys); if (['code', 'label'] !== $keys) { throw new InvalidArgumentException(sprintf( 'Permission malformee declaree par %s : chaque entree doit contenir exactement les cles [code, label], recu [%s].', $moduleClass, implode(', ', array_keys($entry)), )); } $code = $entry['code']; $label = $entry['label']; if ('' === $code) { throw new InvalidArgumentException(sprintf( 'Permission invalide declaree par %s : le code ne peut pas etre vide.', $moduleClass, )); } if ('' === $label) { throw new InvalidArgumentException(sprintf( 'Permission invalide declaree par %s (code "%s") : le libelle ne peut pas etre vide.', $moduleClass, $code, )); } $expectedPrefix = $moduleId.'.'; if (!str_starts_with($code, $expectedPrefix)) { throw new InvalidArgumentException(sprintf( 'Permission invalide declaree par %s : le code "%s" doit etre prefixe par "%s" (ID du module).', $moduleClass, $code, $expectedPrefix, )); } $desired[$code] = [ 'code' => $code, 'label' => $label, 'module' => $moduleId, ]; } } return $desired; } }