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:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user