插件(Plug-in),是指一类特定的功能模块(通常由第三方开发者实现)。它的主要特点是:当我们需要它的时候激活它,不需要它的时候可以禁用/删除它;无论是激活还是禁用都不会影响系统核心模块的运行,也就是说插件是一种非侵入式的模块化设计,实现了核心程序与插件程序之间的松散耦合。

因此,一个优秀的插件机制,应该具备以下特点:

  1. 动态监听和加载 
  2. 动态触发 
  3. 不影响核心程序的运行

现在市面上流行的开源系统大都支持插件机制。要实现插件机制,我们一般是通过定义一些钩子(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,再通过在插件程序中修改这些全局变量,让程序变得更加弹性。