philippurbschat.de/home/app/services/ModuleService.php
Philipp Urbschat f393beb7ff
feat: inkubator-standard & modul-autodiscovery
- Dynamische Modulerkennung via module.json in ModuleService.php
- Kaskadierender Env-Loader fuer zentrale API-Keys (GEMINI_API_KEY)
- Graceful Auth fuer alle Subprojekte (dinos, plants, storymachine, test)
- Wiederverwendbares Starter-Template in _template/
- test-Modul auf philippurbschat.de Dark-Design modernisiert
2026-09-13 00:23:16 +02:00

151 lines
5.2 KiB
PHP

<?php
/**
* ModuleService - Dynamische Auto-Discovery für Subprojekte & Module
*
* Jedes Verzeichnis im Root mit einer `module.json` wird automatisch als Modul registriert
* und steht sofort in der Benutzer- und Rechteverwaltung zur Verfügung.
*/
class ModuleService {
private static ?array $cachedModules = null;
// Fallback-Definitionen, falls Dateisystem-Zugriff blockiert ist
private static array $defaultFallback = [
'dinos' => [
'slug' => 'dinos',
'name' => 'Dino Generator',
'badge' => 'AI Art',
'description' => 'KI Dino-Bilderstellung & Google Photos Sync',
'path' => '/dinos',
'adminOnly' => false,
'order' => 1
],
'plants' => [
'slug' => 'plants',
'name' => 'Plant Tracker',
'badge' => 'AI Vision',
'description' => 'Pflanzen-Tracking, Pflege & KI-Erkennung',
'path' => '/plants',
'adminOnly' => false,
'order' => 2
],
'storymachine' => [
'slug' => 'storymachine',
'name' => 'Story Machine',
'badge' => 'Creative',
'description' => 'Interaktive Kinder-Geschichtenmaschine',
'path' => '/storymachine',
'adminOnly' => false,
'order' => 3
],
'test' => [
'slug' => 'test',
'name' => 'Test & Lab',
'badge' => 'Audio',
'description' => 'Modul-Showcase & Web Audio Synthesizer',
'path' => '/test',
'adminOnly' => false,
'order' => 4
]
];
/**
* Ermittelt den Webroot-Pfad der Domain
*/
private static function getRootPath(): string {
return dirname(__DIR__, 3);
}
/**
* Liest alle Module dynamisch aus dem Dateisystem
*/
public static function getAll(bool $refresh = false): array {
if (self::$cachedModules !== null && !$refresh) {
return self::$cachedModules;
}
$rootPath = self::getRootPath();
$modules = [];
// Ignorierte Systemverzeichnisse
$ignoredDirs = ['.git', '.vscode', 'home', 'philcore', 'node_modules', 'vendor', '_template'];
if (is_dir($rootPath)) {
$entries = scandir($rootPath);
if ($entries !== false) {
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..' || in_array($entry, $ignoredDirs, true)) {
continue;
}
$moduleDir = $rootPath . '/' . $entry;
if (!is_dir($moduleDir)) {
continue;
}
$descriptorFile = $moduleDir . '/module.json';
if (file_exists($descriptorFile)) {
$jsonContent = @file_get_contents($descriptorFile);
$data = $jsonContent ? json_decode($jsonContent, true) : null;
if (is_array($data)) {
// Ignorieren, falls explizit deaktiviert
if (isset($data['enabled']) && $data['enabled'] === false) {
continue;
}
$modules[$entry] = [
'slug' => $entry,
'name' => $data['name'] ?? ucfirst($entry),
'badge' => $data['badge'] ?? 'Tool',
'description' => $data['description'] ?? '',
'path' => '/' . $entry,
'adminOnly' => !empty($data['adminOnly']),
'order' => isset($data['order']) ? (int)$data['order'] : 999
];
}
}
}
}
}
// Falls keine Module gefunden wurden, Fallback nutzen
if (empty($modules)) {
$modules = self::$defaultFallback;
}
// Sortierung nach 'order', dann nach Name
uasort($modules, function ($a, $b) {
$orderA = $a['order'] ?? 999;
$orderB = $b['order'] ?? 999;
if ($orderA !== $orderB) {
return $orderA <=> $orderB;
}
return strcasecmp($a['name'] ?? '', $b['name'] ?? '');
});
self::$cachedModules = $modules;
return self::$cachedModules;
}
public static function getBySlug(string $slug): ?array {
$all = self::getAll();
return $all[$slug] ?? null;
}
public static function getAccessibleForUser(array $userProjects, bool $isAdmin = false): array {
$accessible = [];
foreach (self::getAll() as $slug => $module) {
if ($isAdmin) {
$accessible[$slug] = $module;
} elseif (in_array($slug, $userProjects, true) && empty($module['adminOnly'])) {
$accessible[$slug] = $module;
}
}
return $accessible;
}
public static function isValidModule(string $slug): bool {
$all = self::getAll();
return isset($all[$slug]);
}
}