插件(Plug-in),是指一类特定的功能模块(通常由第三方开发者实现)。它的主要特点是:当我们需要它的时候激活它,不需要它的时候可以禁用/删除它;无论是激活还是禁用都不会影响系统核心模块的运行,也就是说插件是一种非侵入式的模块化设计,实现了核心程序与插件程序之间的松散耦合。
因此,一个优秀的插件机制,应该具备以下特点:
- 动态监听和加载
- 动态触发
- 不影响核心程序的运行
现在市面上流行的开源系统大都支持插件机制。要实现插件机制,我们一般是通过定义一些钩子(Hooks)作为程插件序执行的触发条件,当核心程序执行到某一个钩子的时候,判断钩子条件是否满足,如果满足则执行插件程序,如果不满足直接跳过。我们可以用代码来表述:
function func() {
//TODO: 程序逻辑
if(true) { //在这里判断钩子条件是否满足,条件满足执行,不满足不执行
//TODO: 调用满足条件的插件
}
//TODO: 继续执行程序逻辑
}
所以我们在核心程序里要实现插件机制大概有以下步骤
首先,定义个抽象类(接口),凡是继承(实现)了该抽象类(接口)的类可以识别为插件程序;在抽象类(接口)中定义入口方法,插件程序需要复写(实现)入口方法。
/** * 继承该类的视为插件 */
abstract class WidgetBase{
// 覆写该属性设置为true,代表插件生效
protected $registered = false;
/** * 插件的主入口 */
abstract function main();
}
然后,我们约定好所有的插件程序放置在widgets目录下,并且每个插件对应一个子目录,所有插件程序的类名应该跟文件夹名字一样。

接着,我们需要创建一个Manager的插件程序管理类,用以识别插件、管理插件的运行,Manager程序中,我们需要执行以下步骤:
1、扫描所有插件
2、把满足条件的插件加入队列
3、插件执行
final class WidgetManager
{
// 实例锁
static private $_instance = null;
// 插件容器
private $_widgetListeners = null;
// 让构造函数为 private,这样该类就不会被实例化
private function __construct()
{
// 初始化
$this->_widgetListeners = [];
// 扫描插件,并将插件信息存入插件容器 $this->_widgetListeners 中
$this->scanWidgets();
}
// 获取唯一可用的对象
static public function getInstance()
{
if(!self::$_instance instanceof self) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* 添加钩子方法
*
* @param String $widgetName
* @param String $hookName
* @param Function $callback
* @return void
*/
static public function registerHook($widgetName, $hookName, $callback)
{
self::$_instance->_widgets[$widgetName][$hookName] = $callback;
}
private function scanWidgets()
{
// 获取插件目录
$widgetDir = App::$rootPath . '/widgets';
// 扫描插件目录
foreach(array_diff(\scandir($widgetDir), ['.', '..']) as $wdName) {
$widgetPath = $widgetDir . '/' . $wdName;
if(is_dir($widgetPath)) {
$this->setWidget($wdName, $widgetPath . '/' . $wdName . '.php');
}
}
}
private function setWidget($widgetName, $widgetClassFile)
{
// 缓存插件文件地址
$this->_widgetListeners[$widgetName]['file'] = $widgetClassFile;
}
/**
* 遍历所有插件,所有插件中有 $hookName 钩子的都会被执行
*
* @param String $hookName
* @return void
*/
public function triggerByHook($hookName)
{
foreach($this->_widgets as $widgetName => $widgetConfig) {
$this->trigger($widgetName, $hookName);
}
}
/**
* 执行插件
*
* @param String $widgetName // 插件名称
* @param string $hookName // 钩子名称
* @return void
*/
public function trigger($widgetName, $hookName)
{
$widgetFile = $this->_widgetListeners[$widgetName]['file'];
// 插件文件不存在
if(!is_file($widgetFile)) {
return;
}
// 引入插件文件
include_once $widgetFile;
$widgetRef = new \ReflectionClass($widgetName);
$widget = $widgetRef->newInstance();
// 如果不是 Base 的继承类,说明不是 widget,不执行
if(!$widget instanceof Base) {
return;
}
$registeredProperty = $widgetRef->getProperty('registered');
$registeredProperty->setAccessible(true);
// 未启用插件,不执行
if(!$registeredProperty->getValue($widget)) {
return;
}
// 获取程序入口
$main = $widgetRef->getMethod('main');
// 是否该方法可以被调用,如果不是 public,报错
if(!$main->isPublic()) {
throw new \Exception('插件的入口方法 main() 不存在或者不可访问!');
}
call_user_func_array(array($widget, 'main'), []);
$widgetHookCallback = $this->_widgetListeners[$widgetName][$hookName];
// 如果没有该 filter 方法,不执行
if(!$widgetHookCallback) {
return;
}
// 如果是可执行的方法
if(\is_callable($widgetHookCallback)) {
$widgetHookCallback();
return;
}
// 如果是字符串
\call_user_func_array(array($widget, $widgetHookCallback), []);
}
}
至此,我们的核心程序中的插件机制就完成了,接下来我们可以在程序中添加一些钩子。如下:
WidgetManager::getInstance()->triggerByHook('test');
// test 钩子
然后,我们创建一个 HelloWorld 插件,该插件继承了WidgetBase抽象类,并复写了 main 入口方法,同时,我们在 main 方法中注册 test 钩子。
class HelloWorld extends WidgetBase
{
public function __construct()
{
$this->registered = true;
}
public function main()
{
$this->registerHook('test', '__runTestHook');
}
public function __runTestHook()
{
echo 'run widget HelloWorld';
}
}
最后,我们就可以通过改变 $this->registered 属性来选择打开或者关闭插件而不影响主程序的运行。
除此,在插件机制的实际运用中,我们可以在主程序中定义一些全局变量 $GLOBALS,再通过在插件程序中修改这些全局变量,让程序变得更加弹性。