Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

System pluginów w Koseven – architektura, lifecycle i rozszerzalność

Grafika przedstawia System pluginw w Koseven  architektura lifecycle i rozszerzalno

Wstęp

Koseven, jako fork Kohany, zachował prostą, plikową architekturę, opartą na konwencjach PSR-0 i autoloadzie klas z katalogu classes/. To sprawia, że framework idealnie nadaje się do budowy systemu pluginów, który:

  • nie wymaga bazy danych
  • nie korzysta z composera
  • nie używa namespaces
  • jest zgodny z filozofią frameworka

W tym artykule opisano praktyczną implementację systemu pluginów w Koseven, inspirowaną WordPressem, ale zaprojektowaną w sposób jawny, testowalny i bez „magii”.

Założenia architektoniczne

Projekt systemu pluginów opiera się na kilku kluczowych założeniach:

  1. Plugin ≠ moduł
    • moduły rozszerzają core
    • pluginy rozszerzają zachowanie aplikacji
  2. Brak bazy danych
    • stan pluginów przechowywany w pliku PHP (storage/plugins.php)
  3. PSR-0 i klasy z podkreśleniem
    • pełna kompatybilność z autoloadem Koseven
  4. Jawny lifecycle
    • activate()
    • boot()
    • deactivate()
  5. Rozdzielenie Eventów i Hooków
    • Event → domena biznesowa
    • Hook → rozszerzalność

Struktura katalogów

/application
  /classes
    Event.php
    Hook.php
    Plugin_Manager.php
    Plugin_Interface.php

  /storage
    plugins.php

/plugins
  /article_stats
    plugin.php
    /classes
      Article/
        Stats/
          Plugin.php

Stan pluginów

Stan aktywności pluginów przechowywany jest w jednym pliku:

application/storage/plugins.php

<?php defined('SYSPATH') or die('No direct script access.');

return [
    'article_stats' => true,
];
  • klucz = slug pluginu
  • true = włączony
  • false lub brak = wyłączony

Dzięki temu:

  • system działa bez DB
  • łatwo debugować
  • ręczne usunięcie pluginu nie powoduje błędów

Metadane pluginu

Każdy plugin posiada plik opisowy:

plugins/article_stats/plugin.php

<?php defined('SYSPATH') or die('No direct script access.');

return [
    'name'        => 'Article Statistics',
    'description' => 'Zlicza wyświetlenia artykułów',
    'version'     => '1.0.0',
    'author'      => 'ACME',
];

Plik ten:

  • nie jest wykonywany
  • służy wyłącznie do wyświetlania informacji w panelu admina

Interfejs pluginu

Każdy plugin musi implementować wspólny interfejs:

<?php defined('SYSPATH') or die('No direct script access.');

interface Plugin_Interface
{
    public function activate();
    public function boot();
    public function deactivate();
}

Dzięki temu PluginManager ma gwarancję spójnego API.

Eventy – system zdarzeń

System eventów umożliwia pluginom:

  • logowanie zdarzeń
  • realizację systemów powiadomień

Klasa Event

<?php defined('SYSPATH') or die('No direct script access.');

class Event
{
    protected static $listeners = [];

    public static function listen($event, callable $callback)
    {
        static::$listeners[$event][] = $callback;
    }

    public static function dispatch($event, array $payload = [])
    {
        if (empty(static::$listeners[$event]))
        {
            return;
        }

        foreach (static::$listeners[$event] as $listener)
        {
            call_user_func_array($listener, $payload);
        }
    }
}

Hooki – system rozszerzeń

System hooków umożliwia pluginom:

  • reagowanie na zdarzenia aplikacji (actions)
  • modyfikowanie danych (filters)

Klasa Hook

<?php defined('SYSPATH') or die('No direct script access.');

class Hook
{
    protected static $actions = [];
    protected static $filters = [];

    /* =======================
     * ACTIONS
     * ======================= */

    public static function add_action($name, callable $callback)
    {
        static::$actions[$name][] = $callback;
    }

    public static function do_action($name, array $payload = [])
    {
        if (empty(static::$actions[$name]))
        {
            return;
        }

        foreach (static::$actions[$name] as $callback)
        {
            call_user_func_array($callback, $payload);
        }
    }

    /* =======================
     * FILTERS
     * ======================= */

    public static function add_filter($name, callable $callback)
    {
        static::$filters[$name][] = $callback;
    }

    public static function apply_filters($name, $value)
    {
        if (empty(static::$filters[$name]))
        {
            return $value;
        }

        foreach (static::$filters[$name] as $callback)
        {
            $value = call_user_func_array($callback, [$value]);
        }

        return $value;
    }
}

Event vs Hook – rozdzielenie odpowiedzialności

Event Hook
domena biznesowa rozszerzalność
informacyjny ingerujący
brak zwrotki pipeline wartości
deterministyczny opcjonalny

Przykład:

  • article.viewed → Event
  • article.title → Hook

PluginManager – serce systemu

PluginManager odpowiada za:

  • ładowanie pluginów
  • aktywację i dezaktywację
  • mapowanie slug → klasa → plik

Mapowanie PSR-0

slug: article_stats
class: Article_Stats_Plugin
file: plugins/article_stats/classes/Article/Stats/Plugin.php

Kluczowy fragment _boot_plugin()

$base  = implode('_', array_map('ucfirst', explode('_', $slug)));
$class = $base.'_Plugin';

$file = $classes_path.DIRECTORY_SEPARATOR
    .str_replace('_', DIRECTORY_SEPARATOR, $base)
    .DIRECTORY_SEPARATOR.'Plugin.php';

Cała klasa plugin managera

<?php defined('SYSPATH') or die('No direct script access.');

class Plugin_Manager
{
    protected $_state_file;
    protected $_enabled;
    protected $_instances = [];

    public function __construct()
    {
        $this->_state_file = APPPATH.'storage/plugins.php';
        $this->_enabled = $this->_load_state();
    }

    public function load()
    {
        foreach ($this->_enabled as $slug => $enabled)
        {
            if ($enabled === true)
            {
                $this->_boot_plugin($slug);
            }
        }
    }

    public function activate($slug)
    {
        $plugins = $this->_load_state();

        if ( ! empty($plugins[$slug]))
        {
            return;
        }

        $plugin = $this->_boot_plugin($slug);
        $plugin->activate();

        $plugins[$slug] = true;
        $this->_save_state($plugins);
    }

    public function deactivate($slug)
    {
        $plugins = $this->_load_state();

        if (empty($plugins[$slug]))
        {
            return;
        }

        $plugin = $this->_boot_plugin($slug);
        $plugin->deactivate();

        $plugins[$slug] = false;
        $this->_save_state($plugins);

    }

    public function list_plugins()
    {
        $state   = $this->_enabled;
        $plugins = [];

        $root = DOCROOT.'plugins'.DIRECTORY_SEPARATOR;

        foreach (glob($root.'*', GLOB_ONLYDIR) as $dir)
        {
            $slug = basename($dir);

            $info_file = $dir.DIRECTORY_SEPARATOR.'plugin.php';

            if ( ! is_file($info_file))
            {
                continue;
            }

            $info = include $info_file;

            $plugins[$slug] = [
                'slug'        => $slug,
                'name'        => Arr::get($info, 'name', $slug),
                'description' => Arr::get($info, 'description', ''),
                'version'     => Arr::get($info, 'version', ''),
                'author'      => Arr::get($info, 'author', ''),
                'enabled'     => ! empty($state[$slug]),
            ];
        }

        return $plugins;
    }

    protected function _boot_plugin($slug)
    {
        if (isset($this->_instances[$slug]))
        {
            return $this->_instances[$slug];
        }

        $plugin_root = DOCROOT.'plugins'.DIRECTORY_SEPARATOR.$slug;
        $classes_path = $plugin_root.DIRECTORY_SEPARATOR.'classes';

        if ( ! is_dir($classes_path))
        {
            throw new Kohana_Exception('Plugin :slug not found', [
                ':slug' => $slug,
            ]);
        }

        /**
         * article_stats → Article_Stats
         */
        $base_name = implode('_', array_map('ucfirst', explode('_', $slug)));

        /**
         * Article_Stats_Plugin
         */
        $class = $base_name.'_Plugin';

        /**
         * PSR-0:
         * Article_Stats_Plugin
         * → Article/Stats/Plugin.php
         */
        $file = $classes_path.DIRECTORY_SEPARATOR.
            str_replace('_', DIRECTORY_SEPARATOR, $base_name).
            DIRECTORY_SEPARATOR.'Plugin.php';

        if ( ! is_file($file))
        {
            throw new Kohana_Exception(
                'Plugin file not found: :file',
                [':file' => $file]
            );
        }

        require_once $file;

        if ( ! class_exists($class))
        {
            throw new Kohana_Exception(
                'Plugin class :class not found',
                [':class' => $class]
            );
        }

        $plugin = new $class;

        if ( ! $plugin instanceof Plugin_Interface)
        {
            throw new Kohana_Exception(
                'Plugin :class must implement Plugin_Interface',
                [':class' => $class]
            );
        }

        $plugin->boot();

        $this->_instances[$slug] = $plugin;
        return $this->_instances[$slug];
    }

    protected function _load_state()
    {
        if ( ! is_file($this->_state_file))
        {
            return [];
        }

        return include $this->_state_file;
    }

    protected function _save_state(array $plugins)
    {
        $export = "<?php defined('SYSPATH') or die('No direct script access.');\n\nreturn ".var_export($plugins, true).";\n";

        file_put_contents($this->_state_file, $export, LOCK_EX);
    }

}

Lifecycle pluginu

install
  ↓
activate() 
  ↓
boot()
  ↓
runtime
  ↓
deactivate()

Przykład pluginu

<?php defined('SYSPATH') or die('No direct script access.'); 

class Article_Stats_Plugin implements Plugin_Interface
{
    public function boot()
    {
        Hook::add_action('article.viewed', [$this, 'on_view']);
    }

    public function activate()
    {
        Kohana::$log->add(Log::INFO, 'Plugin activated');
    }

    public function deactivate()
    {
        Kohana::$log->add(Log::INFO, 'Plugin deactivated');
    }

    public function on_view($article)
    {
        $article->views++;
        $article->save();
    }
}

Zastosowanie w kontrolerze

Event::dispatch('article.viewed', [$article]);

$title = Hook::apply_filters('article.title', [$article->title]);

Kontroler:

  • nie zna pluginów
  • nie zawiera ifów
  • jest testowalny

Zalety podejścia

Możliwe rozszerzenia

  • priorytety hooków
  • remove_action / remove_filter
  • zależności pluginów
  • CLI (plugin enable article_stats)
  • upload ZIP
  • uninstall pluginu
  • cache mapy pluginów

Podsumowanie

Przedstawiony system pluginów:

  • wykorzystuje naturalne mechanizmy Koseven
  • zachowuje prostotę Kohany
  • daje elastyczność znaną z WordPressa
  • pozostaje czytelny i przewidywalny

To solidna baza pod rozszerzalną aplikację bez narzutów typowych dla ciężkich frameworków.

22 grudnia 2025 2

Kategorie

programowanie

Tagi

koseven php

Dziękujemy!
()

Powiązane wpisy


Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.