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:
2026-03-18 15:43:25 +01:00
parent d9805c45d3
commit c364e2b46d
33 changed files with 2639 additions and 167 deletions

View File

@@ -12,7 +12,6 @@ class SearchSqlResourceProvider
{
return [
'users' => 'and exists (select 1 from user_tenants ut where ut.user_id = users.id and ut.tenant_id in (???))',
'address-book' => 'and exists (select 1 from user_tenants ut where ut.user_id = users.id and ut.tenant_id in (???))',
'tenants' => 'and id in (???)',
'departments' => 'and tenant_id in (???)',
];
@@ -23,18 +22,6 @@ class SearchSqlResourceProvider
$like = SearchQueryNormalizer::normalizeLikeQuery($query);
return [
[
'key' => 'address-book',
'label' => t('Address book'),
'permission' => PermissionService::ADDRESS_BOOK_VIEW,
// escape '\\\\' — four backslashes: PHP reduces to two, SQL reduces to one literal backslash escape char.
'countSql' => "select count(*) from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}}",
'countParams' => [$like, $like, $like],
'previewSql' => "select uuid, first_name, last_name, email from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name limit ?",
'previewParams' => [$like, $like, $like],
'resultSql' => "select uuid, first_name, last_name, email from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name",
'resultParams' => [$like, $like, $like],
],
[
'key' => 'users',
'label' => t('Users'),