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

php plugins architecture hook


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

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




Answer 1 Kevin


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

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

Output:

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

Notes:

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

Это всего лишь один из способов реализации системы плагинов в PHP.Есть лучшие альтернативы,я предлагаю вам ознакомиться с WordPress Documentation для получения дополнительной информации.




Answer 2 Volomike


Итак, допустим, вам не нужен шаблон 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 это нормальный код работы,но заметьте,что то,что вы могли бы подумать о том,чтобы запустить,совсем не так работает.Например,собака не объявляет свое имя 'Fido',а 'Coco'.Собака говорит не "мяу",а "гав".И когда вы хотите посмотреть на имя собаки после этого,вы обнаружите,что это "Другой",а не "Коко".Все эти переопределения были предусмотрены в третьей части.

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




Answer 3 w-ll


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

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

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




Answer 4 andy.gurin


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

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

?>



Answer 5 helloandre


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

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