允许PHP应用程序使用插件的最佳方法
我正在用PHP启动一个新的Web应用程序,这次我想创建一些可以通过使用插件界面扩展的东西。
如何将"挂钩"写入其代码,以便插件可以添加到特定事件?
解决方案:
我相信最简单的方法是遵循Jeff的建议并浏览现有代码。尝试查看Wordpress,Drupal,Joomla和其他基于PHP的知名CMS,以了解其API挂钩的外观。这样,我们甚至可以获得以前可能没有想到的想法,使事情变得更加混乱。
一个更直接的答案是将它们会" include_once"的通用文件写入其文件中,从而提供所需的可用性。这将被分为几类,并且不会在一个MASSIVE" hooks.php"文件中提供。但是要小心,因为最终发生的是它们所包含的文件最终具有越来越多的依赖关系,并且功能得到了改善。尽量保持较低的API依赖性。 I.E减少了要包含的文件。
我们可以使用观察者模式。实现此目的的简单功能方法:
<?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文档以获取更多信息。
抱歉,下划线字符被Markdown替换为HTML实体?修复此错误后,我可以重新发布此代码。
编辑:没关系,仅在我们编辑时才显示
钩子和侦听器方法是最常用的方法,但是我们还可以执行其他操作。根据应用程序的大小以及允许谁查看代码(这将是一个FOSS脚本,还是内部的东西),这将极大地影响我们希望允许插件的方式。
kdeloach有一个很好的例子,但是他的实现和钩子函数有些不安全。我想请我们提供有关php应用程序性质的更多信息,以及我们如何看待适合的插件。
+1来自我的kdeloach。
Yahoo的Matt Zandstra提出了一个名为Stickleback的简洁项目,该项目处理着处理PHP插件的大部分工作。
它实现了插件类的接口,支持命令行接口,并且启动和运行起来并不难,特别是如果我们在PHP Architect杂志上阅读了有关它的封面故事。
好的建议是看看其他项目是如何做到的。许多人要求安装插件并为服务注册它们的"名称"(就像wordpress一样),因此我们在代码中有"要点",我们可以在其中调用标识已注册的侦听器并执行它们的函数。一个标准的OO设计模式是观察者模式,它是在真正的面向对象的PHP系统中实现的一个不错的选择。
Zend框架使用了许多挂钩方法,并且架构非常好。那将是一个很好的系统。
这是我使用的一种方法,它是尝试从Qt信号/插槽机制(一种观察者模式)中复制的方法。
对象可以发出信号。
每个信号在系统中都有一个ID,该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部分中,这是我们加载类的地方。注意,我不必为该类做任何特殊的事情,这与Observer模式有显着不同。
在第3部分中,我们将类转换为"可插入"的(即,支持让我们覆盖类方法和属性的插件)。因此,例如,如果我们有一个Web应用程序,则可能有一个插件注册表,我们可以在此处激活插件。还要注意Dog_bark_beforeEvent()
函数。如果我在return语句之前设置$ mixed =''BLOCK_EVENT''
,它将阻止狗吠叫,并且也将阻止Dog_bark_afterEvent,因为不会发生任何事件。
在第4部分中,这是正常的操作代码,但是请注意,我们可能认为将要运行的代码根本不会那样运行。例如,狗没有宣布其名字为" Fido",而是宣布为" Coco"。狗不是说"喵",而是说"哇"。然后,当我们想查看狗的名字时,会发现它是"不同的"而不是"可可"。所有这些替代都在第3部分中提供。
那么这是如何工作的呢?好吧,让我们排除掉" eval()"(每个人都说它是"邪恶的")并排除它不是观察者模式。因此,它的工作方式是一个名为Pluggable的偷偷摸摸的空类,它不包含Dog类使用的方法和属性。因此,既然发生了,魔术方法将为我们服务。这就是为什么在第3部分和第4部分中,我们会处理从Pluggable类派生的对象,而不是Dog类本身。相反,我们让Plugin类为我们对Dog对象进行"触摸"。 (如果那是我不知道的某种设计模式,请告诉我。)