From 00b42e4a4cca7a3cc8f458eca0ae0942737cefe0 Mon Sep 17 00:00:00 2001 From: olli Date: Sun, 14 Dec 2025 18:04:10 +0100 Subject: [PATCH] feat: Add module removal command (app:module:remove) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementiert einen sicheren Weg zum Entfernen von Modulen aus dem System: Features: - Zeigt Modul-Informationen (Plugin + Datenbank) an - Widerruft Lizenz automatisch (optional mit --keep-license) - Löscht Datenbank-Eintrag im Permission-System (optional mit --keep-db-entry) - Warnt vor verknüpften Permissions - Zeigt nächste manuelle Schritte (Composer, Bundle, Migrations, Cache) - Bestätigung erforderlich (überspringbar mit --force) Dokumentation: - Command-Hilfe in src/Command/ModuleRemoveCommand.php - Benutzer-Anleitung in docs/PLUGIN_QUICKSTART.md - Entwickler-Referenz in CLAUDE.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 18 +++ composer.lock | 82 +++++------ docs/PLUGIN_QUICKSTART.md | 126 +++++++++++++++++ src/Command/ModuleRemoveCommand.php | 203 ++++++++++++++++++++++++++++ 4 files changed, 388 insertions(+), 41 deletions(-) create mode 100644 src/Command/ModuleRemoveCommand.php diff --git a/CLAUDE.md b/CLAUDE.md index 95d63ca..6d40775 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,6 +80,24 @@ php bin/console cache:clear APP_ENV=prod php bin/console cache:warmup ``` +### Module Management (Plugin System) +```bash +# List all installed modules with license status +php bin/console app:module:list + +# Register/manage module license +php bin/console app:module:license billing # Interactive +php bin/console app:module:license billing # Direct +php bin/console app:module:license billing --validate # Validate +php bin/console app:module:license billing --revoke # Revoke + +# Remove a module safely +php bin/console app:module:remove billing # Interactive +php bin/console app:module:remove billing --force # No confirmation +php bin/console app:module:remove billing --keep-license # Keep license +php bin/console app:module:remove billing --keep-db-entry # Keep DB entry +``` + ## Architecture Overview ### Security System (6 Layers) diff --git a/composer.lock b/composer.lock index 1b070d8..4b279d2 100644 --- a/composer.lock +++ b/composer.lock @@ -6578,16 +6578,16 @@ }, { "name": "symfony/monolog-bundle", - "version": "v3.11.0", + "version": "v3.11.1", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bundle.git", - "reference": "e12eb92655b234cd50c21cda648088847a7ec777" + "reference": "0e675a6e08f791ef960dc9c7e392787111a3f0c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/e12eb92655b234cd50c21cda648088847a7ec777", - "reference": "e12eb92655b234cd50c21cda648088847a7ec777", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/0e675a6e08f791ef960dc9c7e392787111a3f0c1", + "reference": "0e675a6e08f791ef960dc9c7e392787111a3f0c1", "shasum": "" }, "require": { @@ -6634,7 +6634,7 @@ ], "support": { "issues": "https://github.com/symfony/monolog-bundle/issues", - "source": "https://github.com/symfony/monolog-bundle/tree/v3.11.0" + "source": "https://github.com/symfony/monolog-bundle/tree/v3.11.1" }, "funding": [ { @@ -6654,7 +6654,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T09:16:19+00:00" + "time": "2025-12-08T07:58:26+00:00" }, { "name": "symfony/notifier", @@ -9797,16 +9797,16 @@ }, { "name": "twig/extra-bundle", - "version": "v3.22.1", + "version": "v3.22.2", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "b6534bc925bec930004facca92fccebd0c809247" + "reference": "09de9be7f6c0d19ede7b5a1dbfcfb2e9d1e0ea9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/b6534bc925bec930004facca92fccebd0c809247", - "reference": "b6534bc925bec930004facca92fccebd0c809247", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/09de9be7f6c0d19ede7b5a1dbfcfb2e9d1e0ea9e", + "reference": "09de9be7f6c0d19ede7b5a1dbfcfb2e9d1e0ea9e", "shasum": "" }, "require": { @@ -9855,7 +9855,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.22.1" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.22.2" }, "funding": [ { @@ -9867,7 +9867,7 @@ "type": "tidelift" } ], - "time": "2025-11-02T11:00:49+00:00" + "time": "2025-12-05T08:51:53+00:00" }, { "name": "twig/twig", @@ -10362,16 +10362,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -10414,9 +10414,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -10538,23 +10538,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.0", + "version": "12.5.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "bca180c050dd3ae15f87c26d25cabb34fe1a0a5a" + "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bca180c050dd3ae15f87c26d25cabb34fe1a0a5a", - "reference": "bca180c050dd3ae15f87c26d25cabb34fe1a0a5a", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c467c59a4f6e04b942be422844e7a6352fa01b57", + "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.6.2", + "nikic/php-parser": "^5.7.0", "php": ">=8.3", "phpunit/php-file-iterator": "^6.0", "phpunit/php-text-template": "^5.0", @@ -10562,10 +10562,10 @@ "sebastian/environment": "^8.0.3", "sebastian/lines-of-code": "^4.0", "sebastian/version": "^6.0", - "theseer/tokenizer": "^1.3.1" + "theseer/tokenizer": "^2.0" }, "require-dev": { - "phpunit/phpunit": "^12.4.4" + "phpunit/phpunit": "^12.5.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -10603,7 +10603,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.0" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.1" }, "funding": [ { @@ -10623,7 +10623,7 @@ "type": "tidelift" } ], - "time": "2025-11-29T07:15:54+00:00" + "time": "2025-12-08T07:17:58+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10872,16 +10872,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.0", + "version": "12.5.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fef037fe50d20ce826cdbd741b7a2afcdec5f45b" + "reference": "6dc2e076d09960efbb0c1272aa9bc156fc80955e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fef037fe50d20ce826cdbd741b7a2afcdec5f45b", - "reference": "fef037fe50d20ce826cdbd741b7a2afcdec5f45b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6dc2e076d09960efbb0c1272aa9bc156fc80955e", + "reference": "6dc2e076d09960efbb0c1272aa9bc156fc80955e", "shasum": "" }, "require": { @@ -10895,7 +10895,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.0", + "phpunit/php-code-coverage": "^12.5.1", "phpunit/php-file-iterator": "^6.0.0", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", @@ -10949,7 +10949,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.0" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.3" }, "funding": [ { @@ -10973,7 +10973,7 @@ "type": "tidelift" } ], - "time": "2025-12-05T04:59:40+00:00" + "time": "2025-12-11T08:52:59+00:00" }, { "name": "sebastian/cli-parser", @@ -12379,23 +12379,23 @@ }, { "name": "theseer/tokenizer", - "version": "1.3.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -12417,7 +12417,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -12425,7 +12425,7 @@ "type": "github" } ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2025-12-08T11:19:18+00:00" } ], "aliases": [], diff --git a/docs/PLUGIN_QUICKSTART.md b/docs/PLUGIN_QUICKSTART.md index c418a50..fe0f930 100644 --- a/docs/PLUGIN_QUICKSTART.md +++ b/docs/PLUGIN_QUICKSTART.md @@ -223,6 +223,132 @@ Navigiere zu: `http://localhost:8000/admin/modules` --- +## 🗑️ Modul entfernen + +### Option A: CLI (empfohlen) + +```bash +# Interaktiv mit Bestätigung +php bin/console app:module:remove billing + +# Ohne Bestätigung (force) +php bin/console app:module:remove billing --force + +# Lizenz behalten +php bin/console app:module:remove billing --keep-license + +# Permission-Datenbank-Eintrag behalten +php bin/console app:module:remove billing --keep-db-entry +``` + +Der Command führt folgende Schritte durch: + +1. ✓ **Modul-Informationen anzeigen** (Plugin + Datenbank) +2. ✓ **Bestätigung einholen** (außer mit `--force`) +3. ✓ **Lizenz widerrufen** (außer mit `--keep-license`) +4. ✓ **Datenbank-Eintrag löschen** (außer mit `--keep-db-entry`) +5. → **Composer-Command vorschlagen** + +### Manuelle Schritte nach app:module:remove + +Der Command zeigt dir die nötigen manuellen Schritte: + +```bash +# 1. Composer-Package entfernen +composer remove mycrm/billing-module + +# 2. Bundle aus config/bundles.php entfernen (falls vorhanden) +# Öffne config/bundles.php und entferne die Zeile: +# MyCRM\BillingModule\BillingBundle::class => ['all' => true], + +# 3. Optional: Migrationen zurückrollen (falls benötigt) +php bin/console doctrine:migrations:migrate prev + +# 4. Cache leeren +php bin/console cache:clear +``` + +### Option B: Vollständig manuell + +Falls du den Command nicht nutzen möchtest: + +```bash +# 1. Lizenz widerrufen +php bin/console app:module:license billing --revoke + +# 2. Composer-Package entfernen +composer remove mycrm/billing-module + +# 3. Bundle aus config/bundles.php entfernen (manuell editieren) + +# 4. Optional: Module-Entity aus DB löschen +# Nur nötig, wenn das Modul auch im Permission-System registriert ist +# VORSICHT: Löscht auch verknüpfte RolePermissions! + +# 5. Cache leeren +php bin/console cache:clear +``` + +### Beispiel-Ausgabe + +``` +Modul "billing" entfernen +========================= + +Modul-Informationen +------------------- + + Eigenschaft Wert + Plugin-Informationen + Name Rechnungsmodul + Version 1.0.0 + Beschreibung Rechnungsverwaltung für myCRM + Lizenziert ✓ Ja + Aktiv ✓ Ja + + Datenbank-Informationen (Permission-System) + Name Rechnungen + Code billing + Aktiv ✓ Ja + Permissions 3 + + ! [NOTE] Möchten Sie dieses Modul wirklich entfernen? (yes/no) [no]: + > yes + +Entferne Modul +-------------- + +⏳ Widerrufe Lizenz... + [OK] ✓ Lizenz erfolgreich widerrufen + +⏳ Entferne Datenbank-Eintrag (Permission-System)... + ! [WARNING] ACHTUNG: Dieses Modul hat 3 verknüpfte Permission(s). Diese werden ebenfalls gelöscht! + ! [NOTE] Trotzdem fortfahren? (yes/no) [no]: + > yes + [OK] ✓ Datenbank-Eintrag erfolgreich entfernt + +Zusammenfassung +--------------- + + * ✓ Lizenz widerrufen + * ✓ Datenbank-Eintrag gelöscht + +Nächste Schritte (manuell) +-------------------------- + + * 1. Composer-Package entfernen: composer remove mycrm/billing-module + * 2. Bundle aus config/bundles.php entfernen (falls vorhanden) + * 3. Optional: Migrationen zurückrollen (falls benötigt) + php bin/console doctrine:migrations:migrate prev + * 4. Cache leeren: php bin/console cache:clear + + [OK] Modul "billing" wurde vorbereitet zum Entfernen. Führen Sie die obigen Schritte manuell aus. +``` + +✅ **Modul sicher entfernt!** + +--- + ## 🔧 Troubleshooting ### Problem: "Modul wird nicht erkannt" diff --git a/src/Command/ModuleRemoveCommand.php b/src/Command/ModuleRemoveCommand.php new file mode 100644 index 0000000..7176801 --- /dev/null +++ b/src/Command/ModuleRemoveCommand.php @@ -0,0 +1,203 @@ +addArgument('module', InputArgument::REQUIRED, 'Modul-Identifier (z.B. "billing")') + ->addOption('keep-license', null, InputOption::VALUE_NONE, 'Lizenz behalten (nicht widerrufen)') + ->addOption('keep-db-entry', null, InputOption::VALUE_NONE, 'Datenbank-Eintrag behalten (Permission-System)') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Keine Bestätigung erforderlich') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $moduleIdentifier = $input->getArgument('module'); + $keepLicense = $input->getOption('keep-license'); + $keepDbEntry = $input->getOption('keep-db-entry'); + $force = $input->getOption('force'); + + $io->title(sprintf('Modul "%s" entfernen', $moduleIdentifier)); + + // 1. Modul-Informationen sammeln + $modulePlugin = $this->moduleRegistry->getModule($moduleIdentifier); + $moduleEntity = $this->findModuleEntity($moduleIdentifier); + + if (!$modulePlugin && !$moduleEntity) { + $io->error(sprintf('Modul "%s" wurde nicht gefunden (weder als Plugin noch in der Datenbank).', $moduleIdentifier)); + return Command::FAILURE; + } + + // 2. Informationen anzeigen + $this->displayModuleInfo($io, $modulePlugin, $moduleEntity); + + // 3. Bestätigung einholen + if (!$force && !$io->confirm('Möchten Sie dieses Modul wirklich entfernen?', false)) { + $io->info('Abgebrochen.'); + return Command::SUCCESS; + } + + $io->section('Entferne Modul'); + + $steps = []; + + // 4. Lizenz widerrufen + if (!$keepLicense && $modulePlugin && $modulePlugin->isLicensed()) { + $io->text('⏳ Widerrufe Lizenz...'); + try { + $this->licenseValidator->revokeLicense($moduleIdentifier); + $io->success('✓ Lizenz erfolgreich widerrufen'); + $steps[] = '✓ Lizenz widerrufen'; + } catch (\Exception $e) { + $io->warning(sprintf('Lizenz konnte nicht widerrufen werden: %s', $e->getMessage())); + $steps[] = '⚠ Lizenz-Widerruf fehlgeschlagen'; + } + } elseif ($keepLicense) { + $io->text('⏭ Lizenz wird behalten (--keep-license)'); + $steps[] = '⏭ Lizenz behalten'; + } else { + $io->text('⏭ Keine Lizenz vorhanden'); + $steps[] = '⏭ Keine Lizenz'; + } + + // 5. Datenbank-Eintrag entfernen + if (!$keepDbEntry && $moduleEntity) { + $io->text('⏳ Entferne Datenbank-Eintrag (Permission-System)...'); + + // Warnung, wenn Permissions existieren + $permissionCount = $moduleEntity->getPermissions()->count(); + if ($permissionCount > 0) { + $io->warning(sprintf( + 'ACHTUNG: Dieses Modul hat %d verknüpfte Permission(s). Diese werden ebenfalls gelöscht!', + $permissionCount + )); + + if (!$force && !$io->confirm('Trotzdem fortfahren?', false)) { + $io->info('Abgebrochen.'); + return Command::SUCCESS; + } + } + + try { + $this->entityManager->remove($moduleEntity); + $this->entityManager->flush(); + $io->success('✓ Datenbank-Eintrag erfolgreich entfernt'); + $steps[] = '✓ Datenbank-Eintrag gelöscht'; + } catch (\Exception $e) { + $io->error(sprintf('Datenbank-Eintrag konnte nicht entfernt werden: %s', $e->getMessage())); + $steps[] = '✗ Datenbank-Eintrag Fehler'; + return Command::FAILURE; + } + } elseif ($keepDbEntry) { + $io->text('⏭ Datenbank-Eintrag wird behalten (--keep-db-entry)'); + $steps[] = '⏭ DB-Eintrag behalten'; + } else { + $io->text('⏭ Kein Datenbank-Eintrag vorhanden'); + $steps[] = '⏭ Kein DB-Eintrag'; + } + + // 6. Nächste Schritte anzeigen + $io->section('Zusammenfassung'); + $io->listing($steps); + + $io->section('Nächste Schritte (manuell)'); + + $nextSteps = []; + + // Composer-Package entfernen + if ($modulePlugin) { + $packageName = $this->guessPackageName($moduleIdentifier); + $nextSteps[] = sprintf('1. Composer-Package entfernen: composer remove %s', $packageName); + } + + // Bundle aus config/bundles.php entfernen + $nextSteps[] = '2. Bundle aus config/bundles.php entfernen (falls vorhanden)'; + + // Migrationen + $nextSteps[] = '3. Optional: Migrationen zurückrollen (falls benötigt)'; + $nextSteps[] = ' php bin/console doctrine:migrations:migrate prev'; + + // Cache leeren + $nextSteps[] = '4. Cache leeren: php bin/console cache:clear'; + + $io->listing($nextSteps); + + $io->success(sprintf('Modul "%s" wurde vorbereitet zum Entfernen. Führen Sie die obigen Schritte manuell aus.', $moduleIdentifier)); + + return Command::SUCCESS; + } + + private function displayModuleInfo(SymfonyStyle $io, $modulePlugin, ?Module $moduleEntity): void + { + $io->section('Modul-Informationen'); + + $rows = []; + + if ($modulePlugin) { + $rows[] = ['Plugin-Informationen', '']; + $rows[] = [' Name', $modulePlugin->getDisplayName()]; + $rows[] = [' Version', $modulePlugin->getVersion()]; + $rows[] = [' Beschreibung', $modulePlugin->getDescription()]; + $rows[] = [' Lizenziert', $modulePlugin->isLicensed() ? '✓ Ja' : '✗ Nein']; + $rows[] = [' Aktiv', $this->moduleRegistry->isModuleActive($modulePlugin->getIdentifier()) ? '✓ Ja' : '✗ Nein']; + } + + if ($moduleEntity) { + if (!empty($rows)) { + $rows[] = ['', '']; + } + $rows[] = ['Datenbank-Informationen (Permission-System)', '']; + $rows[] = [' Name', $moduleEntity->getName()]; + $rows[] = [' Code', $moduleEntity->getCode()]; + $rows[] = [' Aktiv', $moduleEntity->isActive() ? '✓ Ja' : '✗ Nein']; + $rows[] = [' Permissions', (string)$moduleEntity->getPermissions()->count()]; + } + + $io->table(['Eigenschaft', 'Wert'], $rows); + } + + private function findModuleEntity(string $identifier): ?Module + { + return $this->entityManager + ->getRepository(Module::class) + ->findOneBy(['code' => $identifier]); + } + + private function guessPackageName(string $identifier): string + { + // Versuche Package-Name zu erraten (z.B. billing -> mycrm/billing-module) + return sprintf('mycrm/%s-module', $identifier); + } +}