forked from fa/breadcrumb-the-shire
feat: module architecture improvements — session keys, dependency graph, event dispatcher, deactivation hooks
Six targeted improvements to the modular monolith platform: 1. Fix AddressBook session key prefix to follow module.<id>.* convention 2. Move ADDRESS_BOOK_VIEW permission constant from core PermissionService into module 3. Add declarative JSON schema for module manifests (.agents/contracts/) 4. Add `requires` field with missing-dependency and circular-dependency detection 5. Add lightweight fire-and-forget event dispatcher (user.created/deleted/login/logout) 6. Add module deactivation hook interface and CLI script (bin/module-deactivate.php) Includes 15 new architecture/unit tests covering all new functionality. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,9 @@ final class ModuleRegistry
|
||||
/** @var array<string, string> scheduler job key => owning module id */
|
||||
private array $schedulerJobOwners = [];
|
||||
|
||||
/** @var array<string, list<array{class: class-string, method: string}>> merged event listeners keyed by event name */
|
||||
private array $mergedEventListeners = [];
|
||||
|
||||
/**
|
||||
* Boot the registry from a modules directory and activation config.
|
||||
*
|
||||
@@ -150,6 +153,7 @@ final class ModuleRegistry
|
||||
$registry->registerModule($manifest);
|
||||
}
|
||||
|
||||
$registry->validateDependencies();
|
||||
$registry->finalizeMergedContributions();
|
||||
|
||||
return $registry;
|
||||
@@ -354,6 +358,14 @@ final class ModuleRegistry
|
||||
$this->mergedAuthorizationPolicies[] = $policyClass;
|
||||
}
|
||||
|
||||
// — Merge event listeners ——————————————————————————
|
||||
foreach ($manifest->eventListeners as $eventName => $handlers) {
|
||||
if (!isset($this->mergedEventListeners[$eventName])) {
|
||||
$this->mergedEventListeners[$eventName] = [];
|
||||
}
|
||||
array_push($this->mergedEventListeners[$eventName], ...$handlers);
|
||||
}
|
||||
|
||||
// — Merge scheduler jobs —————————————————————————
|
||||
foreach ($manifest->schedulerJobs as $schedulerJob) {
|
||||
$jobKey = trim((string) $schedulerJob['job_key']);
|
||||
@@ -388,6 +400,52 @@ final class ModuleRegistry
|
||||
}
|
||||
}
|
||||
|
||||
private function validateDependencies(): void
|
||||
{
|
||||
$enabledIds = array_keys($this->modules);
|
||||
|
||||
// Check all required modules are enabled
|
||||
foreach ($this->modules as $manifest) {
|
||||
foreach ($manifest->requires as $requiredId) {
|
||||
if (!isset($this->modules[$requiredId])) {
|
||||
throw new RuntimeException(
|
||||
"Module '{$manifest->id}' requires module '{$requiredId}' which is not enabled."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for circular dependencies (DFS cycle detection)
|
||||
$visited = [];
|
||||
$inStack = [];
|
||||
|
||||
$visit = function (string $moduleId) use (&$visit, &$visited, &$inStack): void {
|
||||
if (isset($inStack[$moduleId])) {
|
||||
throw new RuntimeException(
|
||||
"Circular module dependency detected involving module '{$moduleId}'."
|
||||
);
|
||||
}
|
||||
if (isset($visited[$moduleId])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$inStack[$moduleId] = true;
|
||||
|
||||
if (isset($this->modules[$moduleId])) {
|
||||
foreach ($this->modules[$moduleId]->requires as $requiredId) {
|
||||
$visit($requiredId);
|
||||
}
|
||||
}
|
||||
|
||||
unset($inStack[$moduleId]);
|
||||
$visited[$moduleId] = true;
|
||||
};
|
||||
|
||||
foreach ($enabledIds as $moduleId) {
|
||||
$visit($moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
private function finalizeMergedContributions(): void
|
||||
{
|
||||
$this->mergedPublicPaths = array_values(array_unique(array_filter(
|
||||
@@ -626,4 +684,10 @@ final class ModuleRegistry
|
||||
{
|
||||
return $this->mergedSchedulerJobs;
|
||||
}
|
||||
|
||||
/** @return array<string, list<array{class: class-string, method: string}>> */
|
||||
public function getEventListeners(): array
|
||||
{
|
||||
return $this->mergedEventListeners;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user