1
0

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>
This commit is contained in:
2026-03-18 22:19:56 +01:00
parent c364e2b46d
commit c7b8fd516a
139 changed files with 7591 additions and 2535 deletions

View File

@@ -35,10 +35,32 @@ final class ModuleManifestTest extends TestCase
'ui_slots' => ['aside.tab_panel' => [['key' => 'people', 'label' => 'People']]],
'search_resources' => ['App\\SearchProvider'],
'asset_groups' => ['address-book' => ['css/view.css']],
'scheduler_jobs' => [],
'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'],
],
],
'layout_context_providers' => ['App\\LayoutProvider'],
'session_providers' => ['App\\SessionProvider'],
'permissions' => ['address_book.view'],
'permissions' => [
[
'key' => 'address_book.view',
'description' => 'Can view address book',
'active' => true,
'is_system' => true,
],
],
'migrations_path' => 'db/updates',
];
@@ -49,7 +71,13 @@ final class ModuleManifestTest extends TestCase
self::assertTrue($manifest->enabledByDefault);
self::assertSame(10, $manifest->loadOrder);
self::assertCount(1, $manifest->routes);
self::assertSame(['address_book.view'], $manifest->permissions);
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']);
self::assertSame('/srv/modules/addressbook/db/updates', $manifest->migrationsPath);
self::assertSame(['App\\LayoutProvider'], $manifest->layoutContextProviders);
self::assertSame(['App\\SessionProvider'], $manifest->sessionProviders);
@@ -69,4 +97,38 @@ final class ModuleManifestTest extends TestCase
ModuleManifest::fromArray(['id' => ' '], '/tmp/modules/broken');
}
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');
}
}