Лучший способ разрешить плагины для PHP-приложения
Я запускаю новое веб-приложение на PHP, и на этот раз я хочу создать что-то, что люди могут расширить с помощью интерфейса плагина.
Как можно написать "крючки" в свой код, чтобы плагины могли подключаться к определенным событиям?
8 answers
Вы могли бы использовать шаблон наблюдателя. Простой функциональный способ сделать это:
<?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-объектами с помощью уценки? Я могу повторно опубликовать этот код, когда эта ошибка будет исправлена.
Редактировать: Неважно, это выглядит так только при редактировании
Итак, допустим, вам не нужен шаблон наблюдателя, потому что он требует, чтобы вы изменили методы своего класса для выполнения задачи прослушивания, и хотите что-то общее. И допустим, вы не хотите использовать наследование 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 именно здесь мы переключаем наш класс на "подключаемый" (то есть поддерживает плагины, которые позволяют нам переопределять методы и свойства класса). Так, например, если у вас есть веб-приложение, у вас может быть реестр плагинов, и вы можно активировать плагины здесь. Обратите также внимание на функцию Dog_bark_beforeEvent()
. Если я установлю $mixed = 'BLOCK_EVENT'
перед оператором return, он заблокирует лай собаки, а также заблокирует событие Dog_bark_afterEvent, потому что не будет никакого события.
В части 4 это обычный код операции, но обратите внимание, что то, что вы могли бы подумать, будет выполняться совсем не так. Например, собака объявляет свое имя не как "Фидо", а как "Коко". Собака говорит не "мяу", а "Гав". И когда ты захочешь посмотреть после того, как вы назовете собаку, вы обнаружите, что она "Другая", а не "Коко". Все эти переопределения были предоставлены в части 3.
Так как же это работает? Что ж, давайте исключим eval()
(что все называют "злом") и исключим, что это не шаблон наблюдателя. Итак, способ, которым это работает, - это скрытый пустой класс, называемый Pluggable, который не содержит методов и свойств, используемых классом Dog. Таким образом, поскольку это происходит, магические методы будут задействованы для нас. Вот почему в частях 3 и 4 мы возитесь с объектом, производным от подключаемого класса, а не с самим классом Dog. Вместо этого мы позволяем классу плагина выполнять "прикосновение" к объекту Dog за нас. (Если это какой-то шаблон дизайна, о котором я не знаю - пожалуйста, дайте мне знать.)
Метод крючка и прослушивателя является наиболее часто используемым, но есть и другие вещи, которые вы можете сделать. В зависимости от размера вашего приложения и от того, кому вы разрешите просматривать код (будет ли это скрипт FOSS или что-то в этом роде), это сильно повлияет на то, как вы хотите разрешить плагины.
У Kdeloach есть хороший пример, но его реализация и функция крючка немного небезопасны. Я бы попросил вас предоставить больше информации о характере php-приложения, которое вы написание И то, как вы видите, как плагины вписываются.
+1 к kdeloach от меня.
Вот подход, который я использовал, это попытка скопировать механизм сигналов/слотов 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-крючки. Таким образом, вы даже можете получить идеи, о которых, возможно, раньше не думали, чтобы сделать вещи немного более прочными.
Более прямым ответом было бы написать общие файлы, которые они будут "включать в себя" в свой файл, что обеспечит удобство использования, которое они будут необходимость. Это было бы разбито на категории и НЕ представлено в одном МАССИВНОМ"hooks.php "файл. Однако будьте осторожны, потому что в конечном итоге происходит то, что файлы, которые они включают, в конечном итоге имеют все больше и больше зависимостей, а функциональность улучшается. Старайтесь поддерживать низкий уровень зависимостей API. Т.Е. меньше файлов для их включения.
Есть аккуратный проект под названием Stickleback Мэтта Зандстры из Yahoo, который выполняет большую часть работы по обработке плагинов в PHP.
Он обеспечивает интерфейс класса плагинов, поддерживает интерфейс командной строки и не слишком сложен для запуска и запуска - особенно если вы прочитали об этом в журнале PHP architect magazine.
Хороший совет - посмотреть, как это сделали другие проекты. Многие требуют установки плагинов и регистрации их "имени" для служб (как это делает wordpress), поэтому в вашем коде есть "точки", где вы вызываете функцию, которая идентифицирует зарегистрированных слушателей и выполняет их. Стандартным шаблоном проектирования OO является шаблон наблюдателя , который был бы хорошим вариантом для реализации в действительно объектно-ориентированной системе PHP.
Zend Framework использует множество подключений методы, и очень красиво спроектирован. Это была бы хорошая система для рассмотрения.
Я удивлен, что большинство ответов здесь, похоже, касаются плагинов, которые являются локальными для веб-приложения, т. Е. плагинов, которые работают на локальном веб-сервере.
Как насчет того, чтобы вы хотели, чтобы плагины запускались на другом удаленном сервере? Лучший способ сделать это - предоставить форму, позволяющую определять различные URL-адреса, которые будут вызываться при возникновении определенных событий в вашем приложении.
Различные события будут отправлять различную информацию на основе событие, которое только что произошло.
Таким образом, вы просто выполните вызов cURL по URL-адресу, который был предоставлен вашему приложению (например, по протоколу https), где удаленные серверы могут выполнять задачи на основе информации, отправленной вашим приложением.
Это дает два преимущества:
- Вам не нужно размещать какой-либо код на своем локальном сервере (безопасность)
- Код может находиться на удаленных серверах (расширяемость) на разных языках, кроме PHP (мобильность)