myCRM/src/Command/ModuleRemoveCommand.php
olli 00b42e4a4c feat: Add module removal command (app:module:remove)
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 <noreply@anthropic.com>
2025-12-14 18:04:10 +01:00

204 lines
7.9 KiB
PHP

<?php
namespace App\Command;
use App\Entity\Module;
use App\Plugin\LicenseValidatorInterface;
use App\Plugin\ModuleRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* CLI-Command zum sicheren Entfernen von Modulen
*/
#[AsCommand(
name: 'app:module:remove',
description: 'Entfernt ein Modul sicher aus dem System',
)]
class ModuleRemoveCommand extends Command
{
public function __construct(
private readonly ModuleRegistry $moduleRegistry,
private readonly LicenseValidatorInterface $licenseValidator,
private readonly EntityManagerInterface $entityManager
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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: <comment>composer remove %s</comment>', $packageName);
}
// Bundle aus config/bundles.php entfernen
$nextSteps[] = '2. Bundle aus <comment>config/bundles.php</comment> entfernen (falls vorhanden)';
// Migrationen
$nextSteps[] = '3. Optional: Migrationen zurückrollen (falls benötigt)';
$nextSteps[] = ' <comment>php bin/console doctrine:migrations:migrate prev</comment>';
// Cache leeren
$nextSteps[] = '4. Cache leeren: <comment>php bin/console cache:clear</comment>';
$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[] = ['<info>Plugin-Informationen</info>', ''];
$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[] = ['<info>Datenbank-Informationen (Permission-System)</info>', ''];
$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);
}
}