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

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

Obraz ilustrujacy 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 6

Kategorie

programowanie

Tagi

koseven php

Dziękujemy!
()

Powiązane wpisy

Ilustracja tematu Automatyczny motyw Bootstrap 53 na bazie pory dnia
4 stycznia 2025 6 min 71

Automatyczny motyw Bootstrap 5.3 na bazie pory dnia

php
Czytaj więcej
Grafika przedstawia Wyraenia regularne i ich obsuga w PHP oraz JavaScript
1 lutego 2025 5 min 19

Wyrażenia regularne i ich obsługa w PHP oraz JavaScript

php
Czytaj więcej
Wizualizacja tematu Klasa Promise w PHP i jej zastosowanie
5 lutego 2025 7 min 39

Klasa Promise w PHP i jej zastosowanie

php
Czytaj więcej
Wymiana doświadczeń

Masz podobne doświadczenia?

Chętnie poznam Twoją perspektywę i porozmawiam o tym temacie szerzej.

Napisz do mnie

Każda perspektywa może wnieść coś wartościowego do dyskusji.

Twoja prywatność i pliki cookies

  1. Ta strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania – m.in. do poprawnego wyświetlania treści, zapamiętania podstawowych ustawień przeglądarki oraz zapewnienia stabilności serwisu.
  2. Nie stosuję plików cookies w celach marketingowych, reklamowych ani analitycznych.
  3. Strona ma charakter wyłącznie informacyjny i nie zawiera formularzy kontaktowych, rejestracyjnych ani zakupowych, przez które dane mogłyby być przesyłane na serwer.
  4. Nie zbieram danych osobowych podczas zwykłego korzystania z witryny.
  5. Serwis nie korzysta z certyfikatu SSL, jednak ze względu na informacyjny charakter strony nie jest wymagane przesyłanie poufnych danych. Zalecam jednak, aby nigdy nie wpisywać haseł ani danych osobowych na stronach bez szyfrowanego połączenia.
  6. Korzystając z tej strony, wyrażasz zgodę na używanie wyłącznie niezbędnych plików cookies.

Więcej informacji znajdziesz w mojej polityce prywatności.