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:
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Module\AddressBook\Providers;
|
||||
|
||||
use MintyPHP\App\Module\Contracts\SearchResourceProvider;
|
||||
|
||||
/**
|
||||
* Provides the address-book search resource for the global search.
|
||||
*/
|
||||
final class AddressBookSearchProvider implements SearchResourceProvider
|
||||
{
|
||||
public function resources(): array
|
||||
{
|
||||
return [
|
||||
'address-book' => [
|
||||
'label' => t('Address book'),
|
||||
'permission' => 'address_book.view',
|
||||
'count_sql' => "select count(*) from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}}",
|
||||
'preview_sql' => "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 ?",
|
||||
'result_sql' => "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",
|
||||
'tenant_filter' => 'and exists (select 1 from user_tenants ut where ut.user_id = users.id and ut.tenant_id in (???))',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function mapResultItem(string $resourceKey, array $row): ?array
|
||||
{
|
||||
if ($resourceKey !== 'address-book') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$title = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? ''));
|
||||
$subtitle = (string) ($row['email'] ?? '');
|
||||
$uuid = (string) ($row['uuid'] ?? '');
|
||||
|
||||
if ($title === '' && $subtitle === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $title !== '' ? $title : $subtitle,
|
||||
'subtitle' => $subtitle,
|
||||
'url' => lurl('address-book/view/' . $uuid),
|
||||
'icon' => 'bi-people',
|
||||
];
|
||||
}
|
||||
|
||||
public function listUrl(string $resourceKey, string $encodedSearch): string
|
||||
{
|
||||
if ($resourceKey !== 'address-book') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return lurl('address-book') . '?search=' . $encodedSearch;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user