feat: introduce module system and extract address book as first module (MODULAR-MONOLITH-V1-001)
Add a module kernel (ModuleManifest, ModuleRegistry) that allows modules to contribute routes, UI slots, search providers, layout context, session lifecycle, and permissions. Modules are activated via config/modules.php or APP_ENABLED_MODULES env variable. Conflicts (duplicate routes, permissions, slot keys) cause a fail-fast with a clear error message. Extract the address book from hardcoded Core integration points into the first module (modules/addressbook/). The module provides: - Aside icon-bar tab + People panel via UI slot system - Global search resource via AddressBookSearchProvider - Layout context data via AddressBookLayoutProvider - Session lifecycle via AddressBookSessionProvider Core cleanup removes address-book hardcodings from SearchSqlResourceProvider, SearchUiMetaProvider, SearchItemMapperProvider, appBuildLayoutNavContext(), and the aside templates. Permissions (ADDRESS_BOOK_VIEW) and business logic (AddressBookService) remain in Core as they gate general user visibility. Includes 38 new tests (894 total), PHPStan level 5 clean, and architecture tests verifying zero hardcoded address-book references in search/templates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,9 +59,6 @@ function setAppContainer(\MintyPHP\App\AppContainer $container): void
|
||||
static fn (): \MintyPHP\Service\Access\AuthorizationService => $container->get(\MintyPHP\Service\Access\AuthorizationService::class)
|
||||
);
|
||||
|
||||
\MintyPHP\Support\Search\SearchItemMapperProvider::configure(
|
||||
static fn (): \MintyPHP\Service\User\UserAvatarService => $container->get(\MintyPHP\Service\User\UserAvatarService::class)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -610,8 +607,19 @@ function appBuildLayoutNavContext(array $layoutAuth, array $session, array $quer
|
||||
$csrfKey = \MintyPHP\Session::$csrfSessionKey;
|
||||
$csrfToken = (string) ($session[$csrfKey] ?? '');
|
||||
|
||||
return [
|
||||
// Pre-resolve module UI slot contributions for templates (templates must not call app() directly)
|
||||
$moduleUiSlots = [];
|
||||
try {
|
||||
/** @var \MintyPHP\App\Module\ModuleRegistry $moduleReg */
|
||||
$moduleReg = app(\MintyPHP\App\Module\ModuleRegistry::class);
|
||||
$moduleUiSlots = $moduleReg->getUiSlots();
|
||||
} catch (\Throwable) {
|
||||
// fail-open
|
||||
}
|
||||
|
||||
$layoutNav = [
|
||||
'hasAdminPanel' => layoutHasAdminPanel($layoutAuth),
|
||||
'moduleSlots' => $moduleUiSlots,
|
||||
'currentTenant' => $currentTenant,
|
||||
'availableTenants' => $availableTenants,
|
||||
'tenantQueryParam' => $tenantQueryParam,
|
||||
@@ -620,19 +628,33 @@ function appBuildLayoutNavContext(array $layoutAuth, array $session, array $quer
|
||||
'name' => $tenantName,
|
||||
'hasAvatar' => $tenantHasAvatar,
|
||||
],
|
||||
'addressBook' => [
|
||||
'url' => lurl('address-book'),
|
||||
'activeSearch' => trim((string) ($query['search'] ?? '')),
|
||||
'activeTenants' => appNormalizeStringList($query['tenants'] ?? ''),
|
||||
'activeDepartments' => appNormalizePositiveIntList($query['departments'] ?? ''),
|
||||
'activeRoles' => appNormalizePositiveIntList($query['roles'] ?? ''),
|
||||
'activeCustomFilters' => appNormalizeAddressBookCustomFilterQuery($query),
|
||||
'peopleGroups' => is_array($session['available_departments_by_tenant'] ?? null) ? $session['available_departments_by_tenant'] : [],
|
||||
],
|
||||
'bookmarks' => is_array($session['user_bookmarks'] ?? null)
|
||||
? $session['user_bookmarks']
|
||||
: ['groups' => [], 'ungrouped' => []],
|
||||
'csrfKey' => $csrfKey,
|
||||
'csrfToken' => $csrfToken,
|
||||
];
|
||||
|
||||
// ── Module layout context providers ──────────────────────────────
|
||||
// Modules can contribute additional layout data via LayoutContextProvider.
|
||||
// Each provider returns a key-value array that is merged into $layoutNav.
|
||||
try {
|
||||
/** @var \MintyPHP\App\Module\ModuleRegistry $moduleRegistry */
|
||||
$moduleRegistry = app(\MintyPHP\App\Module\ModuleRegistry::class);
|
||||
/** @var \MintyPHP\App\AppContainer $container */
|
||||
$container = $GLOBALS['minty_app_container'];
|
||||
foreach ($moduleRegistry->getLayoutContextProviders() as $providerClass) {
|
||||
if (!class_exists($providerClass)) {
|
||||
continue;
|
||||
}
|
||||
$provider = new $providerClass();
|
||||
if ($provider instanceof \MintyPHP\App\Module\Contracts\LayoutContextProvider) {
|
||||
$layoutNav = array_merge($layoutNav, $provider->provide($session, $container));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// fail-open: if module registry is not available, skip module providers
|
||||
}
|
||||
|
||||
return $layoutNav;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user