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

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

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

Ответ 1

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

<?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? Я могу повторно опубликовать этот код, когда эта ошибка будет исправлена.

Edit: Nevermind, он появляется только тогда, когда вы редактируете

Ответ 2

Итак, скажем, вам не нужен шаблон 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;

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

В части 2, где мы загружаем класс. Примечание. Мне не нужно было делать ничего особенного для класса, который существенно отличается от шаблона Observer.

В части 3, где мы переключим наш класс на "подключаемый" (т.е. поддерживаем плагины, которые позволяют нам переопределять методы и свойства класса). Так, например, если у вас есть веб-приложение, у вас может быть реестр плагинов, и вы можете активировать плагины здесь. Обратите внимание также на функцию Dog_bark_beforeEvent(). Если я установил $mixed = 'BLOCK_EVENT' перед оператором return, он заблокирует собаку от лая и также заблокирует Dog_bark_afterEvent, потому что не будет никакого события.

В части 4, что нормальный код операции, но обратите внимание, что то, что вы могли бы подумать, будет работать не так. Например, собака не объявляет его именем "Fido", но "Coco". Собака не говорит "мяу", но "Уоф". И когда вы захотите посмотреть имя собаки потом, вы обнаружите, что это "Разное", а не "Коко". Все эти переопределения были представлены в части 3.

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

Ответ 3

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

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

+1 к kdeloach от меня.

Ответ 4

Вот подход, который я использовал, это попытка скопировать из механизма Qt сигналов/слотов, своего рода шаблон Observer. Объекты могут излучать сигналы. Каждый сигнал имеет идентификатор в системе - он состоит из идентификатора id + объекта-отправителя Каждый сигнал может быть привязан к приемникам, который просто является "вызываемым", Вы используете класс шины для передачи сигналов любому, кто заинтересован в их получении Когда что-то происходит, вы посылаете сигнал. Ниже приведен пример и пример реализации

    <?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();

?>

Ответ 5

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

Более прямой ответ заключался бы в том, чтобы написать общие файлы, которые они бы включили в include файл, чтобы обеспечить удобство использования. Это будет разбито на категории и НЕ предоставлено в одном файле "hooks.php" MASSIVE. Будьте осторожны, потому что то, что заканчивается, заключается в том, что файлы, которые они включают, в конечном итоге имеют все больше и больше зависимостей и функциональности. Попытайтесь ограничить зависимость API. I.E меньше файлов для их включения.

Ответ 6

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

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

Ответ 7

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

Zend Framework использует многие методы подключения и очень хорошо продуман. Это была бы хорошая система для просмотра.

Ответ 8

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

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

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

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

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

  • Вам не нужно размещать код на локальном сервере (безопасность)
  • Код может быть на удаленных серверах (расширяемость) на разных языках, кроме PHP (переносимость)