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:
57
modules/addressbook/module.php
Normal file
57
modules/addressbook/module.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Address Book module manifest.
|
||||
*
|
||||
* Contributes the "People" aside tab/panel, global search section,
|
||||
* layout context data, and session lifecycle.
|
||||
*
|
||||
* Permissions (address_book.view) remain in Core PermissionService because
|
||||
* they gate general user-visibility in UserAuthorizationPolicy, not just
|
||||
* address-book access. Asset groups remain in config/assets.php because the
|
||||
* asset pipeline does not yet consume ModuleRegistry::getAssetGroups().
|
||||
*/
|
||||
return [
|
||||
'id' => 'addressbook',
|
||||
'version' => '1.0.0',
|
||||
'enabled_by_default' => false,
|
||||
'load_order' => 10,
|
||||
|
||||
'routes' => [],
|
||||
'public_paths' => [],
|
||||
'container_registrars' => [],
|
||||
|
||||
'ui_slots' => [
|
||||
'aside.tab_panel' => [
|
||||
[
|
||||
'key' => 'people',
|
||||
'label' => 'Address book',
|
||||
'icon' => 'bi-people',
|
||||
'href' => 'address-book',
|
||||
'permission' => 'can_view_address_book',
|
||||
'panel_template' => __DIR__ . '/templates/aside-people-panel.phtml',
|
||||
'details_storage' => 'aside-people-tenant',
|
||||
'details_open_active' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'search_resources' => [
|
||||
\MintyPHP\Module\AddressBook\Providers\AddressBookSearchProvider::class,
|
||||
],
|
||||
|
||||
'asset_groups' => [],
|
||||
'scheduler_jobs' => [],
|
||||
|
||||
'layout_context_providers' => [
|
||||
\MintyPHP\Module\AddressBook\Providers\AddressBookLayoutProvider::class,
|
||||
],
|
||||
|
||||
'session_providers' => [
|
||||
\MintyPHP\Module\AddressBook\Providers\AddressBookSessionProvider::class,
|
||||
],
|
||||
|
||||
'permissions' => [],
|
||||
|
||||
'migrations_path' => 'db/updates',
|
||||
];
|
||||
89
modules/addressbook/templates/aside-people-panel.phtml
Normal file
89
modules/addressbook/templates/aside-people-panel.phtml
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Address book aside panel — people browser with tenant/department hierarchy.
|
||||
*
|
||||
* Rendered by the module slot system. The outer <nav> with id, role, aria
|
||||
* attributes is created by the slot renderer in app-main-aside.phtml.
|
||||
* This template provides only the inner content.
|
||||
*
|
||||
* Available in scope via include: $layoutNav, $layoutAuth, $viewAuth
|
||||
*/
|
||||
$addressBook = is_array($layoutNav['addressBook'] ?? null) ? $layoutNav['addressBook'] : [];
|
||||
$addressBookUrl = trim((string) ($addressBook['url'] ?? lurl('address-book')));
|
||||
$activeAddressSearch = trim((string) ($addressBook['activeSearch'] ?? ''));
|
||||
$activeAddressTenants = is_array($addressBook['activeTenants'] ?? null) ? $addressBook['activeTenants'] : [];
|
||||
$activeAddressDepartments = is_array($addressBook['activeDepartments'] ?? null) ? $addressBook['activeDepartments'] : [];
|
||||
$activeAddressRoles = is_array($addressBook['activeRoles'] ?? null) ? $addressBook['activeRoles'] : [];
|
||||
$activeAddressCustomFilters = is_array($addressBook['activeCustomFilters'] ?? null) ? $addressBook['activeCustomFilters'] : [];
|
||||
$peopleGroups = is_array($addressBook['peopleGroups'] ?? null) ? $addressBook['peopleGroups'] : [];
|
||||
$addressBookActive = navActive('address-book', true);
|
||||
?>
|
||||
<ul>
|
||||
<?php if (!$peopleGroups): ?>
|
||||
<li>
|
||||
<a href="<?php e($addressBookUrl); ?>" class="<?php e($addressBookActive['class'] ?? ''); ?>"
|
||||
<?php echo $addressBookActive['aria'] ?? ''; ?>>
|
||||
<?php e(t('Address book')); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<?php foreach ($peopleGroups as $index => $group): ?>
|
||||
<?php
|
||||
$tenant = $group['tenant'] ?? [];
|
||||
$departments = $group['departments'] ?? [];
|
||||
$tenantUuid = (string) ($tenant['uuid'] ?? '');
|
||||
$tenantName = (string) ($tenant['description'] ?? '');
|
||||
if ($tenantUuid === '') {
|
||||
continue;
|
||||
}
|
||||
$baseHref = $addressBookUrl . '?tenants=' . urlencode($tenantUuid);
|
||||
$isActiveTenant = $addressBookActive['isActive']
|
||||
&& !$activeAddressDepartments
|
||||
&& count($activeAddressTenants) === 1
|
||||
&& $activeAddressTenants[0] === $tenantUuid;
|
||||
?>
|
||||
<li class="app-sidebar-group">
|
||||
<small>
|
||||
<a href="<?php e($baseHref); ?>" class="<?php e($isActiveTenant ? 'active' : 'muted'); ?>"
|
||||
<?php echo $isActiveTenant ? 'aria-current="page"' : ''; ?>>
|
||||
<?php e($tenantName); ?>
|
||||
</a>
|
||||
</small>
|
||||
<ul>
|
||||
<?php if (!$departments): ?>
|
||||
<li>
|
||||
<?php
|
||||
$emptyState = [
|
||||
'message' => t('No departments'),
|
||||
'size' => 'small',
|
||||
'align' => 'center',
|
||||
];
|
||||
require templatePath('partials/app-empty-state.phtml');
|
||||
?>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<?php foreach ($departments as $department): ?>
|
||||
<?php
|
||||
$departmentId = (int) ($department['id'] ?? 0);
|
||||
if ($departmentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$departmentName = (string) ($department['description'] ?? '');
|
||||
$href = $baseHref . '&departments=' . $departmentId;
|
||||
$isActive = $addressBookActive['isActive']
|
||||
&& in_array($tenantUuid, $activeAddressTenants, true)
|
||||
&& in_array($departmentId, $activeAddressDepartments, true);
|
||||
?>
|
||||
<li>
|
||||
<a href="<?php e($href); ?>" class="<?php e($isActive ? 'active' : ''); ?>"
|
||||
<?php echo $isActive ? 'aria-current="page"' : ''; ?>>
|
||||
<?php e($departmentName); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
Reference in New Issue
Block a user