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

@@ -2,6 +2,7 @@
namespace MintyPHP\App\Module;
use Composer\Autoload\ClassLoader;
use RuntimeException;
/**
@@ -66,9 +67,6 @@ final class ModuleRegistry
/** @var list<class-string> merged authorization policy FQCNs */
private array $mergedAuthorizationPolicies = [];
/** @var array<string, string> merged layout capabilities: UI-key → ability */
private array $mergedLayoutCapabilities = [];
/**
* @var list<array{
* job_key: string,
@@ -154,8 +152,9 @@ final class ModuleRegistry
return $a->loadOrder <=> $b->loadOrder ?: strcmp($a->id, $b->id);
});
// Allow module-local classes (modules/<id>/lib/...) to autoload.
ModuleAutoloader::register($manifests);
// Register module lib dirs with Composer's PSR-4 autoloader so that
// module classes (MintyPHP\Module\<Name>\*) resolve without a custom loader.
self::registerModuleAutoloading($manifests);
// Register and merge
foreach ($manifests as $manifest) {
@@ -192,6 +191,33 @@ final class ModuleRegistry
return array_values(array_unique(array_filter($fromConfig, static fn (string $id): bool => trim($id) !== '')));
}
/**
* Register module lib/ directories with Composer's ClassLoader.
*
* @param list<ModuleManifest> $manifests
*/
private static function registerModuleAutoloading(array $manifests): void
{
$composerLoader = null;
foreach (spl_autoload_functions() as $autoloader) {
if (is_array($autoloader) && $autoloader[0] instanceof ClassLoader) {
$composerLoader = $autoloader[0];
break;
}
}
if ($composerLoader === null) {
return;
}
foreach ($manifests as $manifest) {
$libDir = rtrim($manifest->basePath, '/') . '/lib';
if (is_dir($libDir)) {
$composerLoader->addPsr4('MintyPHP\\', [$libDir]);
}
}
}
private function registerModule(ModuleManifest $manifest): void
{
if (isset($this->modules[$manifest->id])) {
@@ -240,7 +266,7 @@ final class ModuleRegistry
}
$contributionList = is_array($contributions) ? $contributions : [$contributions];
foreach ($contributionList as $contribution) {
$normalizedContribution = $this->normalizeUiSlotContribution($slotName, $contribution, $manifest->id);
$normalizedContribution = $this->normalizeUiSlotContribution($slotName, $contribution, $manifest->id, $manifest->basePath);
$key = (string) $normalizedContribution['key'];
foreach ($this->mergedUiSlots[$slotName] as $existing) {
$existingKey = is_array($existing) ? ($existing['key'] ?? null) : null;
@@ -339,23 +365,6 @@ final class ModuleRegistry
$this->mergedAuthorizationPolicies[] = $policyClass;
}
// — Merge layout capabilities (fail-fast on duplicate key) ———
foreach ($manifest->layoutCapabilities as $uiKey => $ability) {
$uiKey = trim((string) $uiKey);
$ability = trim((string) $ability);
if ($uiKey === '' || $ability === '') {
throw new RuntimeException(
"Layout capability in module '{$manifest->id}' must have non-empty key and ability."
);
}
if (isset($this->mergedLayoutCapabilities[$uiKey])) {
throw new RuntimeException(
"Layout capability conflict: key '{$uiKey}' from module '{$manifest->id}' is already registered."
);
}
$this->mergedLayoutCapabilities[$uiKey] = $ability;
}
// — Merge scheduler jobs —————————————————————————
foreach ($manifest->schedulerJobs as $schedulerJob) {
$jobKey = trim((string) $schedulerJob['job_key']);
@@ -450,7 +459,7 @@ final class ModuleRegistry
/**
* @return array<string, mixed>
*/
private function normalizeUiSlotContribution(string $slotName, mixed $rawContribution, string $moduleId): array
private function normalizeUiSlotContribution(string $slotName, mixed $rawContribution, string $moduleId, string $basePath): array
{
if (!is_array($rawContribution)) {
throw new RuntimeException(
@@ -465,6 +474,17 @@ final class ModuleRegistry
);
}
// Resolve relative template paths to absolute using the module's basePath.
foreach (['panel_template', 'template'] as $templateKey) {
if (!isset($rawContribution[$templateKey])) {
continue;
}
$templatePath = trim((string) $rawContribution[$templateKey]);
if ($templatePath !== '' && !str_starts_with($templatePath, '/')) {
$rawContribution[$templateKey] = rtrim($basePath, '/') . '/' . $templatePath;
}
}
foreach (self::UI_SLOT_REQUIRED_KEYS[$slotName] ?? [] as $requiredKey) {
$requiredValue = trim((string) ($rawContribution[$requiredKey] ?? ''));
if ($requiredValue === '') {
@@ -660,10 +680,26 @@ final class ModuleRegistry
return $this->mergedAuthorizationPolicies;
}
/** @return array<string, string> UI-capability-key → ability-string */
public function getLayoutCapabilities(): array
/**
* Collect unique ability strings from all module UI slot contributions.
*
* Used by UiAccessService to resolve module abilities into the layout auth map.
* Each ability maps to itself (key = ability, value = ability).
*
* @return array<string, string>
*/
public function getModuleUiAbilities(): array
{
return $this->mergedLayoutCapabilities;
$abilities = [];
foreach ($this->mergedUiSlots as $items) {
foreach ($items as $item) {
$ability = trim((string) ($item['permission'] ?? ''));
if ($ability !== '') {
$abilities[$ability] = $ability;
}
}
}
return $abilities;
}
/**