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>
204 lines
7.9 KiB
PHP
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);
|
|
}
|
|
}
|