295 lines
9.1 KiB
PHP
295 lines
9.1 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
final class ModuleManager
|
|
{
|
|
private array $modules = [];
|
|
private array $callbacks = [];
|
|
|
|
public function __construct(
|
|
private ?\PDO $basePdo,
|
|
private string $modulesPath
|
|
) {
|
|
if ($this->basePdo) {
|
|
BaseSchema::ensure($this->basePdo);
|
|
}
|
|
$this->scanModules();
|
|
}
|
|
|
|
public function all(): array
|
|
{
|
|
return $this->modules;
|
|
}
|
|
|
|
public function get(string $name): ?array
|
|
{
|
|
return $this->modules[$name] ?? null;
|
|
}
|
|
|
|
public function isEnabled(string $name): bool
|
|
{
|
|
$module = $this->get($name);
|
|
return (bool)($module['enabled'] ?? false);
|
|
}
|
|
|
|
public function setEnabled(string $name, bool $enabled): void
|
|
{
|
|
if (!$this->basePdo) {
|
|
return;
|
|
}
|
|
|
|
$module = $this->get($name);
|
|
if (!$module) {
|
|
return;
|
|
}
|
|
|
|
$stmt = $this->basePdo->prepare(
|
|
"INSERT INTO nexus_modules (name, title, version, enabled, installed_at, updated_at)
|
|
VALUES (:name, :title, :version, :enabled, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
enabled = excluded.enabled,
|
|
title = excluded.title,
|
|
version = excluded.version,
|
|
updated_at = CURRENT_TIMESTAMP"
|
|
);
|
|
|
|
$stmt->execute([
|
|
'name' => $name,
|
|
'title' => (string)($module['title'] ?? $name),
|
|
'version' => (string)($module['version'] ?? ''),
|
|
'enabled' => $enabled ? 1 : 0,
|
|
]);
|
|
|
|
$this->modules[$name]['enabled'] = $enabled;
|
|
}
|
|
|
|
public function settings(string $name): array
|
|
{
|
|
if (!$this->basePdo) {
|
|
return [];
|
|
}
|
|
|
|
$stmt = $this->basePdo->prepare(
|
|
"SELECT settings FROM nexus_module_settings WHERE name = :name LIMIT 1"
|
|
);
|
|
$stmt->execute(['name' => $name]);
|
|
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
if (!$row || empty($row['settings'])) {
|
|
return [];
|
|
}
|
|
|
|
$decoded = json_decode((string)$row['settings'], true);
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
public function saveSettings(string $name, array $settings): void
|
|
{
|
|
if (!$this->basePdo) {
|
|
return;
|
|
}
|
|
|
|
$payload = json_encode($settings, JSON_UNESCAPED_UNICODE);
|
|
if ($payload === false) {
|
|
return;
|
|
}
|
|
|
|
$stmt = $this->basePdo->prepare(
|
|
"INSERT INTO nexus_module_settings (name, settings, updated_at)
|
|
VALUES (:name, :settings, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
settings = excluded.settings,
|
|
updated_at = CURRENT_TIMESTAMP"
|
|
);
|
|
$stmt->execute([
|
|
'name' => $name,
|
|
'settings' => $payload,
|
|
]);
|
|
}
|
|
|
|
public function modulePdo(string $name, array $fallback = []): ?\PDO
|
|
{
|
|
$settings = $this->settings($name);
|
|
$db = $settings['db'] ?? $fallback;
|
|
if (!is_array($db) || empty($db)) {
|
|
throw new ModuleConfigException(
|
|
$name,
|
|
'Modul nicht konfiguriert. Bitte Setup ausfuehren.'
|
|
);
|
|
}
|
|
|
|
if (!isset($db['options'])) {
|
|
$db['options'] = [
|
|
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
|
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
|
|
];
|
|
}
|
|
|
|
try {
|
|
$pdo = Database::createFromArray($db);
|
|
} catch (\Throwable $e) {
|
|
if (defined('APP_DEBUG_TOOL') && APP_DEBUG_TOOL) {
|
|
@file_put_contents(
|
|
__DIR__ . '/../../debug/module_db_error.log',
|
|
'[' . date('c') . '] ' . $name . ': ' . $e->getMessage() . PHP_EOL,
|
|
FILE_APPEND
|
|
);
|
|
}
|
|
throw new ModuleConfigException(
|
|
$name,
|
|
'Modul-Datenbank nicht korrekt konfiguriert.',
|
|
$e->getMessage(),
|
|
0,
|
|
$e
|
|
);
|
|
}
|
|
|
|
if ($name === 'kea' && !empty($settings['kea_auto_init'])) {
|
|
try {
|
|
Database::ensureKeaSchema($pdo, [
|
|
'auto_init' => true,
|
|
'init_cmd' => $settings['kea_init_cmd'] ?? null,
|
|
'init_script' => $settings['kea_init_script'] ?? null,
|
|
'kea_db_version' => $settings['kea_db_version'] ?? '',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
if (defined('APP_DEBUG_TOOL') && APP_DEBUG_TOOL) {
|
|
@file_put_contents(
|
|
__DIR__ . '/../../debug/module_db_error.log',
|
|
'[' . date('c') . '] ' . $name . ': ' . $e->getMessage() . PHP_EOL,
|
|
FILE_APPEND
|
|
);
|
|
}
|
|
throw new ModuleConfigException(
|
|
$name,
|
|
'Modul-Datenbank nicht korrekt konfiguriert.',
|
|
$e->getMessage(),
|
|
0,
|
|
$e
|
|
);
|
|
}
|
|
}
|
|
|
|
return $pdo;
|
|
}
|
|
|
|
public function resolvePage(string $name, string $page): ?string
|
|
{
|
|
$module = $this->get($name);
|
|
if (!$module) {
|
|
return null;
|
|
}
|
|
|
|
if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $page)) {
|
|
return null;
|
|
}
|
|
|
|
$path = $module['path'] . '/pages/' . $page . '.php';
|
|
if (is_file($path)) {
|
|
return $path;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function bootEnabled(): void
|
|
{
|
|
foreach ($this->modules as $name => $module) {
|
|
if (!empty($module['enabled'])) {
|
|
$bootstrap = $module['path'] . '/bootstrap.php';
|
|
if (is_file($bootstrap)) {
|
|
$modules = $this;
|
|
require_once $bootstrap;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function registerFunction(string $module, string $name, callable $fn): void
|
|
{
|
|
$key = $module . ':' . $name;
|
|
$this->callbacks[$key] = $fn;
|
|
}
|
|
|
|
public function call(string $module, string $name, mixed ...$args): mixed
|
|
{
|
|
$key = $module . ':' . $name;
|
|
if (!isset($this->callbacks[$key])) {
|
|
throw new \RuntimeException("Module callback not found: {$key}");
|
|
}
|
|
return ($this->callbacks[$key])(...$args);
|
|
}
|
|
|
|
private function scanModules(): void
|
|
{
|
|
$this->modules = [];
|
|
if (!is_dir($this->modulesPath)) {
|
|
if (defined('APP_DEBUG_TOOL') && APP_DEBUG_TOOL) {
|
|
@file_put_contents(__DIR__ . '/../../debug/module_scan.log', 'Modules path not found: ' . $this->modulesPath . PHP_EOL, FILE_APPEND);
|
|
}
|
|
return;
|
|
}
|
|
|
|
foreach (glob($this->modulesPath . '/*', GLOB_ONLYDIR) as $dir) {
|
|
$name = basename($dir);
|
|
$manifest = $dir . '/module.json';
|
|
if (!is_file($manifest)) {
|
|
continue;
|
|
}
|
|
|
|
$raw = file_get_contents($manifest);
|
|
$data = $raw ? json_decode($raw, true) : null;
|
|
if (!is_array($data)) {
|
|
continue;
|
|
}
|
|
|
|
$module = [
|
|
'name' => $name,
|
|
'title' => $data['title'] ?? $name,
|
|
'version' => $data['version'] ?? '',
|
|
'description' => $data['description'] ?? '',
|
|
'setup' => $data['setup'] ?? [],
|
|
'menu' => $data['menu'] ?? [],
|
|
'sidebar' => $data['sidebar'] ?? [],
|
|
'db_defaults' => $data['db_defaults'] ?? [],
|
|
'path' => $dir,
|
|
'enabled' => false,
|
|
];
|
|
|
|
$module['enabled'] = $this->loadEnabledState($name, $module);
|
|
$this->modules[$name] = $module;
|
|
}
|
|
|
|
if (defined('APP_DEBUG_TOOL') && APP_DEBUG_TOOL) {
|
|
@file_put_contents(__DIR__ . '/../../debug/module_scan.log', 'Modules loaded: ' . implode(', ', array_keys($this->modules)) . PHP_EOL, FILE_APPEND);
|
|
}
|
|
}
|
|
|
|
private function loadEnabledState(string $name, array $module): bool
|
|
{
|
|
if (!$this->basePdo) {
|
|
return false;
|
|
}
|
|
|
|
$stmt = $this->basePdo->prepare(
|
|
"SELECT enabled FROM nexus_modules WHERE name = :name LIMIT 1"
|
|
);
|
|
$stmt->execute(['name' => $name]);
|
|
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
if ($row !== false) {
|
|
return (bool)$row['enabled'];
|
|
}
|
|
|
|
$stmt = $this->basePdo->prepare(
|
|
"INSERT INTO nexus_modules (name, title, version, enabled, installed_at, updated_at)
|
|
VALUES (:name, :title, :version, :enabled, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
|
|
);
|
|
$stmt->bindValue(':name', $name, \PDO::PARAM_STR);
|
|
$stmt->bindValue(':title', (string)$module['title'], \PDO::PARAM_STR);
|
|
$stmt->bindValue(':version', (string)$module['version'], \PDO::PARAM_STR);
|
|
$stmt->bindValue(':enabled', false, \PDO::PARAM_BOOL);
|
|
$stmt->execute();
|
|
return false;
|
|
}
|
|
}
|