pluginsPath = $pluginsPath; $this->enabledPlugins = $enabledPlugins; $this->loadPlugins(); } public function setAPI(CMSAPI $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(); if ($this->api && method_exists($this->plugins[$pluginName], 'setAPI')) { $this->plugins[$pluginName]->setAPI($this->api); } // 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(); if (isset($config['viewable']) && $config['viewable'] === false) { return false; } // System plugins don't show in sidebar if (isset($config['type']) && $config['type'] === 'system') { return false; } } return true; } public function getSidebarContent(?array $allowedPlugins = null): string { $sidebarContent = ''; // If allowedPlugins is specified, iterate in that order (user-defined order) if ($allowedPlugins !== null) { foreach ($allowedPlugins as $pluginName) { $plugin = $this->plugins[$pluginName] ?? null; if (!$plugin || !$this->isPluginViewable($plugin) || !method_exists($plugin, 'getSidebarContent')) { 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 . '
'; } } else { // No filter — show all viewable plugins in load order foreach ($this->plugins as $pluginName => $plugin) { if (!$this->isPluginViewable($plugin) || !method_exists($plugin, 'getSidebarContent')) { 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; } /** * Get admin menu items from system plugins. * Each system plugin can register admin menu items via getAdminMenu(). * Returns an array of [label, route, icon] tuples. * * @return array Admin menu items */ public function getAdminMenuItems(): array { $items = []; foreach ($this->plugins as $pluginName => $plugin) { // Only system plugins provide admin menu items if (method_exists($plugin, 'getConfig')) { $config = $plugin->getConfig(); if (($config['type'] ?? 'content') !== 'system') { continue; } } if (method_exists($plugin, 'getAdminMenu')) { $menuItems = $plugin->getAdminMenu(); if (is_array($menuItems)) { foreach ($menuItems as $item) { $items[] = $item; } } } } return $items; } /** * Handle an admin route for a system plugin. * Called when a route matches a plugin's registered admin route. * Returns the rendered content string, or null if no plugin handled it. * * @param string $route The admin route * @return string|null Rendered content or null if not handled */ public function handleAdminRoute(string $route): ?string { foreach ($this->plugins as $pluginName => $plugin) { if (method_exists($plugin, 'getConfig')) { $config = $plugin->getConfig(); if (($config['type'] ?? 'content') !== 'system') { continue; } } if (method_exists($plugin, 'getAdminRoutes')) { $routes = $plugin->getAdminRoutes(); if (is_array($routes) && in_array($route, $routes, true)) { ob_start(); $plugin->handleAdminRoute($route); return ob_get_clean(); } } } return null; } /** * Get a plugin's type (content or system). * * @param string $pluginName Plugin name * @return string 'content', 'system', or 'content' if unknown */ public function getPluginType(string $pluginName): string { $pluginDir = $this->pluginsPath . '/' . $pluginName; $jsonFile = $pluginDir . '/plugin.json'; if (file_exists($jsonFile)) { $data = json_decode(file_get_contents($jsonFile), true); return $data['type'] ?? 'content'; } return 'content'; } }