feat: Implement a core plugin system, integrate flag icon assets, and establish a GitHub release workflow.

This commit is contained in:
MivoDev
2026-01-18 11:00:36 +07:00
parent b245f31236
commit c95c8b08ea
579 changed files with 25054 additions and 313 deletions

120
app/Core/Hooks.php Normal file
View File

@@ -0,0 +1,120 @@
<?php
namespace App\Core;
class Hooks
{
/**
* @var array Stores all registered actions
*/
private static $actions = [];
/**
* @var array Stores all registered filters
*/
private static $filters = [];
/**
* Register a new action
*
* @param string $tag The name of the action hook
* @param callable $callback The function to call
* @param int $priority Lower numbers correspond to earlier execution
* @param int $accepted_args The number of arguments the function accepts
*/
public static function addAction($tag, $callback, $priority = 10, $accepted_args = 1)
{
self::$actions[$tag][$priority][] = [
'function' => $callback,
'accepted_args' => $accepted_args
];
}
/**
* Execute an action
*
* @param string $tag The name of the action hook
* @param mixed ...$args Optional arguments to pass to the callback
*/
public static function doAction($tag, ...$args)
{
if (empty(self::$actions[$tag])) {
return;
}
// Sort by priority
ksort(self::$actions[$tag]);
foreach (self::$actions[$tag] as $priority => $callbacks) {
foreach ($callbacks as $callbackData) {
call_user_func_array($callbackData['function'], array_slice($args, 0, $callbackData['accepted_args']));
}
}
}
/**
* Register a new filter
*
* @param string $tag The name of the filter hook
* @param callable $callback The function to call
* @param int $priority Lower numbers correspond to earlier execution
* @param int $accepted_args The number of arguments the function accepts
*/
public static function addFilter($tag, $callback, $priority = 10, $accepted_args = 1)
{
self::$filters[$tag][$priority][] = [
'function' => $callback,
'accepted_args' => $accepted_args
];
}
/**
* Apply filters to a value
*
* @param string $tag The name of the filter hook
* @param mixed $value The value to be filtered
* @param mixed ...$args Optional extra arguments
* @return mixed The filtered value
*/
public static function applyFilters($tag, $value, ...$args)
{
if (empty(self::$filters[$tag])) {
return $value;
}
// Sort by priority
ksort(self::$filters[$tag]);
foreach (self::$filters[$tag] as $priority => $callbacks) {
foreach ($callbacks as $callbackData) {
// Prepend value to args
$params = array_merge([$value], array_slice($args, 0, $callbackData['accepted_args'] - 1));
$value = call_user_func_array($callbackData['function'], $params);
}
}
return $value;
}
/**
* Check if any action has been registered for a hook.
*
* @param string $tag The name of the action hook.
* @return bool True if action exists, false otherwise.
*/
public static function hasAction($tag)
{
return isset(self::$actions[$tag]);
}
/**
* Check if any filter has been registered for a hook.
*
* @param string $tag The name of the filter hook.
* @return bool True if filter exists, false otherwise.
*/
public static function hasFilter($tag)
{
return isset(self::$filters[$tag]);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Core;
class PluginManager
{
/**
* @var string Path to plugins directory
*/
private $pluginsDir;
/**
* @var array List of active plugins
*/
private $activePlugins = [];
public function __construct()
{
$this->pluginsDir = dirname(__DIR__, 2) . '/plugins'; // Root/plugins
}
/**
* Load all active plugins
*/
public function loadPlugins()
{
// Ensure plugins directory exists
if (!is_dir($this->pluginsDir)) {
return;
}
// 1. Get List of Active Plugins (For now, we load ALL folders as active)
// TODO: Implement database/config check for active status
$plugins = scandir($this->pluginsDir);
foreach ($plugins as $pluginName) {
if ($pluginName === '.' || $pluginName === '..') {
continue;
}
$pluginPath = $this->pluginsDir . '/' . $pluginName;
// Check if it is a directory and has specific plugin file
if (is_dir($pluginPath) && file_exists($pluginPath . '/plugin.php')) {
$this->loadPlugin($pluginName, $pluginPath);
}
}
// Fire 'plugins_loaded' action after all plugins are loaded
Hooks::doAction('plugins_loaded');
}
/**
* Load a single plugin
*
* @param string $name Plugin folder name
* @param string $path Full path to plugin directory
*/
private function loadPlugin($name, $path)
{
try {
require_once $path . '/plugin.php';
$this->activePlugins[] = $name;
} catch (\Exception $e) {
error_log("Failed to load plugin [$name]: " . $e->getMessage());
}
}
/**
* Get list of loaded plugins
*
* @return array
*/
public function getActivePlugins()
{
return $this->activePlugins;
}
}

View File

@@ -92,6 +92,9 @@ class Router {
}
public function dispatch($uri, $method) {
// Fire hook to allow plugins to register routes
\App\Core\Hooks::doAction('router_init', $this);
$path = parse_url($uri, PHP_URL_PATH);
// Handle subdirectory