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

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

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

8 ответов


можно использовать шаблон 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: неважно, он появляется только тогда, когда вы редактирование


Итак, предположим, вам не нужен шаблон 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. Он загружает классы, чтобы сделать что-то вставные.

в части 2, где мы загружаем класс. Обратите внимание, что мне не нужно было делать ничего особенного для класса, который значительно отличается от шаблона наблюдателя.

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

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

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


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

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

+1 к kdeloach от меня.


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

    <?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 и другие известные PHP-CMS, чтобы увидеть, как выглядят и чувствуют их API-крючки. Таким образом, вы даже можете получить идеи, которые вы, возможно, не думали ранее, чтобы сделать вещи немного более rubust.

более прямым ответом было бы написать общие файлы, которые они будут "include_once" в свой файл, который обеспечит удобство использования они потребоваться. Это будет разбито на категории и не предусмотрено в одном массивном "крючке".PHP-файл. Будьте осторожны, потому что в конечном итоге происходит то, что файлы, которые они включают, в конечном итоге имеют все больше зависимостей и улучшаются функции. Старайтесь держать зависимости API низкими. Т. е. меньше файлов для их включения.


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

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


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

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


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

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

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

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

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

  1. вам не нужно размещать какой-либо код на локальном сервере (безопасность)
  2. код может быть на удаленных серверах (расширяемость) на разных языках другое тогда PHP (переносимость)