Лучший способ разрешить плагины для PHP-приложения

StackOverflow https://stackoverflow.com/questions/42

Вопрос

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

Как можно вписать в свой код «крючки», чтобы плагины могли привязываться к определенным событиям?

Это было полезно?

Решение

Вы можете использовать шаблон наблюдателя.Простой функциональный способ сделать это:

<?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 для получения дополнительной информации.

Извините, похоже, символы подчеркивания заменяются объектами HTML с помощью Markdown?Я могу повторно опубликовать этот код, когда эта ошибка будет исправлена.

Редактировать:Неважно, это появляется только тогда, когда вы редактируете

Другие советы

Допустим, вам не нужен шаблон 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' перед оператором return он заблокирует лай собаки, а также заблокирует Dog_bark_afterEvent, поскольку не будет никакого события.

В части 4 это обычный код операции, но обратите внимание: то, что, по вашему мнению, могло бы работать, работает совсем не так.Например, собака называет свое имя не «Фидо», а «Коко».Собака говорит не «мяу», а «гав».А когда вы потом захотите посмотреть на имя собаки, вы обнаружите, что оно «Другой», а не «Коко».Все эти переопределения были представлены в Части 3.

Так как же это работает?Ну, давайте исключим eval() (который все называют «злым») и исключить, что это не шаблон Observer.Итак, он работает с помощью хитрого пустого класса под названием Pluggable, который не содержит методов и свойств, используемых классом Dog.Таким образом, когда это произойдет, для нас будут задействованы магические методы.Вот почему в частях 3 и 4 мы возимся с объектом, производным от класса Pluggable, а не с самим классом Dog.Вместо этого мы позволяем классу Plugin «прикасаться» к объекту Dog за нас.(Если это какой-то шаблон проектирования, о котором я не знаю, дайте мне знать.)

А крюк и слушатель метод является наиболее часто используемым, но есть и другие вещи, которые вы можете сделать.В зависимости от размера вашего приложения и того, кому вы разрешите просматривать код (будет ли это сценарий FOSS или что-то собственное), это будет сильно влиять на то, как вы хотите разрешить плагины.

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

+1 от меня кделоачу.

Вот подход, который я использовал, это попытка скопировать механизм сигналов/слотов 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();

?>

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

Более прямым ответом было бы написать общие файлы, которые они бы «include_once» в свой файл, что обеспечило бы необходимое им удобство использования.Это будет разбито на категории и НЕ будет представлено в одном ОГРОМНОМ файле «hooks.php».Однако будьте осторожны, потому что в конечном итоге файлы, которые они включают, приобретают все больше и больше зависимостей, а функциональность улучшается.Старайтесь свести к минимуму зависимости API.То есть меньше файлов для их включения.

Есть классный проект под названием Колюшка Мэтт Зандстра из Yahoo, который выполняет большую часть работы по работе с плагинами в PHP.

Он реализует интерфейс класса плагина, поддерживает интерфейс командной строки, и его не так уж сложно запустить и запустить, особенно если вы прочитаете о нем статью в Журнал PHP-архитекторов.

Хороший совет — посмотреть, как это сделали другие проекты.Многие требуют установки плагинов и регистрации их «имени» для сервисов (как это делает WordPress), поэтому в вашем коде есть «точки», где вы вызываете функцию, которая идентифицирует зарегистрированных прослушивателей и выполняет их.Стандартный шаблон объектно-ориентированного проектирования — это Модель наблюдателя, что было бы хорошим вариантом для реализации в действительно объектно-ориентированной системе PHP.

А Zend-фреймворк использует множество методов перехвата и имеет очень хорошую архитектуру.Это была бы хорошая система, на которую стоит обратить внимание.

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

А что, если вы хотите, чтобы плагины запускались на другом удаленном сервере?Лучший способ сделать это — предоставить форму, позволяющую определять различные URL-адреса, которые будут вызываться при возникновении определенных событий в вашем приложении.

Различные события будут отправлять разную информацию в зависимости от только что произошедшего события.

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

Это дает два преимущества:

  1. Вам не нужно размещать какой-либо код на локальном сервере (безопасность).
  2. Код может находиться на удаленных серверах (расширяемость) на разных языках, кроме PHP (переносимость).
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top