pluginsPath = $pluginsPath; $this->enabledPlugins = $enabledPlugins; $this->siteDefaultLanguage = $siteDefaultLanguage !== '' ? $siteDefaultLanguage : 'nl'; $this->loadPlugins(); } /** * Set the CMS site default language. Used as a fallback when a plugin * does not declare its own `default_language` in plugin.json. */ public function setSiteDefaultLanguage(string $lang): void { $this->siteDefaultLanguage = $lang !== '' ? $lang : 'nl'; } 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); } /** * Get the runtime config for a plugin: defaults from plugin.json `settings` * merged with overrides from the plugin's config.json. * * @param string $pluginName Plugin name (directory name) * @return array Resolved config: [key => value, ...] */ public function getPluginConfig(string $pluginName): array { $pluginDir = $this->pluginsPath . '/' . $pluginName; $pluginJsonFile = $pluginDir . '/plugin.json'; $configJsonFile = $pluginDir . '/config.json'; $defaults = []; if (file_exists($pluginJsonFile)) { $pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? []; foreach ($pluginJson['settings'] ?? [] as $setting) { if (isset($setting['key'])) { $defaults[$setting['key']] = $setting['default'] ?? null; } } } $overrides = []; if (file_exists($configJsonFile)) { $overrides = json_decode(file_get_contents($configJsonFile), true) ?? []; } return array_merge($defaults, $overrides); } /** * Get the default (fallback) language declared by a plugin. * * Resolved from (in order): * 1. plugin.json `default_language` * 2. CMS site config `language.default` * 3. 'nl' * * @param string $pluginName Plugin directory name * @return string Language code (e.g. 'nl', 'en') */ public function getPluginDefaultLanguage(string $pluginName): string { $pluginJsonFile = $this->pluginsPath . '/' . $pluginName . '/plugin.json'; if (file_exists($pluginJsonFile)) { $pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? []; if (!empty($pluginJson['default_language'])) { return (string)$pluginJson['default_language']; } } return $this->siteDefaultLanguage ?? 'nl'; } /** * Load the translations for a plugin for the requested language. * * Fallback chain: requested language -> plugin default language -> empty array. * * @param string $pluginName Plugin directory name * @param string $lang Requested language code * @param string $context Translation context: 'admin' or 'site' (front-end). * Defaults to 'admin' for system plugins, 'site' for content plugins. * @return array Translations [key => value] */ public function getPluginTranslations(string $pluginName, string $lang, string $context = 'admin'): array { $langDir = $this->pluginsPath . '/' . $pluginName . '/language'; if (!is_dir($langDir)) { return []; } // Try requested language first $file = $langDir . '/' . $lang . '/' . $context . '.php'; if (file_exists($file)) { $t = include $file; if (is_array($t)) { return $t; } } // Fallback to the plugin's default language $defaultLang = $this->getPluginDefaultLanguage($pluginName); if ($defaultLang !== $lang) { $fallbackFile = $langDir . '/' . $defaultLang . '/' . $context . '.php'; if (file_exists($fallbackFile)) { $t = include $fallbackFile; if (is_array($t)) { return $t; } } } return []; } /** * Collect translations for every loaded plugin for the requested language. * * Returns an associative array keyed by plugin name: * ['Statistics' => [...], 'Logs' => [...], ...] * * @param string $lang Requested language code * @param string $context 'admin' or 'site' * @return array Plugin translations keyed by plugin name */ public function getAllPluginTranslations(string $lang, string $context = 'admin'): array { $all = []; foreach ($this->plugins as $pluginName => $plugin) { // System plugins follow the admin language; content plugins follow the content language. // The caller decides the context; here we just load for every plugin. $all[$pluginName] = $this->getPluginTranslations($pluginName, $lang, $context); } return $all; } 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 .= '