Files
CodePress/cms/core/plugin/PluginManager.php
T
E.Noorlander c2bcd7be22 System plugin support: admin menu items, admin routes, API integration
- PluginManager: getAdminMenuItems(), handleAdminRoute(), getPluginType()
- admin.php: load PluginManager, pass plugin_admin_menu to Twig
- admin.php: route to plugin admin pages via handleAdminRoute()
- admin.twig: show 'Plugins' sidebar section for system plugins
- plugins-new.twig: choose content or system plugin type
- handlePluginsNew: generate proper template based on type (content/system)
- plugin-admin.twig: renders plugin output in admin layout
- GeoIPInfo: example system plugin (admin page with GeoIP info)
- System plugins: getAdminMenu(), getAdminRoutes(), handleAdminRoute()
- Content plugins: getSidebarContent() (unchanged)
2026-08-11 18:06:17 +02:00

309 lines
9.9 KiB
PHP

<?php
class PluginManager
{
private array $plugins = [];
private string $pluginsPath;
private ?CMSAPI $api = null;
private array $enabledPlugins = [];
private array $actions = [];
private array $filters = [];
public function __construct(string $pluginsPath, array $enabledPlugins = [])
{
$this->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 .= '
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">' . htmlspecialchars($title) . '</h5>
</div>
<div class="card-body">
' . $content . '
</div>
</div>';
}
} 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 .= '
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">' . htmlspecialchars($title) . '</h5>
</div>
<div class="card-body">
' . $content . '
</div>
</div>';
}
}
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';
}
}