允许PHP应用程序使用插件的最佳方法

我开始一个新的web应用程序在PHP和这一次,我想创建一些东西,人们可以通过使用插件接口扩展。

如何在他们的代码中编写“钩子”,以便插件可以附加到特定的事件?

40128 次浏览

我相信最简单的方法是遵循Jeff自己的建议,查看一下现有的代码。试着看看WordPress、Drupal、Joomla和其他知名的基于php的CMS,看看它们的API挂钩的外观和感觉如何。通过这种方式,您甚至可以获得您以前可能没有想到的想法,从而使事情变得更加健壮。

一个更直接的答案将是编写通用文件,他们将&;include_once&;放入他们的文件中,以提供他们所需的可用性。这将被分成不同的类别,而不是在一个庞大的“钩子”中提供。文件。但是要小心,因为最终发生的情况是,它们包含的文件最终具有越来越多的依赖关系,功能也得到了改进。尽量保持较低的API依赖性。也就是要包含更少的文件。

您可以使用观察者模式。一个简单的功能方法来完成这个:

<?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文档以获得更多信息。

侦听器方法是最常用的方法,但你还可以做其他事情。取决于你的应用程序的大小,以及你允许谁看到代码(这将是一个自由/开源软件脚本,还是内部的东西)将极大地影响你想要如何允许插件。

Kdeloach有一个很好的例子,但是他的实现和钩子函数有点不安全。我想请你给更多的信息,php应用程序的性质,你写的,以及你如何看待插件适合。

+1给我。

Yahoo的Matt Zandstra有一个名为棘鱼的整洁项目,它处理了PHP中处理插件的大部分工作。

它加强了一个插件类的接口,支持命令行接口,并且安装和运行起来并不太难——特别是如果你在PHP架构师杂志中阅读了关于它的封面故事。

好的建议是看看其他项目是如何做到这一点的。许多人要求安装插件,并为服务注册插件的“名称”(就像wordpress那样),这样在代码中就有了“点”,可以调用一个函数来标识已注册的侦听器并执行它们。一个标准的OO设计模式是观察者模式,这将是在真正面向对象的PHP系统中实现的一个很好的选择。

Zend框架使用了许多钩子方法,并且架构非常好。这是一个很好的系统。

这是我使用的一种方法,它试图从Qt信号/槽机制复制,一种观察者模式。 物体可以发射信号。 每个信号在系统中都有一个ID——它由发送者的ID +对象名称组成 每个信号都可以绑定到接收器,这就是一个“可调用的”。 您使用总线类将信号传递给任何有兴趣接收它们的人 当某事发生时,你“发送”一个信号。 下面是一个示例实现

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


?>

因此,假设您不想要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部分中,这就是在PHP脚本顶部的require_once()调用中可能包含的内容。它加载类以使某些东西可插入。

在第2部分中,我们将加载一个类。注意,我不需要对类做任何特殊的操作,这与观察者模式有很大不同。

在第3部分中,我们将类转换为“可插入的”(也就是说,支持让我们重写类方法和属性的插件)。例如,如果你有一个web应用,你可能有一个插件注册表,你可以在这里激活插件。还要注意Dog_bark_beforeEvent()函数。如果我在return语句之前设置$mixed = 'BLOCK_EVENT',它会阻止狗叫,也会阻止Dog_bark_afterEvent,因为不会有任何事件。

在第4部分中,这是正常的操作代码,但请注意,您可能认为会运行的代码根本不是那样运行的。例如,这只狗不会说它的名字是“菲多”,而是“可可”。这只狗不说“喵”,只说“汪”。当你事后想看狗狗的名字时,你会发现它是“Different”而不是“Coco”。第3部分提供了所有这些覆盖。

那么这是如何工作的呢?好吧,让我们排除eval()(每个人都说它是“邪恶的”),并排除它不是观察者模式。因此,它的工作方式是一个叫做Pluggable的狡猾的空类,它不包含Dog类使用的方法和属性。因此,既然发生了这种情况,魔法方法就会为我们所用。这就是为什么在第3部分和第4部分中,我们混淆了从Pluggable类派生的对象,而不是Dog类本身。相反,我们让Plugin类为我们对Dog对象进行“触摸”。(如果这是某种我不知道的设计模式,请告诉我。)

我很惊讶,这里的大多数答案似乎都是关于web应用程序的本地插件,即运行在本地web服务器上的插件。

如果你想让插件在另一个远程服务器上运行呢?最好的方法是提供一个表单,允许您定义不同的url,当应用程序中发生特定事件时将调用这些url。

不同的事件会根据刚刚发生的事件发送不同的信息。

这样,您只需对提供给应用程序的URL(例如通过https)执行cURL调用,远程服务器可以根据应用程序发送的信息执行任务。

这提供了两个好处:

  1. 您不必在本地服务器上托管任何代码(安全性)
  2. 代码可以在远程服务器上(可扩展性),使用PHP以外的不同语言(可移植性)