pluginsPath = $pluginsPath; $this->enabledPlugins = $enabledPlugins; $this->loadPlugins(); } public function setAPI($api): void { $this->api = $api; foreach ($this->plugins as $plugin) { if (method_exists($plugin, 'setAPI')) { $plugin->setAPI($api); } } } private function loadPlugins(): void { if (!is_dir($this->pluginsPath)) { return; } $pluginDirs = glob($this->pluginsPath . '/*', GLOB_ONLYDIR); foreach ($pluginDirs as $pluginDir) { $pluginName = basename($pluginDir); if (!in_array($pluginName, $this->enabledPlugins, true)) { continue; } $pluginFile = $pluginDir . '/' . $pluginName . '.php'; if (file_exists($pluginFile)) { require_once $pluginFile; $className = $pluginName; if (class_exists($className)) { $this->plugins[$pluginName] = new $className(); // Auto-register hooks from plugin methods $hookMethods = ['onPageLoad', 'onBeforeRender', 'onAfterRender', 'onSearch', 'onMenuBuild']; foreach ($hookMethods as $hook) { if (method_exists($this->plugins[$pluginName], $hook)) { $this->addAction($hook, [$this->plugins[$pluginName], $hook]); } } // Register filter methods $filterMethods = ['onContentFilter', 'onTitleFilter', 'onMenuFilter']; foreach ($filterMethods as $filter) { if (method_exists($this->plugins[$pluginName], $filter)) { $this->addFilter($filter, [$this->plugins[$pluginName], $filter]); } } } } } } public function addAction(string $hook, callable $callback, int $priority = 10): void { $this->actions[$hook][$priority][] = $callback; } public function addFilter(string $hook, callable $callback, int $priority = 10): void { $this->filters[$hook][$priority][] = $callback; } public function doAction(string $hook, ...$args): void { if (!isset($this->actions[$hook])) return; ksort($this->actions[$hook]); foreach ($this->actions[$hook] as $callbacks) { foreach ($callbacks as $callback) { $callback(...$args); } } } public function applyFilters(string $hook, $value, ...$args) { if (!isset($this->filters[$hook])) return $value; ksort($this->filters[$hook]); foreach ($this->filters[$hook] as $callbacks) { foreach ($callbacks as $callback) { $value = $callback($value, ...$args); } } return $value; } public function getPlugin(string $name): ?object { return $this->plugins[$name] ?? null; } public function getAllPlugins(): array { return $this->plugins; } public function getEnabledPlugins(): array { return $this->enabledPlugins; } public function isEnabled(string $pluginName): bool { return in_array($pluginName, $this->enabledPlugins, true); } public function isPluginViewable(object $plugin): bool { if (method_exists($plugin, 'getConfig')) { $config = $plugin->getConfig(); return !isset($config['viewable']) || $config['viewable'] !== false; } return true; } public function getSidebarContent(?array $allowedPlugins = null): string { $sidebarContent = ''; foreach ($this->plugins as $pluginName => $plugin) { if (!$this->isPluginViewable($plugin) || !method_exists($plugin, 'getSidebarContent')) { continue; } if ($allowedPlugins !== null && !in_array($pluginName, $allowedPlugins, true)) { continue; } $content = $plugin->getSidebarContent(); if (trim($content) === '') { continue; } $title = 'Plugin'; if (method_exists($plugin, 'getConfig')) { $config = $plugin->getConfig(); $title = $config['title'] ?? 'Plugin'; } $sidebarContent .= '
' . htmlspecialchars($title) . '
' . $content . '
'; } return $sidebarContent; } /** * Get CSS URLs from all enabled plugins that provide getCssUrl(). * * @return array List of CSS URLs */ public function getPluginCssUrls(): array { $urls = []; foreach ($this->plugins as $pluginName => $plugin) { if (method_exists($plugin, 'getCssUrl')) { $url = $plugin->getCssUrl(); if (!empty($url)) { $urls[] = $url; } } } return $urls; } /** * Collect admin menu items from all enabled plugins that implement getAdminMenu(). * Each plugin returns [['route' => 'plugin-name/action', 'label' => 'Label', 'icon' => 'bi-icon']]. * * @return array Admin menu items from all plugins */ public function getAdminMenuItems(): array { $items = []; foreach ($this->plugins as $pluginName => $plugin) { if (method_exists($plugin, 'getAdminMenu')) { $pluginItems = $plugin->getAdminMenu(); if (is_array($pluginItems)) { foreach ($pluginItems as $item) { $items[] = $item; } } } } return $items; } /** * Dispatch an admin route to a plugin that handles it. * Plugins implement handleAdminRoute(string $action): ?string (returns rendered HTML or null). * * @param string $pluginName Plugin name * @param string $action Action/sub-route within the plugin * @return string|null Rendered HTML, or null if plugin doesn't handle it */ public function dispatchAdminRoute(string $pluginName, string $action): ?string { $plugin = $this->getPlugin($pluginName); if ($plugin === null) { return null; } if (method_exists($plugin, 'handleAdminRoute')) { return $plugin->handleAdminRoute($action); } return null; } /** * Find which plugin handles a given admin route. * * @param string $route The admin route (e.g. 'statistics' or 'statistics/details') * @return array|null ['plugin' => name, 'action' => action] or null */ public function resolveAdminRoute(string $route): ?array { foreach ($this->getAdminMenuItems() as $item) { $itemRoute = $item['route'] ?? ''; // Check if the requested route starts with this plugin's route if ($route === $itemRoute || str_starts_with($route, $itemRoute . '/')) { $action = substr($route, strlen($itemRoute) + 1); return [ 'plugin' => $item['plugin'] ?? '', 'action' => $action, ]; } } return null; } }