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

@@ -8,6 +8,7 @@ use MintyPHP\App\Module\Contracts\LayoutContextProvider;
use MintyPHP\App\Module\Contracts\ModuleDeactivationHandler;
use MintyPHP\App\Module\Contracts\SearchResourceProvider;
use MintyPHP\App\Module\Contracts\SessionProvider;
use MintyPHP\App\Module\ModuleEvents;
use MintyPHP\App\Module\ModuleManifestLoader;
use MintyPHP\Console\Command;
use MintyPHP\Service\Access\AuthorizationPolicyInterface;
@@ -292,9 +293,63 @@ final class ValidateCommand extends Command
}
}
// 20. AuthorizationPolicy classes referenced in ContainerRegistrar source
$registrarSources = $this->loadRegistrarSources($moduleDir, $manifest->containerRegistrars);
foreach ($manifest->authorizationPolicies as $policyClass) {
if (!class_exists($policyClass)) {
continue; // Already reported by check 9
}
$shortName = (new \ReflectionClass($policyClass))->getShortName();
$found = false;
foreach ($registrarSources as $source) {
if (str_contains($source, $shortName)) {
$found = true;
break;
}
}
if (!$found) {
$warnings[] = "AuthorizationPolicy '{$shortName}' is not referenced in any ContainerRegistrar — may not be registered in the container";
}
}
// 21. Event listener keys reference known events
$knownEvents = ModuleEvents::all();
foreach ($manifest->eventListeners as $eventName => $handlers) {
if (!in_array($eventName, $knownEvents, true)) {
$warnings[] = "Event listener subscribes to unknown event '{$eventName}' — not defined in ModuleEvents";
}
}
return [$errors, $warnings];
}
/**
* @param list<class-string> $registrarClasses
* @return list<string> Source code of each registrar file
*/
private function loadRegistrarSources(string $moduleDir, array $registrarClasses): array
{
$sources = [];
foreach ($registrarClasses as $class) {
if (!class_exists($class)) {
continue;
}
try {
$refClass = new \ReflectionClass($class);
$file = $refClass->getFileName();
if ($file !== false && is_file($file)) {
$content = file_get_contents($file);
if ($content !== false) {
$sources[] = $content;
}
}
} catch (\ReflectionException) {
// Skip unresolvable classes
}
}
return $sources;
}
/**
* @param list<string> &$errors
*/