- Root: README.md, LICENSE, INSTALL.de.md, INSTALL.en.md only - schneespur/: all application source code - Added .gitignore for vendor/, node_modules/, release/, .env, build artifacts - Removed vendor/ and release/ from git tracking (15,699 files) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
6.5 KiB
PHP
227 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Services\Diagnostic\DiagnosticManager;
|
|
use Illuminate\Contracts\Foundation\Application;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\ServiceProvider;
|
|
|
|
class ModuleManager
|
|
{
|
|
protected array $modules = [];
|
|
|
|
protected array $disabledModules = [];
|
|
|
|
protected bool $discovered = false;
|
|
|
|
public function __construct(
|
|
protected Application $app,
|
|
protected string $modulesPath = '',
|
|
) {
|
|
if ($this->modulesPath === '') {
|
|
$this->modulesPath = base_path('modules');
|
|
}
|
|
}
|
|
|
|
public function discover(): void
|
|
{
|
|
$this->modules = [];
|
|
$this->discovered = true;
|
|
|
|
if (! is_dir($this->modulesPath)) {
|
|
return;
|
|
}
|
|
|
|
$dirs = glob($this->modulesPath . '/*/module.json');
|
|
if ($dirs === false) {
|
|
return;
|
|
}
|
|
|
|
foreach ($dirs as $manifestPath) {
|
|
$json = file_get_contents($manifestPath);
|
|
if ($json === false) {
|
|
continue;
|
|
}
|
|
|
|
$manifest = json_decode($json, true);
|
|
if (! is_array($manifest) || empty($manifest['name'])) {
|
|
Log::warning('ModuleManager: invalid module.json', ['path' => $manifestPath]);
|
|
continue;
|
|
}
|
|
|
|
$slug = basename(dirname($manifestPath));
|
|
$manifest['slug'] = $slug;
|
|
$manifest['path'] = dirname($manifestPath);
|
|
|
|
$this->modules[$slug] = $manifest;
|
|
|
|
Log::info('ModuleManager: module discovered', [
|
|
'slug' => $slug,
|
|
'version' => $manifest['version'] ?? 'unknown',
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function registerAutoloader(string $slug, string $namespace, string $path): void
|
|
{
|
|
$namespace = rtrim($namespace, '\\') . '\\';
|
|
$path = rtrim($path, '/') . '/';
|
|
|
|
spl_autoload_register(function (string $class) use ($namespace, $path) {
|
|
if (! str_starts_with($class, $namespace)) {
|
|
return;
|
|
}
|
|
|
|
$relative = substr($class, strlen($namespace));
|
|
$file = $path . str_replace('\\', '/', $relative) . '.php';
|
|
|
|
if (file_exists($file)) {
|
|
require_once $file;
|
|
}
|
|
});
|
|
}
|
|
|
|
public function boot(): void
|
|
{
|
|
if (! $this->discovered) {
|
|
$this->discover();
|
|
}
|
|
|
|
foreach ($this->modules as $slug => $manifest) {
|
|
if (! $this->isEnabled($slug)) {
|
|
continue;
|
|
}
|
|
|
|
$namespace = $manifest['namespace'] ?? null;
|
|
$srcPath = ($manifest['path'] ?? '') . '/src';
|
|
|
|
if ($namespace && is_dir($srcPath)) {
|
|
$this->registerAutoloader($slug, $namespace, $srcPath);
|
|
}
|
|
|
|
$providerClass = $manifest['service_provider'] ?? null;
|
|
if (! $providerClass) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
if (! class_exists($providerClass)) {
|
|
Log::error('ModuleManager: ServiceProvider class not found', [
|
|
'slug' => $slug,
|
|
'class' => $providerClass,
|
|
]);
|
|
$this->autoDisable($slug, "ServiceProvider class not found: {$providerClass}");
|
|
continue;
|
|
}
|
|
|
|
$provider = new $providerClass($this->app);
|
|
if (! $provider instanceof ServiceProvider) {
|
|
Log::error('ModuleManager: class is not a ServiceProvider', [
|
|
'slug' => $slug,
|
|
'class' => $providerClass,
|
|
]);
|
|
$this->autoDisable($slug, "Class is not a ServiceProvider: {$providerClass}");
|
|
continue;
|
|
}
|
|
|
|
$provider->register();
|
|
$provider->boot();
|
|
|
|
Log::info('ModuleManager: module booted', [
|
|
'slug' => $slug,
|
|
'version' => $manifest['version'] ?? 'unknown',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('ModuleManager: module boot failed', [
|
|
'slug' => $slug,
|
|
'exception' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
]);
|
|
$this->autoDisable($slug, $e->getMessage());
|
|
$this->reportDiagnostic('module_boot_failed', $slug, $e);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function enable(string $slug): bool
|
|
{
|
|
if (! isset($this->modules[$slug])) {
|
|
return false;
|
|
}
|
|
|
|
$this->disabledModules = array_diff($this->disabledModules, [$slug]);
|
|
|
|
return true;
|
|
}
|
|
|
|
public function disable(string $slug): bool
|
|
{
|
|
if (! isset($this->modules[$slug])) {
|
|
return false;
|
|
}
|
|
|
|
if (! in_array($slug, $this->disabledModules, true)) {
|
|
$this->disabledModules[] = $slug;
|
|
}
|
|
|
|
Log::info('ModuleManager: module disabled', ['slug' => $slug]);
|
|
|
|
return true;
|
|
}
|
|
|
|
public function isEnabled(string $slug): bool
|
|
{
|
|
return isset($this->modules[$slug]) && ! in_array($slug, $this->disabledModules, true);
|
|
}
|
|
|
|
public function getManifest(string $slug): ?array
|
|
{
|
|
return $this->modules[$slug] ?? null;
|
|
}
|
|
|
|
public function getAll(): array
|
|
{
|
|
return $this->modules;
|
|
}
|
|
|
|
public function getDisabled(): array
|
|
{
|
|
return $this->disabledModules;
|
|
}
|
|
|
|
public function getPermissions(string $slug): array
|
|
{
|
|
$manifest = $this->getManifest($slug);
|
|
|
|
return $manifest['requires_permissions'] ?? [];
|
|
}
|
|
|
|
protected function autoDisable(string $slug, string $reason): void
|
|
{
|
|
if (! in_array($slug, $this->disabledModules, true)) {
|
|
$this->disabledModules[] = $slug;
|
|
}
|
|
|
|
Log::warning('ModuleManager: module auto-disabled', [
|
|
'slug' => $slug,
|
|
'reason' => $reason,
|
|
]);
|
|
}
|
|
|
|
private function reportDiagnostic(string $type, string $slug, \Throwable $e): void
|
|
{
|
|
try {
|
|
$manager = app(DiagnosticManager::class);
|
|
$manager->report($type, [
|
|
'module_slug' => $slug,
|
|
'class' => get_class($e),
|
|
'message' => $e->getMessage(),
|
|
'file' => $e->getFile(),
|
|
'line' => $e->getLine(),
|
|
]);
|
|
} catch (\Throwable) {
|
|
// Never let diagnostic reporting interfere with module management
|
|
}
|
|
}
|
|
}
|