feat(modules): harden module platform with event constants, validation checks, and CLI improvements

- Add ModuleEvents constants class with all 8 event strings; update dispatch call sites
- ValidateCommand: check AuthorizationPolicy container registration (check 20)
- ValidateCommand: warn on unknown event_listeners keys (check 21)
- make:module --simple flag for minimal manifests
- module:deactivate reverse-dependency warning
- 12 new PHPUnit tests covering all improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 08:23:04 +02:00
parent 143d887a5c
commit b1a907b79c
10 changed files with 604 additions and 24 deletions

View File

@@ -227,25 +227,31 @@ final class ModuleRunner implements ModuleRunnerInterface
return 1;
}
$manifest = ModuleManifestLoader::loadFromDisk(
ModuleCliRuntime::projectRoot() . '/modules',
$moduleId
);
$modulesDir = ModuleCliRuntime::projectRoot() . '/modules';
$manifest = ModuleManifestLoader::loadFromDisk($modulesDir, $moduleId);
// Check for reverse dependencies (other modules that require this one)
$dependents = $this->findReverseDependencies($modulesDir, $moduleId);
$warning = '';
if ($dependents !== []) {
$dependentList = implode(', ', $dependents);
$warning = "Warning: module(s) [{$dependentList}] declare '{$moduleId}' in their requires — deactivation may break them.\n";
}
$handlerClass = $manifest->deactivationHandler;
if ($handlerClass === null) {
$message = "Module '{$moduleId}' has no deactivation_handler declared — nothing to do.";
$message = $warning . "Module '{$moduleId}' has no deactivation_handler declared — nothing to do.";
return 0;
}
if (!class_exists($handlerClass)) {
$message = "Error: deactivation handler class '{$handlerClass}' not found.";
$message = $warning . "Error: deactivation handler class '{$handlerClass}' not found.";
return 1;
}
$container = $GLOBALS['minty_app_container'] ?? null;
if (!$container instanceof AppContainer) {
$message = 'Error: app container is not initialized.';
$message = $warning . 'Error: app container is not initialized.';
return 1;
}
$handler = $container->has($handlerClass)
@@ -253,17 +259,17 @@ final class ModuleRunner implements ModuleRunnerInterface
: new $handlerClass();
if (!$handler instanceof ModuleDeactivationHandler) {
$message = "Error: '{$handlerClass}' does not implement ModuleDeactivationHandler.";
$message = $warning . "Error: '{$handlerClass}' does not implement ModuleDeactivationHandler.";
return 1;
}
if ($dryRun) {
$message = "Dry-run: handler '{$handlerClass}' found and valid for module '{$moduleId}'.";
$message = $warning . "Dry-run: handler '{$handlerClass}' found and valid for module '{$moduleId}'.";
return 0;
}
$handler->deactivate($container);
$message = "Deactivation handler for module '{$moduleId}' completed.";
$message = $warning . "Deactivation handler for module '{$moduleId}' completed.";
return 0;
});
@@ -345,4 +351,34 @@ final class ModuleRunner implements ModuleRunnerInterface
'message' => $step['error'] ?? $message,
];
}
/**
* @return list<string> Module IDs that declare $targetId in their requires
*/
private function findReverseDependencies(string $modulesDir, string $targetId): array
{
$dependents = [];
$entries = is_dir($modulesDir) ? (scandir($modulesDir) ?: []) : [];
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..' || $entry === $targetId) {
continue;
}
$manifestFile = $modulesDir . '/' . $entry . '/module.php';
if (!is_file($manifestFile)) {
continue;
}
try {
$manifest = ModuleManifestLoader::loadFromDisk($modulesDir, $entry);
if (in_array($targetId, $manifest->requires, true)) {
$dependents[] = $entry;
}
} catch (\Throwable) {
// Skip unparseable manifests
}
}
sort($dependents);
return $dependents;
}
}