1
0

refactor: simplify module platform — drop custom autoloader, layout_capabilities indirection, and absolute template paths

- Replace ModuleAutoloader with Composer ClassLoader (addPsr4 via spl_autoload_functions)
- Eliminate layout_capabilities mapping; UI slots reference ability strings directly
- Resolve relative template paths in ModuleRegistry instead of baking __DIR__ into manifests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 08:26:45 +01:00
parent 5739cc1200
commit cb27fe090d
7 changed files with 115 additions and 144 deletions

View File

@@ -229,38 +229,21 @@ final class ModuleStructureContractTest extends TestCase
}
}
// ─── Layout capabilities ─────────────────────────────────────────
// ─── UI slot abilities ─────────────────────────────────────────
public function testLayoutCapabilitiesHaveNonEmptyKeysAndAbilities(): void
/**
* Every 'permission' value in a module's UI slots must reference an ability
* declared as a constant on one of the module's authorization policies.
*/
public function testUiSlotAbilitiesMatchPolicySupports(): void
{
foreach (self::$modules as $module) {
foreach ($module['manifest']->layoutCapabilities as $uiKey => $ability) {
self::assertNotEmpty(
trim((string) $uiKey),
"Module '{$module['id']}' has empty layout_capabilities key"
);
self::assertNotEmpty(
trim((string) $ability),
"Module '{$module['id']}' layout_capabilities key '{$uiKey}' has empty ability"
);
}
}
}
public function testLayoutCapabilityAbilitiesMatchPolicySupports(): void
{
foreach (self::$modules as $module) {
if (empty($module['manifest']->layoutCapabilities)) {
continue;
}
// Collect all abilities supported by this module's policies
$supportedAbilities = [];
foreach ($module['manifest']->authorizationPolicies as $policyClass) {
if (!class_exists($policyClass)) {
continue;
}
// Check each layout capability ability against this policy
$reflection = new \ReflectionClass($policyClass);
foreach ($reflection->getConstants() as $constValue) {
if (is_string($constValue)) {
@@ -269,13 +252,28 @@ final class ModuleStructureContractTest extends TestCase
}
}
foreach ($module['manifest']->layoutCapabilities as $uiKey => $ability) {
self::assertContains(
$ability,
$supportedAbilities,
"Module '{$module['id']}' layout_capabilities['{$uiKey}'] references ability '{$ability}' "
. "but no module authorization policy declares it as a constant"
);
if ($supportedAbilities === []) {
continue;
}
// Check every permission value from UI slots
foreach ($module['manifest']->uiSlots as $slotName => $contributions) {
$contributionList = is_array($contributions) ? $contributions : [$contributions];
foreach ($contributionList as $idx => $contribution) {
if (!is_array($contribution)) {
continue;
}
$ability = trim((string) ($contribution['permission'] ?? ''));
if ($ability === '') {
continue;
}
self::assertContains(
$ability,
$supportedAbilities,
"Module '{$module['id']}' ui_slots['{$slotName}'][{$idx}] permission '{$ability}' "
. "is not declared as a constant on any module authorization policy"
);
}
}
}
}
@@ -410,9 +408,14 @@ final class ModuleStructureContractTest extends TestCase
if ($template === null) {
continue;
}
// Resolve relative paths against module basePath
$templatePath = (string) $template;
if (!str_starts_with($templatePath, '/')) {
$templatePath = $module['manifest']->basePath . '/' . $templatePath;
}
self::assertFileExists(
(string) $template,
"Module '{$module['id']}' {$slotName} {$templateKey} does not exist: {$template}"
$templatePath,
"Module '{$module['id']}' {$slotName} {$templateKey} does not exist: {$templatePath}"
);
}
}