phppluginsframeworks

PHP Plugin Listerners


So I'm trying to code a plugin system in PHP using listeners just like minecraft does with priorities and actions, but I don't really do not know how. Since if I try to use event listeners I have to give it to a value called $event, but I can't share that value with other classes since they are going to tell me that I can use the variables outside the index.php file.

My project structure is like a lavarel one a public/index.php and rest is just app/

Here is the plugin event:

<?php

namespace MythicalSystemsFramework\Plugins;

/**
 * This class is used to handle plugin events.
 *
 * @since 1.0.0
 *
 * @version 1.0.0
 *
 * @category Plugins
 *
 * This class is inspired by Evenement.
 *
 * @see https://github.com/igorw/even  ement
 *
 * @license MIT
 *
 *  */
class PluginEvent
{
    protected array $listeners = [];

    protected array $onceListeners = [];

    /**
     * Adds a listener for the specified event.
     *
     * @param string $event the name of the event
     * @param callable $listener the listener function to be added
     *
     * @return static returns the current instance of the PluginEvent class
     */
    public function on(string $event, callable $listener): static
    {
        if (!isset($this->listeners[$event])) {
            $this->listeners[$event] = [];
        }

        $this->listeners[$event][] = $listener;

        return $this;
    }

    /**
     * Adds a listener for the specified event that will be triggered only once.
     *
     * @param string $event the name of the event
     * @param callable $listener the listener function to be added
     *
     * @return static returns the current instance of the PluginEvent class
     */
    public function once(string $event, callable $listener): static
    {
        if (!isset($this->onceListeners[$event])) {
            $this->onceListeners[$event] = [];
        }

        $this->onceListeners[$event][] = $listener;

        return $this;
    }

    /**
     * Removes a listener for the specified event.
     *
     * @param string $event the name of the event
     * @param callable $listener the listener function to be removed
     */
    public function removeListener(string $event, callable $listener): void
    {
        if (isset($this->listeners[$event])) {
            $index = array_search($listener, $this->listeners[$event], true);

            if ($index !== false) {
                unset($this->listeners[$event][$index]);

                if (count($this->listeners[$event]) === 0) {
                    unset($this->listeners[$event]);
                }
            }
        }

        if (isset($this->onceListeners[$event])) {
            $index = array_search($listener, $this->onceListeners[$event], true);

            if ($index !== false) {
                unset($this->onceListeners[$event][$index]);

                if (count($this->onceListeners[$event]) === 0) {
                    unset($this->onceListeners[$event]);
                }
            }
        }
    }

    /**
     * Removes all listeners for the specified event or all events if no event is specified.
     *
     * @param string|null $event the name of the event (optional)
     */
    public function removeAllListeners(?string $event = null): void
    {
        if ($event !== null) {
            unset($this->listeners[$event], $this->onceListeners[$event]);
        } else {
            $this->listeners = [];
            $this->onceListeners = [];
        }
    }

    /**
     * Removes all listeners for the specified event or all events if no event is specified.
     *
     * @param string|null $event the name of the event (optional)
     *
     * @return void
     */
    public function listeners(?string $event = null): array
    {
        if ($event === null) {
            $events = [];
            $eventNames = array_unique(
                array_merge(
                    array_keys($this->listeners),
                    array_keys($this->onceListeners)
                )
            );

            foreach ($eventNames as $eventName) {
                $events[$eventName] = array_merge(
                    $this->listeners[$eventName] ?? [],
                    $this->onceListeners[$eventName] ?? []
                );
            }

            return $events;
        }

        return array_merge(
            $this->listeners[$event] ?? [],
            $this->onceListeners[$event] ?? []
        );
    }

    /**
     * Emits the specified event and triggers all associated listeners.
     *
     * @param string $event the name of the event
     * @param array $arguments the arguments to be passed to the listeners (optional)
     */
    public function emit(string $event, array $arguments = []): void
    {
        $listeners = [];
        if (isset($this->listeners[$event])) {
            $listeners = array_values($this->listeners[$event]);
        }

        $onceListeners = [];
        if (isset($this->onceListeners[$event])) {
            $onceListeners = array_values($this->onceListeners[$event]);
        }

        if ($listeners !== []) {
            foreach ($listeners as $listener) {
                $listener(...$arguments);
            }
        }

        if ($onceListeners !== []) {
            unset($this->onceListeners[$event]);

            foreach ($onceListeners as $listener) {
                $listener(...$arguments);
            }
        }
    }

    /**
     * Get an instance of the PluginEvent class.
     */
    public static function getInstance(): PluginEvent
    {
        return new PluginEvent();
    }
}

And here is what loads the plugins events:

<?php

namespace MythicalSystemsFramework\Plugins;

use MythicalSystemsFramework\Kernel\Config;
use MythicalSystemsFramework\Kernel\Logger;
use MythicalSystemsFramework\Kernel\LoggerTypes;
use MythicalSystemsFramework\Kernel\LoggerLevels;

class PluginsManager
{
    public static string $plugins_path = __DIR__ . '/../../storage/addons';

    public static function init(\Router\Router $router, \Twig\Environment $renderer, PluginEvent $eventHandler): void
    {
        if (!file_exists(self::$plugins_path)) {
            mkdir(self::$plugins_path, 0777, true);
        }

        $plugins = self::getAllPlugins();
        foreach ($plugins as $plugin) {
            $plugin_info = self::readPluginFile($plugin);
            /*
             * Are all the requirements installed?
             */
            if (isset($plugin_info['require'])) {
                $requirements = $plugin_info['require'];
                foreach ($requirements as $requirement) {
                    if ($requirement == 'MythicalSystemsFramework') {
                        continue;
                    }
                    $isInstalled = self::readPluginFile($requirement);
                    if ($isInstalled) {
                        continue;
                    } else {
                        Logger::log(LoggerLevels::CRITICAL, LoggerTypes::PLUGIN, "Plugin $plugin requires $requirement to be installed.");
                    }
                }
            }

            /*
             * Register the plugin in the database if it is not already registered.
             */
            if (!Database::doesInfoExist('name', $plugin_info['name']) == true) {
                $p = $plugin_info;

                $p_homepage = $p['homepage'] ?? null;
                $p_license = $p['license'] ?? null;
                $p_support = $p['support'] ?? null;
                $p_funding = $p['funding'] ?? null;
                $p_require = $p['require'] ?? 'MythicalSystemsFramework';
                Database::registerNewPlugin($p['name'], $p['description'], $p_homepage, $p_require, $p_license, $p['stability'], $p['authors'], $p_support, $p_funding, $p['version'], false);
                continue;
            }

            /**
             * Is plugin enabled?
             */
            $plugin_info_db = Database::getPlugin($plugin_info['name']);
            if ($plugin_info_db['enabled'] == 'true') {
                $plugin_home_dir = self::$plugins_path . '/' . $plugin_info['name'];
                $main_class = $plugin_home_dir . '/' . $plugin_info_db['name'] . '.php';
                if (file_exists($main_class)) {
                    /*
                     * Start the plugin main class.
                     */
                    try {
                        require_once $main_class;
                        $plugin_class = new $plugin_info_db['name']();
                        $plugin_class->Main();
                        try {
                            $plugin_class->Route($router, $renderer);
                        } catch (\Exception $e) {
                            Logger::log(LoggerLevels::CRITICAL, LoggerTypes::PLUGIN, 'Failed to add routes for plugin' . $plugin_info_db['name'] . '' . $e->getMessage());
                        }
                        try {
                            $plugin_class->Event($eventHandler);
                        } catch (\Exception $e) {
                            Logger::log(LoggerLevels::CRITICAL, LoggerTypes::PLUGIN, 'Failed to add events for plugin' . $plugin_info_db['name'] . '' . $e->getMessage());
                        }
                    } catch (\Exception $e) {
                        Logger::log(LoggerLevels::CRITICAL, LoggerTypes::PLUGIN, "Something failed while we tried to enable the plugin '" . $plugin_info_db['name'] . "'. " . $e->getMessage());
                    }
                } else {
                    Logger::log(LoggerLevels::CRITICAL, LoggerTypes::PLUGIN, "The main class for plugin '$plugin' does not exist.");
                }
            }
        }
    }

    /**
     * Get all plugins.
     */
    public static function getAllPlugins(): array
    {
        $plugins = [];
        foreach (scandir(self::$plugins_path) as $plugin) {
            if ($plugin == '.' || $plugin == '..') {
                continue;
            }
            $pluginPath = self::$plugins_path . '/' . $plugin;
            if (is_dir($pluginPath)) {
                $json_file = $pluginPath . '/MythicalFramework.json';
                if (file_exists($json_file)) {
                    $json = json_decode(file_get_contents($json_file), true);
                    if (isset($json['name']) && $json['name'] === $plugin) {
                        $plugins[] = $plugin;
                    }
                }
            }
        }

        return $plugins;
    }

    /**
     * Get plugin info.
     */
    public static function readPluginFile(string $plugin_name): array
    {
        if (!self::doesPluginExist($plugin_name)) {
            return [];
        }
        if (!self::isPluginConfigValid($plugin_name)) {
            return [];
        }
        $json_file = self::$plugins_path . '/' . $plugin_name . '/MythicalFramework.json';

        return json_decode(file_get_contents($json_file), true);
    }

    /**
     * Does a plugin exist?
     *
     * @param string $plugin_name The name of the plugin
     */
    public static function doesPluginExist(string $plugin_name): bool
    {
        $plugin_folder = self::$plugins_path . '/' . $plugin_name . '/MythicalFramework.json';

        return file_exists($plugin_folder);
    }

    /**
     * Is the plugin config valid?
     *
     * @param string $plugin_name The name of the plugin
     */
    public static function isPluginConfigValid(string $plugin_name): bool
    {
        if (!self::doesPluginExist($plugin_name)) {
            return false;
        }
        $json_file = self::$plugins_path . '/' . $plugin_name . '/MythicalFramework.json';
        if (file_exists($json_file)) {
            $json = json_decode(file_get_contents($json_file), true);
            if (isset($json['name']) && isset($json['description']) && isset($json['stability']) && isset($json['authors']) && isset($json['version']) && isset($json['require'])) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}

And here is the public/index.php file:

<?php

try {
    if (file_exists('../vendor/autoload.php')) {
        require '../vendor/autoload.php';
    } else {
        exit('Hello, it looks like you did not run: "<code>composer install --no-dev --optimize-autoloader</code>". Please run that and refresh the page');
    }
} catch (Exception $e) {
    exit('Hello, it looks like you did not run: <code>composer install --no-dev --optimize-autoloader</code> Please run that and refresh');
}

use MythicalSystemsFramework\App;
use MythicalSystemsFramework\Api\Api as api;
use MythicalSystemsFramework\Plugins\PluginEvent;
use MythicalSystemsFramework\Web\Template\Engine;
use MythicalSystemsFramework\Plugins\PluginsManager;
use MythicalSystemsFramework\Web\Installer\Installer;

$router = new Router\Router();
$event = new PluginEvent();
global $event;

define('$event', $event);

/*
 * Check if the app is installed
 */
Installer::Installed($router);
/*
 * Check if the app is healthy and all requirements are met
 */
App::checkIfAppIsHealthy();

/**
 * Get the renderer :).
 */
$renderer = Engine::getRenderer();

/*
 * Load the routes.
 */
api::registerApiRoutes($router);
App::registerRoutes($renderer);
$event->emit('app.onRoutesLoaded', [$router]);

/*
 * Initialize the plugins manager.
 */
PluginsManager::init($router, $renderer, $event);

$router->add('/(.*)', function () {
    global $renderer;
    $renderer->addGlobal('page_name', '404');
    http_response_code(404);
    exit($renderer->render('/errors/404.twig'));
});

try {
    $router->route();
} catch (Exception $e) {
    exit('Failed to start app: ' . $e->getMessage());
}

Now I tried creating a value using define, so I can use it globally, but I did not work when the class was executed and was telling me that that's undefined, and the next problem would be that PluginManager is inited before the app folder so basically the events will not work since plugins start before events! So newly registered events after PluginManager.init() would not work!

And I don't really have an idea how to fix those issues!

All the explication is up there!


Solution

  • The global function was a PHP misconfiguration from the hosting provider of my client!

    And the events did not work because I registered the events after and not before!

    If someone has the same problems, you may copy some code from my project:

    http://github.com/MythicalLTD/Framework