Я запускаю нову веб-програму в PHP, і на цей раз я хочу створити щось, що люди можуть розширити за допомогою інтерфейсу плагінів.
Як можна писати «гачки» у свій код, щоб плагіни могли приєднуватися до конкретних подій?
Я запускаю нову веб-програму в 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 для отримання додаткової інформації.
Mediator Pattern
. Справжні спостерігачі - це суто сповіщення, немає повідомлення про передачу та умовне повідомлення (також немає центрального менеджера для контролю над повідомленнями). Це не робить неправильну відповідь , але слід зазначити, що люди перестають називати речі невірним іменем ...
Тож скажімо, що ви не хочете шаблону 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'
перед заявою повернення, вона заблокує собаку від гавкання, а також заблокує Dog_bark_afterEvent, оскільки не було б жодної події.
У частині 4 це нормальний код роботи, але зауважте, що те, що ви можете подумати, що працює, зовсім не працює. Наприклад, собака не оголошує її ім'ям як "Fido", а "Coco". Собака каже не «мяу», а «чудо». А потім, коли ви хочете подивитися на ім'я собаки, то виявите, що це "Різне", а не "Коко". Усі ці скасування були викладені в частині 3.
То як це працює? Що ж, давайте виключаємо eval()
(що всі кажуть, що "зло") і виключаємо, що це не зразок спостерігача. Отже, спосіб його роботи - це підлий порожній клас під назвою Pluggable, який не містить методів та властивостей, використовуваних класом Dog. Таким чином, оскільки це відбувається, магічні методи будуть залучати нас. Ось чому в частинах 3 та 4 ми псуємось із об’єктом, похідним від класу Pluggable, а не самим класом Dog. Натомість, ми дозволяємо класу Plugin робити «торкання» об’єкта 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 та інші добре відомі на основі PHP CMS, щоб побачити, як виглядають і відчуваються їх гачки API. Таким чином ви навіть можете отримати ідеї, про які ви, можливо, не думали раніше, щоб зробити речі трохи більш жорсткими.
Більш прямою відповіддю було б написати загальні файли, які вони "включать_once" у свій файл, що забезпечить зручність, яка їм потрібна. Це було б розбито на категорії і НЕ надано в одному МАСИВНОМУ файлі "hooks.php". Але будьте обережні, адже те, що в підсумку відбувається, - це те, що файли, які вони включають, виявляють все більше залежностей, а функціональність покращується. Намагайтеся підтримувати низькі залежності API. IE менше файлів для їх включення.
У Yahoo є акуратний проект під назвою Stickleback від Matt Zandstra, який займається великою частиною роботи з обробки плагінів у PHP.
Він застосовує інтерфейс класу плагінів, підтримує інтерфейс командного рядка і не надто важко вставати і працювати, особливо якщо ви читаєте обкладинку про нього в журналі архітектора PHP .
Гарна порада - подивитися, як це зробили інші проекти. Багато хто закликає встановити плагіни та їх "ім'я" зареєструватись для служб (як Wordpress робить), тож у вашому коді є "точки", де ви викликаєте функцію, яка ідентифікує зареєстрованих слухачів та виконує їх. Стандартний шаблон дизайну OO - це шаблон спостерігача , який був би хорошим варіантом для втілення в справді об'єктно-орієнтованій системі PHP.
Zend Framework використовує безліч методів Крюкова, і дуже добре спроектований. Це була б гарна система.
Я здивований, що більшість відповідей тут, здається, орієнтовані на плагіни, локальні для веб-програми, тобто плагіни, які працюють на локальному веб-сервері.
А як бути, якщо ви хочете, щоб плагіни запускалися на іншому - віддаленому сервері? Найкращим способом зробити це було б надати форму, яка дозволяє визначати різні URL-адреси, які б викликалися, коли у вашій програмі відбуваються конкретні події.
Різні події надсилають різну інформацію залежно від події, яка щойно відбулася.
Таким чином, ви б просто здійснили виклик CURL на URL-адресу, надану вашій програмі (наприклад, через https), де віддалені сервери можуть виконувати завдання на основі інформації, надісланої вашою заявкою.
Це забезпечує дві переваги: