Лучший способ разрешить плагины для PHP-приложения
-
08-06-2019 - |
Вопрос
Я запускаю новое веб-приложение на 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), где удаленные серверы могут выполнять задачи на основе информации, отправленной вашим приложением.
Это дает два преимущества:
- Вам не нужно размещать какой-либо код на локальном сервере (безопасность).
- Код может находиться на удаленных серверах (расширяемость) на разных языках, кроме PHP (переносимость).