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

我正在用PHP开始一个新的Web应用程序,这次我想创build一些人们可以使用插件接口来扩展的东西。

一个人如何去编写'钩子'到他们的代码,以便插件可以附加到特定的事件?

你可以使用观察者模式。 一个简单的function方法来实现这一点:

<?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 

笔记:

对于这个示例源代码,您必须在您想要扩展的实际源代码之前声明所有的插件。 我已经包含了一个如何处理传递给插件的单个或多个值的例子。 其中最难的部分是编写实际的文档,列出哪些parameter passing给每个钩子。

这只是在PHP中完成插件系统的一种方法。 有更好的select,我build议你看看WordPress的文档了解更多信息。

抱歉,下划线字符被Markdownreplace为HTML实体? 当这个错误得到解决时,我可以重新发布这个代码。

编辑:没关系,只有在编辑时才会出现这种情况

所以我们假设你不需要Observer模式,因为它需要你改变你的类方法来处理监听的任务,并且需要一些通用的东西。 假设您不想使用extendsinheritance,因为您可能已经在其他类中inheritance了您的类。 有一个通用的方法,使任何类可插拔没有太多的努力是不是很好 ? 就是这样:

 <?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应用程序,你可能有一个插件registry,你可以在这里激活插件。 还要注意Dog_bark_beforeEvent()函数。 如果我在返回语句之前设置$mixed = 'BLOCK_EVENT' ,它将阻止狗叫,也会阻止Dog_bark_afterEvent,因为不会有任何事件发生。

在第4部分中,这是正常的操作代码,但请注意,您可能认为运行的代码根本就不会运行。 例如,狗没有宣布它的名字是'Fido',而是'Coco'。 狗不会说'喵',而是'Woof'。 而当你想看看狗的名字后,你会发现它是'不同'而不是'cocoa'。 所有这些重写都在第3部分中提供。

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

听众方法是最常用的,但还有其他的事情可以做。 根据您的应用程序的大小,以及您将允许查看代码的人(这是否将成为FOSS脚本或内部的东西)将极大地影响您想要允许插件的方式。

kdeloach有一个很好的例子,但是他的实现和钩子函数有点不安全。 我会要求你提供更多关于php应用程序性质的信息,以及你如何看待插件的安装。

从我的kdeloach +1。

这是我用过的一种方法,它是从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(); ?> 

我相信最简单的方法就是遵循Jeff的build议并查看现有的代码。 试试看Wordpress,Drupal,Joomla和其他众所周知的基于PHP的CMS,看看他们的API挂钩的外观和感觉。 这样,你甚至可以得到你以前可能没有想到的想法,使事情变得更加鲁棒。

更直接的答案是写一般文件,他们将“include_once”到他们的文件,将提供他们需要的可用性。 这将被分解成类别,而不是在一个MASSIVE“hooks.php”文件中提供。 但要小心,因为最终发生的事情是它们包含的文件最终会有越来越多的依赖性和function性提高。 尽量保持API依赖性低。 IE包含更less的文件。

雅虎的Matt Zandstra提出了一个名为Stickleback的整洁项目,它负责处理PHP中插件的大部分工作。

它强制插件类的接口,支持命令行界面,并不难以启动和运行 – 尤其是如果您阅读了PHP架构师杂志中关于它的封面故事。

好的build议是看看其他项目如何做到这一点。 许多人呼吁安装插件和他们的“名称”注册的服务(如wordpress的),所以你有你的代码中的“点”,你调用一个函数,标识注册监听器并执行它们。 一个标准的OOdevise模式是Observer模式 ,这是一个在真正的面向对象的PHP系统中实现的好select。

Zend Framework使用了许多钩子方法,并且构build得非常好。 这将是一个很好的系统来看待。

我感到惊讶的是,这里的大多数答案似乎都是针对Web应用程序本地插件的,即在本地Web服务器上运行的插件。

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

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

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

这提供了两个好处:

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