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>
2026-03-18 15:43:25 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace MintyPHP\Tests\App\Module;
|
|
|
|
|
|
|
|
|
|
use MintyPHP\App\Module\ModuleRegistry;
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
use RuntimeException;
|
|
|
|
|
|
|
|
|
|
final class ModuleRegistryTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
private string $fixturesDir;
|
|
|
|
|
|
|
|
|
|
protected function setUp(): void
|
|
|
|
|
{
|
|
|
|
|
$this->fixturesDir = sys_get_temp_dir() . '/corecore-module-test-' . uniqid();
|
|
|
|
|
mkdir($this->fixturesDir, 0777, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function tearDown(): void
|
|
|
|
|
{
|
|
|
|
|
$this->removeDir($this->fixturesDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBootWithNoModules(): void
|
|
|
|
|
{
|
|
|
|
|
$registry = ModuleRegistry::boot($this->fixturesDir, []);
|
|
|
|
|
|
|
|
|
|
self::assertSame([], $registry->getModules());
|
|
|
|
|
self::assertSame([], $registry->getRoutes());
|
|
|
|
|
self::assertSame([], $registry->getPermissions());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBootWithNonExistentDir(): void
|
|
|
|
|
{
|
|
|
|
|
$registry = ModuleRegistry::boot(sys_get_temp_dir() . '/does-not-exist-' . uniqid(), ['foo']);
|
|
|
|
|
|
|
|
|
|
self::assertSame([], $registry->getModules());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBootLoadsSingleModule(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('testmod', [
|
|
|
|
|
'id' => 'testmod',
|
|
|
|
|
'version' => '1.0.0',
|
|
|
|
|
'load_order' => 10,
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
'permissions' => [
|
|
|
|
|
['key' => 'test.view', 'description' => 'Test view'],
|
|
|
|
|
['key' => 'test.edit', 'description' => 'Test edit'],
|
|
|
|
|
],
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
'search_resources' => [],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$registry = ModuleRegistry::boot($this->fixturesDir, ['testmod']);
|
|
|
|
|
|
|
|
|
|
self::assertTrue($registry->hasModule('testmod'));
|
|
|
|
|
self::assertSame('1.0.0', $registry->getModule('testmod')->version);
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
self::assertSame(['test.view', 'test.edit'], $registry->getPermissionKeys());
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeterministicOrdering(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('beta', ['id' => 'beta', 'load_order' => 20]);
|
|
|
|
|
$this->createModuleManifest('alpha', ['id' => 'alpha', 'load_order' => 10]);
|
|
|
|
|
$this->createModuleManifest('gamma', ['id' => 'gamma', 'load_order' => 10]);
|
|
|
|
|
|
|
|
|
|
$registry = ModuleRegistry::boot($this->fixturesDir, ['beta', 'alpha', 'gamma']);
|
|
|
|
|
$ids = array_keys($registry->getModules());
|
|
|
|
|
|
|
|
|
|
// load_order 10: alpha, gamma (alpha < gamma by id); then load_order 20: beta
|
|
|
|
|
self::assertSame(['alpha', 'gamma', 'beta'], $ids);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDuplicateModuleIdThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('dup', ['id' => 'dup']);
|
|
|
|
|
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
|
$this->expectExceptionMessage("Duplicate module id: 'dup'");
|
|
|
|
|
|
|
|
|
|
// Pass same ID twice — registry should fail-fast
|
|
|
|
|
ModuleRegistry::boot($this->fixturesDir, ['dup', 'dup']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testPermissionConflictThrows(): void
|
|
|
|
|
{
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
$this->createModuleManifest('mod-a', [
|
|
|
|
|
'id' => 'mod-a',
|
|
|
|
|
'load_order' => 1,
|
|
|
|
|
'permissions' => [['key' => 'shared.perm', 'description' => 'Shared permission']],
|
|
|
|
|
]);
|
|
|
|
|
$this->createModuleManifest('mod-b', [
|
|
|
|
|
'id' => 'mod-b',
|
|
|
|
|
'load_order' => 2,
|
|
|
|
|
'permissions' => [['key' => 'shared.perm', 'description' => 'Shared permission duplicate']],
|
|
|
|
|
]);
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
|
$this->expectExceptionMessage("Permission conflict: 'shared.perm'");
|
|
|
|
|
|
|
|
|
|
ModuleRegistry::boot($this->fixturesDir, ['mod-a', 'mod-b']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRouteConflictThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('mod-a', [
|
|
|
|
|
'id' => 'mod-a',
|
|
|
|
|
'load_order' => 1,
|
|
|
|
|
'routes' => [['path' => '/shared', 'target' => 'shared/index']],
|
|
|
|
|
]);
|
|
|
|
|
$this->createModuleManifest('mod-b', [
|
|
|
|
|
'id' => 'mod-b',
|
|
|
|
|
'load_order' => 2,
|
|
|
|
|
'routes' => [['path' => '/shared-alt', 'target' => 'shared/index']],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
|
$this->expectExceptionMessage('Route target conflict');
|
|
|
|
|
|
|
|
|
|
ModuleRegistry::boot($this->fixturesDir, ['mod-a', 'mod-b']);
|
|
|
|
|
}
|
|
|
|
|
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
public function testRoutePathConflictThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('mod-a', [
|
|
|
|
|
'id' => 'mod-a',
|
|
|
|
|
'load_order' => 1,
|
|
|
|
|
'routes' => [['path' => '/shared-path', 'target' => 'a/index']],
|
|
|
|
|
]);
|
|
|
|
|
$this->createModuleManifest('mod-b', [
|
|
|
|
|
'id' => 'mod-b',
|
|
|
|
|
'load_order' => 2,
|
|
|
|
|
'routes' => [['path' => '/shared-path', 'target' => 'b/index']],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
|
$this->expectExceptionMessage('Route path conflict');
|
|
|
|
|
|
|
|
|
|
ModuleRegistry::boot($this->fixturesDir, ['mod-a', 'mod-b']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testUiSlotConflictThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$panelTemplateA = $this->createFixtureFile('mod-a-panel.phtml', '<ul></ul>');
|
|
|
|
|
$panelTemplateB = $this->createFixtureFile('mod-b-panel.phtml', '<ul></ul>');
|
|
|
|
|
|
|
|
|
|
$this->createModuleManifest('mod-a', [
|
|
|
|
|
'id' => 'mod-a',
|
|
|
|
|
'load_order' => 1,
|
|
|
|
|
'ui_slots' => [
|
|
|
|
|
'aside.tab_panel' => [
|
|
|
|
|
[
|
|
|
|
|
'key' => 'people',
|
|
|
|
|
'label' => 'People',
|
|
|
|
|
'icon' => 'bi-people',
|
|
|
|
|
'permission' => 'can_view_people',
|
|
|
|
|
'panel_template' => $panelTemplateA,
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
$this->createModuleManifest('mod-b', [
|
|
|
|
|
'id' => 'mod-b',
|
|
|
|
|
'load_order' => 2,
|
|
|
|
|
'ui_slots' => [
|
|
|
|
|
'aside.tab_panel' => [
|
|
|
|
|
[
|
|
|
|
|
'key' => 'people',
|
|
|
|
|
'label' => 'People B',
|
|
|
|
|
'icon' => 'bi-people',
|
|
|
|
|
'permission' => 'can_view_people',
|
|
|
|
|
'panel_template' => $panelTemplateB,
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
|
$this->expectExceptionMessage("UI slot conflict: slot 'aside.tab_panel' key 'people'");
|
|
|
|
|
|
|
|
|
|
ModuleRegistry::boot($this->fixturesDir, ['mod-a', 'mod-b']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testInvalidRouteContractThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('mod-a', [
|
|
|
|
|
'id' => 'mod-a',
|
|
|
|
|
'routes' => [['path' => '', 'target' => 'address-book/index']],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
2026-04-01 19:41:56 +02:00
|
|
|
$this->expectExceptionMessage('Manifest schema validation failed');
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
|
|
|
|
|
ModuleRegistry::boot($this->fixturesDir, ['mod-a']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testInvalidUiSlotContractThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('mod-a', [
|
|
|
|
|
'id' => 'mod-a',
|
|
|
|
|
'ui_slots' => [
|
|
|
|
|
'aside.tab_panel' => [
|
|
|
|
|
['label' => 'Missing Key'],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
2026-04-01 19:41:56 +02:00
|
|
|
$this->expectExceptionMessage('Manifest schema validation failed');
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
|
|
|
|
|
ModuleRegistry::boot($this->fixturesDir, ['mod-a']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testUiSlotsAreSortedDeterministicallyByOrderModuleAndKey(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('mod-z', [
|
|
|
|
|
'id' => 'mod-z',
|
|
|
|
|
'load_order' => 1,
|
|
|
|
|
'ui_slots' => [
|
|
|
|
|
'search.resource_item' => [
|
|
|
|
|
['key' => 'z-second', 'label' => 'Z Second', 'base_url' => 'z-second', 'permission' => 'can.z', 'order' => 20],
|
|
|
|
|
['key' => 'z-first', 'label' => 'Z First', 'base_url' => 'z-first', 'permission' => 'can.z', 'order' => 20],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
$this->createModuleManifest('mod-a', [
|
|
|
|
|
'id' => 'mod-a',
|
|
|
|
|
'load_order' => 1,
|
|
|
|
|
'ui_slots' => [
|
|
|
|
|
'search.resource_item' => [
|
|
|
|
|
['key' => 'a-mid', 'label' => 'A Mid', 'base_url' => 'a-mid', 'permission' => 'can.a', 'order' => 20],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
$this->createModuleManifest('mod-b', [
|
|
|
|
|
'id' => 'mod-b',
|
|
|
|
|
'load_order' => 1,
|
|
|
|
|
'ui_slots' => [
|
|
|
|
|
'search.resource_item' => [
|
|
|
|
|
['key' => 'b-low', 'label' => 'B Low', 'base_url' => 'b-low', 'permission' => 'can.b', 'order' => 10],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$registry = ModuleRegistry::boot($this->fixturesDir, ['mod-z', 'mod-b', 'mod-a']);
|
|
|
|
|
$slotItems = $registry->getSlotContributions('search.resource_item');
|
|
|
|
|
|
|
|
|
|
self::assertSame(['b-low', 'a-mid', 'z-first', 'z-second'], array_column($slotItems, 'key'));
|
|
|
|
|
foreach ($slotItems as $slotItem) {
|
|
|
|
|
self::assertArrayNotHasKey('__module_id', $slotItem);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
public function testMissingManifestFileThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
|
$this->expectExceptionMessage("manifest not found");
|
|
|
|
|
|
|
|
|
|
ModuleRegistry::boot($this->fixturesDir, ['nonexistent']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testManifestIdMismatchThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('wrongdir', ['id' => 'actualid']);
|
|
|
|
|
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
|
$this->expectExceptionMessage("does not match manifest id 'actualid'");
|
|
|
|
|
|
|
|
|
|
ModuleRegistry::boot($this->fixturesDir, ['wrongdir']);
|
|
|
|
|
}
|
|
|
|
|
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
public function testResolveEnabledModulesFromConfigWhenEnvUnset(): void
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
{
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
putenv('APP_ENABLED_MODULES');
|
|
|
|
|
try {
|
|
|
|
|
$result = ModuleRegistry::resolveEnabledModules([
|
|
|
|
|
'enabled_modules' => ['mod-a', 'mod-b'],
|
|
|
|
|
]);
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
self::assertSame(['mod-a', 'mod-b'], $result);
|
|
|
|
|
} finally {
|
|
|
|
|
putenv('APP_ENABLED_MODULES');
|
|
|
|
|
}
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
}
|
|
|
|
|
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
public function testResolveEnabledModulesFromEnvList(): void
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
{
|
|
|
|
|
putenv('APP_ENABLED_MODULES=env-a,env-b');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$result = ModuleRegistry::resolveEnabledModules([
|
|
|
|
|
'enabled_modules' => ['config-a'],
|
|
|
|
|
]);
|
|
|
|
|
self::assertSame(['env-a', 'env-b'], $result);
|
|
|
|
|
} finally {
|
|
|
|
|
putenv('APP_ENABLED_MODULES');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
public function testResolveEnabledModulesFromEmptyEnvReturnsNoModules(): void
|
|
|
|
|
{
|
|
|
|
|
putenv('APP_ENABLED_MODULES=');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$result = ModuleRegistry::resolveEnabledModules([
|
|
|
|
|
'enabled_modules' => ['config-a', 'config-b'],
|
|
|
|
|
]);
|
|
|
|
|
self::assertSame([], $result);
|
|
|
|
|
} finally {
|
|
|
|
|
putenv('APP_ENABLED_MODULES');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
public function testResolveEnabledModulesDeduplicates(): void
|
|
|
|
|
{
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
putenv('APP_ENABLED_MODULES');
|
|
|
|
|
try {
|
|
|
|
|
$result = ModuleRegistry::resolveEnabledModules([
|
|
|
|
|
'enabled_modules' => ['mod-a', 'mod-a', 'mod-b'],
|
|
|
|
|
]);
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
self::assertSame(['mod-a', 'mod-b'], $result);
|
|
|
|
|
} finally {
|
|
|
|
|
putenv('APP_ENABLED_MODULES');
|
|
|
|
|
}
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testGetNonExistentModuleThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$registry = ModuleRegistry::boot($this->fixturesDir, []);
|
|
|
|
|
|
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
|
|
|
$this->expectExceptionMessage("Module 'nope' is not registered");
|
|
|
|
|
|
|
|
|
|
$registry->getModule('nope');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testMergedContributions(): void
|
|
|
|
|
{
|
|
|
|
|
$this->createModuleManifest('mod-a', [
|
|
|
|
|
'id' => 'mod-a',
|
|
|
|
|
'load_order' => 1,
|
|
|
|
|
'layout_context_providers' => ['ProviderA'],
|
|
|
|
|
'session_providers' => ['SessionA'],
|
|
|
|
|
'search_resources' => ['SearchA'],
|
|
|
|
|
'asset_groups' => ['group-a' => ['a.css']],
|
|
|
|
|
]);
|
|
|
|
|
$this->createModuleManifest('mod-b', [
|
|
|
|
|
'id' => 'mod-b',
|
|
|
|
|
'load_order' => 2,
|
|
|
|
|
'layout_context_providers' => ['ProviderB'],
|
|
|
|
|
'session_providers' => ['SessionB'],
|
|
|
|
|
'search_resources' => ['SearchB'],
|
|
|
|
|
'asset_groups' => ['group-b' => ['b.css']],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$registry = ModuleRegistry::boot($this->fixturesDir, ['mod-a', 'mod-b']);
|
|
|
|
|
|
|
|
|
|
self::assertSame(['ProviderA', 'ProviderB'], $registry->getLayoutContextProviders());
|
|
|
|
|
self::assertSame(['SessionA', 'SessionB'], $registry->getSessionProviders());
|
|
|
|
|
self::assertSame(['SearchA', 'SearchB'], $registry->getSearchResources());
|
|
|
|
|
self::assertSame(['group-a' => ['a.css'], 'group-b' => ['b.css']], $registry->getAssetGroups());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string, mixed> $manifest
|
|
|
|
|
*/
|
|
|
|
|
private function createModuleManifest(string $dirName, array $manifest): void
|
|
|
|
|
{
|
|
|
|
|
$dir = $this->fixturesDir . '/' . $dirName;
|
|
|
|
|
if (!is_dir($dir)) {
|
|
|
|
|
mkdir($dir, 0777, true);
|
|
|
|
|
}
|
|
|
|
|
file_put_contents(
|
|
|
|
|
$dir . '/module.php',
|
|
|
|
|
'<?php return ' . var_export($manifest, true) . ';'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00
|
|
|
private function createFixtureFile(string $relativePath, string $content): string
|
|
|
|
|
{
|
|
|
|
|
$path = $this->fixturesDir . '/' . ltrim($relativePath, '/');
|
|
|
|
|
$dir = dirname($path);
|
|
|
|
|
if (!is_dir($dir)) {
|
|
|
|
|
mkdir($dir, 0777, true);
|
|
|
|
|
}
|
|
|
|
|
file_put_contents($path, $content);
|
|
|
|
|
return $path;
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-03-18 15:43:25 +01:00
|
|
|
private function removeDir(string $dir): void
|
|
|
|
|
{
|
|
|
|
|
if (!is_dir($dir)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$items = new \RecursiveIteratorIterator(
|
|
|
|
|
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
|
|
|
|
|
\RecursiveIteratorIterator::CHILD_FIRST
|
|
|
|
|
);
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
if ($item->isDir()) {
|
|
|
|
|
rmdir($item->getPathname());
|
|
|
|
|
} else {
|
|
|
|
|
unlink($item->getPathname());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
rmdir($dir);
|
|
|
|
|
}
|
|
|
|
|
}
|