Найкращий спосіб дозволити плагіни для програми PHP


276

Я запускаю нову веб-програму в PHP, і на цей раз я хочу створити щось, що люди можуть розширити за допомогою інтерфейсу плагінів.

Як можна писати «гачки» у свій код, щоб плагіни могли приєднуватися до конкретних подій?

Відповіді:


162

Ви можете використовувати шаблон спостерігача. Простий функціональний спосіб досягти цього:

<?php

/** Plugin system **/

$listeners = array();

/* Create an entry point for plugins */
function hook() {
    global $listeners;

    $num_args = func_num_args();
    $args = func_get_args();

    if($num_args < 2)
        trigger_error("Insufficient arguments", E_USER_ERROR);

    // Hook name should always be first argument
    $hook_name = array_shift($args);

    if(!isset($listeners[$hook_name]))
        return; // No plugins have registered this hook

    foreach($listeners[$hook_name] as $func) {
        $args = $func($args); 
    }
    return $args;
}

/* Attach a function to a hook */
function add_listener($hook, $function_name) {
    global $listeners;
    $listeners[$hook][] = $function_name;
}

/////////////////////////

/** Sample Plugin **/
add_listener('a_b', 'my_plugin_func1');
add_listener('str', 'my_plugin_func2');

function my_plugin_func1($args) {
    return array(4, 5);
}

function my_plugin_func2($args) {
    return str_replace('sample', 'CRAZY', $args[0]);
}

/////////////////////////

/** Sample Application **/

$a = 1;
$b = 2;

list($a, $b) = hook('a_b', $a, $b);

$str  = "This is my sample application\n";
$str .= "$a + $b = ".($a+$b)."\n";
$str .= "$a * $b = ".($a*$b)."\n";

$str = hook('str', $str);
echo $str;
?>

Вихід:

This is my CRAZY application
4 + 5 = 9
4 * 5 = 20

Примітки:

У цьому прикладі вихідного коду потрібно оголосити всі свої плагіни перед фактичним вихідним кодом, який ви бажаєте розширити. Я включив приклад того, як обробляти одне або кілька значень, переданих плагіну. Найважче це - написання фактичної документації, в якій перераховано, які аргументи передаються кожному гачку.

Це лише один метод створення плагінної системи в PHP. Є кращі альтернативи, я пропоную вам переглянути документацію WordPress для отримання додаткової інформації.


3
Зауважте, що для PHP> = 5.0 ви можете реалізувати це за допомогою інтерфейсів Observer / Subject, визначених у SPL: php.net/manual/en/class.splobserver.php
Джон Картер,

20
Педантична примітка: це не приклад схеми спостерігача. Це приклад Mediator Pattern. Справжні спостерігачі - це суто сповіщення, немає повідомлення про передачу та умовне повідомлення (також немає центрального менеджера для контролю над повідомленнями). Це не робить неправильну відповідь , але слід зазначити, що люди перестають називати речі невірним іменем ...
ircmaxell

Зауважте, що при використанні декількох гаків / слухачів ви повинні повертати лише рядки або масиви, а не обидва. Я реалізував щось подібне для Hound CMS - getbutterfly.com/hound .
Кіпріан

59

Тож скажімо, що ви не хочете шаблону Observer, оскільки він вимагає змінити методи свого класу, щоб впоратися із завданням прослуховування, і хочете чогось загального. Скажімо, ви не хочете використовувати extendsспадщину, тому що ви вже можете успадковувати у своєму класі від іншого класу. Чи не було б чудовим способом зробити будь-який клас підключення без особливих зусиль ? Ось як:

<?php

////////////////////
// PART 1
////////////////////

class Plugin {

    private $_RefObject;
    private $_Class = '';

    public function __construct(&$RefObject) {
        $this->_Class = get_class(&$RefObject);
        $this->_RefObject = $RefObject;
    }

    public function __set($sProperty,$mixed) {
        $sPlugin = $this->_Class . '_' . $sProperty . '_setEvent';
        if (is_callable($sPlugin)) {
            $mixed = call_user_func_array($sPlugin, $mixed);
        }   
        $this->_RefObject->$sProperty = $mixed;
    }

    public function __get($sProperty) {
        $asItems = (array) $this->_RefObject;
        $mixed = $asItems[$sProperty];
        $sPlugin = $this->_Class . '_' . $sProperty . '_getEvent';
        if (is_callable($sPlugin)) {
            $mixed = call_user_func_array($sPlugin, $mixed);
        }   
        return $mixed;
    }

    public function __call($sMethod,$mixed) {
        $sPlugin = $this->_Class . '_' .  $sMethod . '_beforeEvent';
        if (is_callable($sPlugin)) {
            $mixed = call_user_func_array($sPlugin, $mixed);
        }
        if ($mixed != 'BLOCK_EVENT') {
            call_user_func_array(array(&$this->_RefObject, $sMethod), $mixed);
            $sPlugin = $this->_Class . '_' . $sMethod . '_afterEvent';
            if (is_callable($sPlugin)) {
                call_user_func_array($sPlugin, $mixed);
            }       
        } 
    }

} //end class Plugin

class Pluggable extends Plugin {
} //end class Pluggable

////////////////////
// PART 2
////////////////////

class Dog {

    public $Name = '';

    public function bark(&$sHow) {
        echo "$sHow<br />\n";
    }

    public function sayName() {
        echo "<br />\nMy Name is: " . $this->Name . "<br />\n";
    }


} //end class Dog

$Dog = new Dog();

////////////////////
// PART 3
////////////////////

$PDog = new Pluggable($Dog);

function Dog_bark_beforeEvent(&$mixed) {
    $mixed = 'Woof'; // Override saying 'meow' with 'Woof'
    //$mixed = 'BLOCK_EVENT'; // if you want to block the event
    return $mixed;
}

function Dog_bark_afterEvent(&$mixed) {
    echo $mixed; // show the override
}

function Dog_Name_setEvent(&$mixed) {
    $mixed = 'Coco'; // override 'Fido' with 'Coco'
    return $mixed;
}

function Dog_Name_getEvent(&$mixed) {
    $mixed = 'Different'; // override 'Coco' with 'Different'
    return $mixed;
}

////////////////////
// PART 4
////////////////////

$PDog->Name = 'Fido';
$PDog->Bark('meow');
$PDog->SayName();
echo 'My New Name is: ' . $PDog->Name;

У частині 1 це саме те, що ви можете включити під час require_once()дзвінка у верхній частині сценарію PHP. Він завантажує класи, щоб зробити щось підключається.

У другій частині ми тут завантажуємо клас. Зауважте, мені не довелося робити нічого особливого для класу, що суттєво відрізняється від шаблону Observer.

У Частині 3 саме тут ми перетворюємо наш клас на те, щоб бути "підключуваним" (тобто підтримує плагіни, які дозволяють нам переосмислити методи та властивості класу). Так, наприклад, якщо у вас є веб-додаток, у вас може бути реєстр плагінів, і ви можете активувати плагіни тут. Зауважте також Dog_bark_beforeEvent()функцію. Якщо я встановлю $mixed = 'BLOCK_EVENT'перед заявою повернення, вона заблокує собаку від гавкання, а також заблокує Dog_bark_afterEvent, оскільки не було б жодної події.

У частині 4 це нормальний код роботи, але зауважте, що те, що ви можете подумати, що працює, зовсім не працює. Наприклад, собака не оголошує її ім'ям як "Fido", а "Coco". Собака каже не «мяу», а «чудо». А потім, коли ви хочете подивитися на ім'я собаки, то виявите, що це "Різне", а не "Коко". Усі ці скасування були викладені в частині 3.

То як це працює? Що ж, давайте виключаємо eval()(що всі кажуть, що "зло") і виключаємо, що це не зразок спостерігача. Отже, спосіб його роботи - це підлий порожній клас під назвою Pluggable, який не містить методів та властивостей, використовуваних класом Dog. Таким чином, оскільки це відбувається, магічні методи будуть залучати нас. Ось чому в частинах 3 та 4 ми псуємось із об’єктом, похідним від класу Pluggable, а не самим класом Dog. Натомість, ми дозволяємо класу Plugin робити «торкання» об’єкта Dog для нас. (Якщо це якась модель дизайну, про яку я не знаю - будь ласка, дайте мені знати.)


3
Хіба це не декоратор?
МВ.

1
Я читав у Вікіпедії про це, і, правда, ти маєш рацію! :)
Воломіке

35

Крюк і слухач найчастіше застосовується, але є й інші речі. Залежно від розміру вашої програми та того, хто збирається дозволити побачити код (це буде FOSS-скрипт чи щось вдома), буде сильно впливати на те, як ви хочете дозволити плагіни.

kdeloach має приємний приклад, але його реалізація та функція гака трохи небезпечна. Я б просив вас дати більше інформації про природу програми php, яку ви пишете, і про те, як ви бачите, як плагіни вміщуються.

+1 для kdeloach від мене.


25

Ось підхід, який я використав, це спроба копіювання з механізму Qt сигналів / слотів, свого роду шаблон спостерігача. Об'єкти можуть випромінювати сигнали. Кожен сигнал має ідентифікатор в системі - він складається з ідентифікатора відправника + ім'я об'єкта. Кожен сигнал може бути прив’язаний до приймачів, що просто є "дзвінким". Ви використовуєте клас шини для передачі сигналів будь-кому, хто зацікавлений у їх отриманні. трапляється, ви "відправляєте" сигнал. Нижче наведено та приклад реалізації

    <?php

class SignalsHandler {


    /**
     * hash of senders/signals to slots
     *
     * @var array
     */
    private static $connections = array();


    /**
     * current sender
     *
     * @var class|object
     */
    private static $sender;


    /**
     * connects an object/signal with a slot
     *
     * @param class|object $sender
     * @param string $signal
     * @param callable $slot
     */
    public static function connect($sender, $signal, $slot) {
        if (is_object($sender)) {
            self::$connections[spl_object_hash($sender)][$signal][] = $slot;
        }
        else {
            self::$connections[md5($sender)][$signal][] = $slot;
        }
    }


    /**
     * sends a signal, so all connected slots are called
     *
     * @param class|object $sender
     * @param string $signal
     * @param array $params
     */
    public static function signal($sender, $signal, $params = array()) {
        self::$sender = $sender;
        if (is_object($sender)) {
            if ( ! isset(self::$connections[spl_object_hash($sender)][$signal])) {
                return;
            }
            foreach (self::$connections[spl_object_hash($sender)][$signal] as $slot) {
                call_user_func_array($slot, (array)$params);
            }

        }
        else {
            if ( ! isset(self::$connections[md5($sender)][$signal])) {
                return;
            }
            foreach (self::$connections[md5($sender)][$signal] as $slot) {
                call_user_func_array($slot, (array)$params);
            }
        }

        self::$sender = null;
    }


    /**
     * returns a current signal sender
     *
     * @return class|object
     */
    public static function sender() {
        return self::$sender;
    }

}   

class User {

    public function login() {
        /**
         * try to login
         */
        if ( ! $logged ) {
            SignalsHandler::signal(this, 'loginFailed', 'login failed - username not valid' );
        }
    }

}

class App {
    public static function onFailedLogin($message) {
        print $message;
    }
}


$user = new User();
SignalsHandler::connect($user, 'loginFailed', array($Log, 'writeLog'));
SignalsHandler::connect($user, 'loginFailed', array('App', 'onFailedLogin'));

$user->login();

?>

18

Я вважаю, що найпростішим способом було б дотримуватися власних порад Джеффа і оглянути існуючий код. Спробуйте подивитися Wordpress, Drupal, Joomla та інші добре відомі на основі PHP CMS, щоб побачити, як виглядають і відчуваються їх гачки API. Таким чином ви навіть можете отримати ідеї, про які ви, можливо, не думали раніше, щоб зробити речі трохи більш жорсткими.

Більш прямою відповіддю було б написати загальні файли, які вони "включать_once" у свій файл, що забезпечить зручність, яка їм потрібна. Це було б розбито на категорії і НЕ надано в одному МАСИВНОМУ файлі "hooks.php". Але будьте обережні, адже те, що в підсумку відбувається, - це те, що файли, які вони включають, виявляють все більше залежностей, а функціональність покращується. Намагайтеся підтримувати низькі залежності API. IE менше файлів для їх включення.


Я б додав DokuWiki до списку систем, які ви можете подивитися. У ньому є приємна система подій, яка дозволяє отримати багату екосистему плагінів.
чиборг

15

У Yahoo є акуратний проект під назвою Stickleback від Matt Zandstra, який займається великою частиною роботи з обробки плагінів у PHP.

Він застосовує інтерфейс класу плагінів, підтримує інтерфейс командного рядка і не надто важко вставати і працювати, особливо якщо ви читаєте обкладинку про нього в журналі архітектора PHP .


11

Гарна порада - подивитися, як це зробили інші проекти. Багато хто закликає встановити плагіни та їх "ім'я" зареєструватись для служб (як Wordpress робить), тож у вашому коді є "точки", де ви викликаєте функцію, яка ідентифікує зареєстрованих слухачів та виконує їх. Стандартний шаблон дизайну OO - це шаблон спостерігача , який був би хорошим варіантом для втілення в справді об'єктно-орієнтованій системі PHP.

Zend Framework використовує безліч методів Крюкова, і дуже добре спроектований. Це була б гарна система.


8

Я здивований, що більшість відповідей тут, здається, орієнтовані на плагіни, локальні для веб-програми, тобто плагіни, які працюють на локальному веб-сервері.

А як бути, якщо ви хочете, щоб плагіни запускалися на іншому - віддаленому сервері? Найкращим способом зробити це було б надати форму, яка дозволяє визначати різні URL-адреси, які б викликалися, коли у вашій програмі відбуваються конкретні події.

Різні події надсилають різну інформацію залежно від події, яка щойно відбулася.

Таким чином, ви б просто здійснили виклик CURL на URL-адресу, надану вашій програмі (наприклад, через https), де віддалені сервери можуть виконувати завдання на основі інформації, надісланої вашою заявкою.

Це забезпечує дві переваги:

  1. Вам не потрібно розміщувати жоден код на локальному сервері (безпека)
  2. Код може бути на віддалених серверах (розширюваність) іншими мовами, крім PHP (портативність)

8
Це скоріше "push API", ніж система "плагінів" - ви надаєте спосіб іншим службам отримувати повідомлення про вибрані події. Під плагінами зазвичай розуміється те, що ви можете встановити додаток, а потім додати функціональність, щоб налаштувати його поведінку під свої цілі, для чого плагін повинен працювати локально - або принаймні мати безпечну та ефективну двосторонній зв’язок для забезпечення інформація до програми не просто бере її від неї. Дві функції дещо виразні, і для багатьох випадків "канал" (наприклад, RSS, iCal) є простою альтернативою API push.
IMSoP
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.