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

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

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

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

159
27.02.2020 07:45:37
Обратите внимание, что для PHP> = 5.0 вы можете реализовать это с помощью интерфейсов Observer / Subject, определенных в SPL: php.net/manual/en/class.splobserver.php
John Carter 25.02.2010 14:47:31
Педантическое примечание: это не пример паттерна Observer. Это пример Mediator Pattern. Истинные наблюдатели - это просто уведомления, нет передачи сообщений или условных уведомлений (и при этом нет центрального менеджера для управления уведомлениями). Это не делает ответ неправильным , но следует
ircmaxell 9.02.2011 23:35:07
Обратите внимание, что при использовании нескольких перехватчиков / прослушивателей вы должны возвращать только строки или массивы, а не оба. Я реализовал нечто подобное для Hound CMS - getbutterfly.com/hound .
Ciprian 19.12.2017 11:21:08

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

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

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

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

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

58
15.05.2016 16:03:48
Разве это не декоратор?
MV. 7.11.2013 12:29:37
Volomike 8.11.2013 19:07:31

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

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

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

35
1.08.2008 17:23:43

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

?>
24
3.01.2010 13:57:14

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

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

18
1.08.2008 13:44:35
Я бы добавил DokuWiki в список систем, на которые вы можете взглянуть. У этого есть хорошая система событий, которая учитывает богатую экосистему плагина.
chiborg 3.08.2010 13:53:15

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

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

15
25.05.2012 21:01:19

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

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

11
17.09.2008 19:38:52

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

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

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

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

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

  1. Вам не нужно размещать код на локальном сервере (безопасность)
  2. Код может быть на удаленных серверах (расширяемость) на разных языках, кроме PHP (переносимость)
8
22.04.2013 07:41:24
Это скорее «push API», чем «плагин» - вы предоставляете другим службам возможность получать уведомления о выбранных событиях. Под «плагинами» обычно подразумевается то, что вы можете установить приложение, а затем добавить функциональность для настройки его поведения в соответствии с вашими целями, что требует, чтобы плагин работал локально - или, по крайней мере, имел безопасную и эффективную двустороннюю связь для Информация для приложения не просто взять его из нее. Эти две функции несколько различаются, и во многих случаях «канал» (например, RSS, iCal) является простой альтернативой push API.
IMSoP 14.06.2013 18:47:34