|null */ private ?array $apis = null; /** @var array */ private array $errors = []; public function __construct(private PluginRegistry $plugins, private Connection $db, private MediaLibrary $media) { } /** Nach dem Ein- oder Ausschalten: beim nächsten Zugriff neu laden. */ public function reset(): void { $this->apis = null; $this->errors = []; } /** @return array Plugin => Grund, aus dem es gerade nicht läuft */ public function errors(): array { $this->apis(); return $this->errors; } /** * Schaltet ein und richtet ein. Scheitert das Einrichten, bleibt das Plugin aus. * * @throws \InvalidArgumentException */ public function activate(string $name): void { $this->plugins->activate($name); $this->reset(); if (isset($this->errors()[$name])) { $reason = $this->errors()[$name]; $this->plugins->deactivate($name); $this->reset(); throw new \InvalidArgumentException('Das Plugin lässt sich nicht laden: '.$reason); } try { $this->install($this->apis()[$name] ?? null); } catch (\Throwable $e) { $this->plugins->deactivate($name); $this->reset(); throw new \InvalidArgumentException('Das Plugin ließ sich nicht einrichten: '.$e->getMessage()); } } /** @throws \InvalidArgumentException */ public function deactivate(string $name): void { foreach ($this->apis()[$name]->registered['deactivate'] ?? [] as $callback) { try { $callback(); } catch (\Throwable $e) { // Ausschalten muss immer gehen, gerade bei einem Plugin, das Fehler macht. $this->errors[$name] = $e->getMessage(); } } $this->plugins->deactivate($name); $this->reset(); } /** * Führt ein Ereignis aus. Der Fehler eines Plugins hält die übrigen nicht auf. */ public function doAction(string $hook, mixed ...$arguments): void { foreach ($this->hooks('actions', $hook) as [$name, $callback]) { try { $callback(...$arguments); } catch (\Throwable $e) { $this->errors[$name] = $e->getMessage(); } } } /** * Reicht einen Wert durch die Filter. Wirft einer, gilt der Wert von davor. */ public function applyFilters(string $hook, mixed $value, mixed ...$arguments): mixed { foreach ($this->hooks('filters', $hook) as [$name, $callback]) { try { $value = $callback($value, ...$arguments); } catch (\Throwable $e) { $this->errors[$name] = $e->getMessage(); } } return $value; } /** @return list */ public function tasks(): array { $tasks = []; foreach ($this->apis() as $name => $api) { foreach ($api->registered['tasks'] as $task) { $tasks[] = new PluginTask(self::taskName($name, $task['key']), $task['label'], $task['explanation'], $task['interval'], $task['callback']); } } return $tasks; } public static function taskName(string $plugin, string $key): string { return 'plugin-'.$plugin.'-'.$key; } /** * Die Seite eines Plugins zu dieser Adresse, in der Form einer gespeicherten Seite - oder null. */ public function page(string $path): ?array { foreach ($this->apis() as $name => $api) { foreach ($api->registered['pages'] as $page) { if (1 !== preg_match($page['regex'], $path, $match)) { continue; } try { $result = $page['callback'](array_filter($match, 'is_string', ARRAY_FILTER_USE_KEY)); } catch (\Throwable $e) { $this->errors[$name] = $e->getMessage(); return null; } return is_array($result) ? $this->pageRecord($name, $path, $result) : null; } } return null; } public function data(string $plugin, string $key, array $arguments = []): mixed { $callback = $this->apis()[$plugin]->registered['data'][$key] ?? null; if (null === $callback) { return null; } try { return $callback($arguments); } catch (\Throwable $e) { $this->errors[$plugin] = $e->getMessage(); return null; } } /** @return array{pages: array, actions: array} */ public function admin(string $plugin): array { $api = $this->apis()[$plugin] ?? null; return ['pages' => $api?->registered['adminPages'] ?? [], 'actions' => $api?->registered['adminActions'] ?? []]; } /** @return array nur Plugins, deren plugin.php fehlerfrei geladen wurde */ private function apis(): array { if (null !== $this->apis) { return $this->apis; } $this->apis = []; foreach ($this->plugins->all() as $name => $plugin) { if (!$plugin['active'] || null !== $plugin['error'] || null === ($plugin['main'] ?? null)) { continue; } $directory = $this->plugins->directory().'/'.$name; $api = new PluginApi($name, $directory, $this->plugins, $this->db, $this->media); try { $boot = self::load($directory.'/'.$plugin['main']); if (!is_callable($boot)) { throw new \RuntimeException($plugin['main'].' gibt keine Funktion zurück.'); } $boot($api); // Eine neue Fassung im Ordner: einrichten, bevor etwas sie benutzt. if ($plugin['activatedVersion'] !== $plugin['version']) { $this->install($api); $this->plugins->markInstalled($name); } $this->apis[$name] = $api; } catch (\Throwable $e) { $this->errors[$name] = $e->getMessage(); } } return $this->apis; } /** In einer eigenen statischen Funktion, damit die Datei nichts von hier sieht. */ private static function load(string $file): mixed { return require $file; } private function install(?PluginApi $api): void { foreach ($api?->registered['install'] ?? [] as $callback) { $callback(); } } /** @return list nach Priorität, bei Gleichstand in Reihenfolge der Plugins */ private function hooks(string $kind, string $hook): array { $found = []; foreach ($this->apis() as $name => $api) { foreach ($api->registered[$kind][$hook] ?? [] as [$priority, $callback]) { $found[] = [$priority, $name, $callback]; } } usort($found, static fn (array $a, array $b): int => $a[0] <=> $b[0]); return array_map(static fn (array $entry): array => [$entry[1], $entry[2]], $found); } private function pageRecord(string $plugin, string $path, array $result): array { return [ 'id' => 'plugin-'.$plugin, 'path' => ltrim($path, '/'), 'type' => 'page', 'locale' => 'de', 'seo_title' => (string) ($result['seoTitle'] ?? ''), 'robots' => 'noindex' === ($result['robots'] ?? 'index') ? 'noindex' : 'index', 'status' => (int) ($result['status'] ?? 200), 'og_media_id' => $result['image'] ?? null, 'header_area' => null, 'footer_area' => null, 'document' => [ 'schemaVersion' => DocumentValidator::SCHEMA_VERSION, 'title' => (string) ($result['title'] ?? ''), 'description' => (string) ($result['description'] ?? ''), 'blocks' => $result['blocks'] ?? [], ], ]; } }