} */ public function syncAll(bool $force = false): array { $stats = [ 'created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => [], ]; foreach ($this->moduleRegistry->getAllModules() as $plugin) { try { $result = $this->syncPlugin($plugin->getIdentifier(), $force); $stats['created'] += $result['created']; $stats['updated'] += $result['updated']; $stats['skipped'] += $result['skipped']; } catch (\Throwable $e) { $error = sprintf( 'Plugin "%s": %s', $plugin->getIdentifier(), $e->getMessage() ); $stats['errors'][] = $error; $this->logger->error($error); } } return $stats; } /** * Synchronisiert die Module eines bestimmten Plugins * * @param string $pluginIdentifier * @param bool $force Synchronisiert auch unlizenzierte Module (für Development) * @return array{created: int, updated: int, skipped: int} */ public function syncPlugin(string $pluginIdentifier, bool $force = false): array { $stats = ['created' => 0, 'updated' => 0, 'skipped' => 0]; $plugin = $this->moduleRegistry->getModule($pluginIdentifier); if (!$plugin) { throw new \RuntimeException(sprintf('Plugin "%s" nicht gefunden', $pluginIdentifier)); } // Nur lizenzierte Module synchronisieren (außer force) if (!$force && !$plugin->isLicensed()) { $this->logger->info(sprintf( 'Plugin "%s" übersprungen: Nicht lizenziert', $pluginIdentifier )); return $stats; } $permissionModules = $plugin->getPermissionModules(); foreach ($permissionModules as $moduleCode) { $result = $this->syncModule( $moduleCode, $plugin->getDisplayName(), $plugin->getDescription() ); $stats[$result]++; } $this->em->flush(); return $stats; } /** * Synchronisiert ein einzelnes Modul * * @param string $code Modul-Code (z.B. 'billing', 'invoicing') * @param string $displayName Display-Name des Plugins * @param string $description Plugin-Beschreibung * @return string 'created', 'updated' oder 'skipped' */ private function syncModule(string $code, string $displayName, string $description): string { $existingModule = $this->moduleRepository->findOneBy(['code' => $code]); if ($existingModule) { // Modul existiert bereits - Update nur wenn nötig $updated = false; if ($existingModule->getDescription() !== $description) { $existingModule->setDescription($description); $updated = true; } if ($updated) { $this->logger->info(sprintf('Modul "%s" aktualisiert', $code)); return 'updated'; } return 'skipped'; } // Neues Modul anlegen $module = new Module(); $module->setCode($code); // Name: Plugin-Name + Code (falls unterschiedlich) $moduleName = $displayName; if (strcasecmp($code, $displayName) !== 0) { $moduleName = sprintf('%s (%s)', $displayName, ucfirst($code)); } $module->setName($moduleName); $module->setDescription($description); $module->setIsActive(true); $module->setSortOrder($this->getNextSortOrder()); // Icon aus Code ableiten (optional) $icon = $this->guessIcon($code); if ($icon) { $module->setIcon($icon); } $this->em->persist($module); $this->logger->info(sprintf('Modul "%s" erstellt', $code)); return 'created'; } /** * Ermittelt die nächste Sort-Order */ private function getNextSortOrder(): int { $maxSortOrder = $this->em->createQuery( 'SELECT MAX(m.sortOrder) FROM App\Entity\Module m' )->getSingleScalarResult(); return ($maxSortOrder ?? 0) + 10; } /** * Versucht, ein passendes Icon zu raten */ private function guessIcon(string $code): ?string { $iconMap = [ 'billing' => 'pi-file-pdf', 'invoicing' => 'pi-receipt', 'inventory' => 'pi-box', 'reporting' => 'pi-chart-bar', 'crm' => 'pi-users', 'test' => 'pi-code', ]; return $iconMap[$code] ?? 'pi-circle'; } }