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 InvalidArgumentException;
|
|
|
|
|
use MintyPHP\App\Module\ModuleManifest;
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
|
|
|
|
|
final class ModuleManifestTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
public function testMinimalManifest(): void
|
|
|
|
|
{
|
|
|
|
|
$manifest = ModuleManifest::fromArray(['id' => 'testmod'], '/tmp/modules/testmod');
|
|
|
|
|
|
|
|
|
|
self::assertSame('testmod', $manifest->id);
|
|
|
|
|
self::assertSame('0.0.0', $manifest->version);
|
|
|
|
|
self::assertFalse($manifest->enabledByDefault);
|
|
|
|
|
self::assertSame(100, $manifest->loadOrder);
|
|
|
|
|
self::assertSame([], $manifest->routes);
|
|
|
|
|
self::assertSame([], $manifest->permissions);
|
|
|
|
|
self::assertSame([], $manifest->searchResources);
|
|
|
|
|
self::assertNull($manifest->migrationsPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testFullManifest(): void
|
|
|
|
|
{
|
|
|
|
|
$raw = [
|
|
|
|
|
'id' => 'addressbook',
|
|
|
|
|
'version' => '1.0.0',
|
|
|
|
|
'enabled_by_default' => true,
|
|
|
|
|
'load_order' => 10,
|
|
|
|
|
'routes' => [['path' => '/address-book', 'target' => 'address-book/index']],
|
|
|
|
|
'public_paths' => [],
|
|
|
|
|
'container_registrars' => ['App\\Registrar'],
|
|
|
|
|
'ui_slots' => ['aside.tab_panel' => [['key' => 'people', 'label' => 'People']]],
|
|
|
|
|
'search_resources' => ['App\\SearchProvider'],
|
|
|
|
|
'asset_groups' => ['address-book' => ['css/view.css']],
|
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
|
|
|
'scheduler_jobs' => [
|
|
|
|
|
[
|
|
|
|
|
'job_key' => 'addressbook.sync',
|
|
|
|
|
'handler' => 'App\\Scheduler\\AddressBookSyncHandler',
|
|
|
|
|
'label' => 'Address book sync',
|
|
|
|
|
'description' => 'Syncs address data',
|
|
|
|
|
'default_enabled' => true,
|
|
|
|
|
'default_timezone' => 'Europe/Berlin',
|
|
|
|
|
'default_schedule_type' => 'daily',
|
|
|
|
|
'default_schedule_interval' => 1,
|
|
|
|
|
'default_schedule_time' => '02:00',
|
|
|
|
|
'default_schedule_weekdays_csv' => null,
|
|
|
|
|
'default_catchup_once' => true,
|
|
|
|
|
'allowed_schedule_types' => ['daily', 'weekly'],
|
|
|
|
|
],
|
|
|
|
|
],
|
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
|
|
|
'layout_context_providers' => ['App\\LayoutProvider'],
|
|
|
|
|
'session_providers' => ['App\\SessionProvider'],
|
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' => 'address_book.view',
|
|
|
|
|
'description' => 'Can view address book',
|
|
|
|
|
'active' => true,
|
|
|
|
|
'is_system' => true,
|
|
|
|
|
],
|
|
|
|
|
],
|
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
|
|
|
'migrations_path' => 'db/updates',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$manifest = ModuleManifest::fromArray($raw, '/srv/modules/addressbook');
|
|
|
|
|
|
|
|
|
|
self::assertSame('addressbook', $manifest->id);
|
|
|
|
|
self::assertSame('1.0.0', $manifest->version);
|
|
|
|
|
self::assertTrue($manifest->enabledByDefault);
|
|
|
|
|
self::assertSame(10, $manifest->loadOrder);
|
|
|
|
|
self::assertCount(1, $manifest->routes);
|
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([[
|
|
|
|
|
'key' => 'address_book.view',
|
|
|
|
|
'description' => 'Can view address book',
|
|
|
|
|
'active' => 1,
|
|
|
|
|
'is_system' => 1,
|
|
|
|
|
]], $manifest->permissions);
|
|
|
|
|
self::assertSame('addressbook.sync', $manifest->schedulerJobs[0]['job_key']);
|
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
|
|
|
self::assertSame('/srv/modules/addressbook/db/updates', $manifest->migrationsPath);
|
|
|
|
|
self::assertSame(['App\\LayoutProvider'], $manifest->layoutContextProviders);
|
|
|
|
|
self::assertSame(['App\\SessionProvider'], $manifest->sessionProviders);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testMissingIdThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->expectException(InvalidArgumentException::class);
|
|
|
|
|
$this->expectExceptionMessage('non-empty string "id"');
|
|
|
|
|
|
|
|
|
|
ModuleManifest::fromArray([], '/tmp/modules/broken');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEmptyIdThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->expectException(InvalidArgumentException::class);
|
|
|
|
|
|
|
|
|
|
ModuleManifest::fromArray(['id' => ' '], '/tmp/modules/broken');
|
|
|
|
|
}
|
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 testInvalidPermissionContractThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->expectException(InvalidArgumentException::class);
|
|
|
|
|
$this->expectExceptionMessage("must define non-empty 'key' and 'description'");
|
|
|
|
|
|
|
|
|
|
ModuleManifest::fromArray([
|
|
|
|
|
'id' => 'mod-invalid-perm',
|
|
|
|
|
'permissions' => [['key' => 'mod.permission']],
|
|
|
|
|
], '/tmp/modules/mod-invalid-perm');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testInvalidSchedulerJobContractThrows(): void
|
|
|
|
|
{
|
|
|
|
|
$this->expectException(InvalidArgumentException::class);
|
|
|
|
|
$this->expectExceptionMessage("missing required key 'allowed_schedule_types'");
|
|
|
|
|
|
|
|
|
|
ModuleManifest::fromArray([
|
|
|
|
|
'id' => 'mod-invalid-job',
|
|
|
|
|
'scheduler_jobs' => [[
|
|
|
|
|
'job_key' => 'mod.job',
|
|
|
|
|
'handler' => 'App\\Handler',
|
|
|
|
|
'label' => 'Label',
|
|
|
|
|
'description' => 'Desc',
|
|
|
|
|
'default_enabled' => true,
|
|
|
|
|
'default_timezone' => 'UTC',
|
|
|
|
|
'default_schedule_type' => 'daily',
|
|
|
|
|
'default_schedule_interval' => 1,
|
|
|
|
|
'default_schedule_time' => '01:00',
|
|
|
|
|
'default_schedule_weekdays_csv' => null,
|
|
|
|
|
'default_catchup_once' => true,
|
|
|
|
|
]],
|
|
|
|
|
], '/tmp/modules/mod-invalid-job');
|
|
|
|
|
}
|
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
|
|
|
}
|