plugin infrastruktur
This commit is contained in:
1 parent
1545b90f39
commit
3f4ba3c0f4
35 files changed
+1303
-195
No files matched your search
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PluginManager — Plugin discovery, loading, and asset rendering.
|
||||
*
|
||||
* Plugins live in /public/plugins/<name>/ and must have a plugin.json manifest:
|
||||
* {
|
||||
* "name": "canvas",
|
||||
* "version": "1.0.0",
|
||||
* "description": "ASCII art canvas editor",
|
||||
* "frontend": {
|
||||
* "js": ["canvas.js"],
|
||||
* "css": ["canvas.css"],
|
||||
* "html": ["canvas.html"]
|
||||
* },
|
||||
* "backend": "backend.php" // optional
|
||||
* }
|
||||
*
|
||||
* Plugins are entirely optional — the core works fine without any.
|
||||
*/
|
||||
class PluginManager {
|
||||
private array $plugins = [];
|
||||
private array $slots = [];
|
||||
private array $disabled = []; // names of plugins the user has disabled
|
||||
private ?string $stateFile = null; // path to plugins.json
|
||||
private static ?PluginManager $instance = null;
|
||||
|
||||
public static function instance(): self {
|
||||
if (self::$instance === null) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/** Scan a directory for plugins. Each subdirectory with a plugin.json is registered. */
|
||||
public function scan(string $dir): void {
|
||||
if (!is_dir($dir)) return;
|
||||
$entries = @scandir($dir);
|
||||
if (!$entries) return;
|
||||
sort($entries);
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry === '.' || $entry === '..') continue;
|
||||
$pluginDir = $dir . '/' . $entry;
|
||||
if (!is_dir($pluginDir)) continue;
|
||||
$manifestPath = $pluginDir . '/plugin.json';
|
||||
if (!file_exists($manifestPath)) continue;
|
||||
$raw = @file_get_contents($manifestPath);
|
||||
$manifest = $raw ? json_decode($raw, true) : null;
|
||||
if (!is_array($manifest)) continue;
|
||||
$name = $manifest['name'] ?? $entry;
|
||||
$this->plugins[$name] = [
|
||||
'dir' => $pluginDir,
|
||||
'slug' => $entry,
|
||||
'manifest' => $manifest,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/** After scan() and loadDisabled(), require_once the backend.php of every ENABLED plugin. */
|
||||
public function loadBackends(): void {
|
||||
foreach ($this->plugins as $name => $p) {
|
||||
if (!$this->isEnabled($name)) continue;
|
||||
$backend = $p['manifest']['backend'] ?? null;
|
||||
if (!$backend) continue;
|
||||
$backendPath = $p['dir'] . '/' . $backend;
|
||||
if (file_exists($backendPath)) require_once $backendPath;
|
||||
}
|
||||
}
|
||||
|
||||
public function plugins(): array {
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
/** Load the disabled-plugins state file. Creates it lazily on first write. */
|
||||
public function loadDisabled(string $stateFile): void {
|
||||
$this->stateFile = $stateFile;
|
||||
if (!file_exists($stateFile)) { $this->disabled = []; return; }
|
||||
$raw = @file_get_contents($stateFile);
|
||||
if (!$raw) { $this->disabled = []; return; }
|
||||
$data = json_decode($raw, true);
|
||||
$list = is_array($data) ? ($data['disabled'] ?? []) : [];
|
||||
$this->disabled = array_values(array_filter($list, 'is_string'));
|
||||
}
|
||||
|
||||
/** Persist the disabled list to the state file. Returns true on success. */
|
||||
public function setDisabled(array $names): bool {
|
||||
if ($this->stateFile === null) return false;
|
||||
$clean = array_values(array_unique(array_filter($names, 'is_string')));
|
||||
$this->disabled = $clean;
|
||||
$dir = dirname($this->stateFile);
|
||||
if (!is_dir($dir)) @mkdir($dir, 0755, true);
|
||||
$payload = json_encode(['disabled' => $clean], JSON_PRETTY_PRINT);
|
||||
return @file_put_contents($this->stateFile, $payload) !== false;
|
||||
}
|
||||
|
||||
public function isEnabled(string $name): bool {
|
||||
return !in_array($name, $this->disabled, true);
|
||||
}
|
||||
|
||||
public function disabledList(): array {
|
||||
return $this->disabled;
|
||||
}
|
||||
|
||||
/** Public-friendly plugin list for the API: [{name, version, description, enabled}]. */
|
||||
public function pluginsForApi(): array {
|
||||
$out = [];
|
||||
foreach ($this->plugins as $name => $p) {
|
||||
$m = $p['manifest'];
|
||||
$out[] = [
|
||||
'name' => $name,
|
||||
'version' => $m['version'] ?? '',
|
||||
'description' => $m['description'] ?? '',
|
||||
'enabled' => $this->isEnabled($name),
|
||||
'hasBackend' => !empty($m['backend']),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Append HTML content to a named slot. Backend plugins use this. */
|
||||
public function addToSlot(string $slot, string $html): void {
|
||||
$this->slots[$slot][] = $html;
|
||||
}
|
||||
|
||||
/** Render a slot's accumulated HTML. */
|
||||
public function slot(string $slot): string {
|
||||
return implode("\n", $this->slots[$slot] ?? []);
|
||||
}
|
||||
|
||||
/** Render <link> tags for enabled plugin CSS files. */
|
||||
public function cssLinks(string $baseUrl = 'plugins'): string {
|
||||
$out = [];
|
||||
foreach ($this->plugins as $name => $p) {
|
||||
if (!$this->isEnabled($name)) continue;
|
||||
$cssList = $p['manifest']['frontend']['css'] ?? [];
|
||||
foreach ($cssList as $css) {
|
||||
$out[] = '<link rel="stylesheet" href="' . $baseUrl . '/' . $p['slug'] . '/' . $css . '?v=' . time() . '">';
|
||||
}
|
||||
}
|
||||
return implode("\n ", $out);
|
||||
}
|
||||
|
||||
/** Render inlined HTML fragments from enabled plugins (for overlays etc.). */
|
||||
public function htmlFragments(): string {
|
||||
$out = [];
|
||||
foreach ($this->plugins as $name => $p) {
|
||||
if (!$this->isEnabled($name)) continue;
|
||||
$htmlList = $p['manifest']['frontend']['html'] ?? [];
|
||||
foreach ($htmlList as $htmlFile) {
|
||||
$path = $p['dir'] . '/' . $htmlFile;
|
||||
if (file_exists($path)) {
|
||||
$out[] = "<!-- plugin:{$p['slug']}:{$htmlFile} -->\n" . file_get_contents($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return implode("\n", $out);
|
||||
}
|
||||
|
||||
/** Render <script type="module"> tags for enabled plugin JS entry points. */
|
||||
public function scriptTags(string $baseUrl = 'plugins'): string {
|
||||
$out = [];
|
||||
foreach ($this->plugins as $name => $p) {
|
||||
if (!$this->isEnabled($name)) continue;
|
||||
$jsList = $p['manifest']['frontend']['js'] ?? [];
|
||||
foreach ($jsList as $js) {
|
||||
$out[] = '<script type="module" src="' . $baseUrl . '/' . $p['slug'] . '/' . $js . '?v=' . time() . '"></script>';
|
||||
}
|
||||
}
|
||||
return implode("\n ", $out);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user