Typecho 本身简洁的设计让其插件机制的实现也十分简洁。在上一篇文章中(Typecho源码分析1),遇到的有些代码与插件相关所以直接跳过了,实际上跳过的部分代码都十分相似。

Typecho 插件调用过程分析

例如入口文件 index.php 中,路由分发前后的两处插件相关代码:

/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();

/** 开始路由分发 */
Typecho_Router::dispatch();

/** 注册一个结束插件 */
Typecho_Plugin::factory('index.php')->end();

很显然插件类所在文件位于 var/Typecho/Plugin.php ,跟进去分析 factory 工厂函数,代码如下:

    /**
     * 获取实例化插件对象
     *
     * @access public
     * @param string $handle 插件
     * @return Typecho_Plugin
     */
    public static function factory($handle)
    {
        return isset(self::$_instances[$handle]) ? self::$_instances[$handle] :
        (self::$_instances[$handle] = new Typecho_Plugin($handle));
    }

可见也是针对具体传入的“句柄”名称做了单例,而这里所谓的“句柄”即 $handle 实际上就是插件挂载点,入口文件挂载点直接就名为 "index.php" ,挂载点往往以插入插件代码的类名或者文件名命名。例如上例就是 "index.php" 挂载点,有两处插件调用点 "index.php:begain" 和 "index.php:end" ,其实跟进去 Typecho_Plugin 类的魔术函数 __call 就能发现调用点命名的由来:

/**
 * 回调处理函数
 *
 * @access public
 * @param string $component 当前组件
 * @param string $args 参数
 * @return mixed
 */
public function __call($component, $args)
{
    $component = $this->_handle . ':' . $component;
    $last = count($args);
    $args[$last] = $last > 0 ? $args[0] : false;

    if (isset(self::$_plugins['handles'][$component])) {
        $args[$last] = NULL;
        $this->_signal = true;
        foreach (self::$_plugins['handles'][$component] as $callback) {
            $args[$last] = call_user_func_array($callback, $args);
        }
    }

    return $args[$last];
}

可见这里时通过用冒号结合挂载点和调用的方法名作为组件名去调用插件所实现的对应的方法,并将插件点传入的参数传入插件对应的方法内进行处理,最后返回结果。这里有个 $args 的小戏法,简单来说这里把 $args[$last] 在思维中预编译成 $result 就行。在多插件挂载同一个地方时,似乎可以脑补出插件方法返回结果的传递作用及可选是否修改、覆盖前面插件返回结果的效果(有种串联电路的感觉),但是着明显过度解读了且没有这个必要,官方插件文档也未作出说明,算是一处瑕疵吧...代码这么写影响了可读性,容易造成误导。

Typecho 插件的加载

到这里,相信已经有整体脉络了。但是还需要分析一下插件是如何加载并将实例的单例保存到 Typecho_Plugin::$_instances 。这就需要回头看一下工厂函数 factory 里面,在实例不存在的时候进行了实例化操作, self::$_instances[$handle] = new Typecho_Plugin($handle) ,跟进去其构造函数:

public function __construct($handle)
{
    /** 初始化变量 */
    $this->_handle = $handle;
}

发现这里只是对插件类的实例初始化了其挂载点名,并不是想要的结果。那么激活的插件类究竟是如何加载到 Typecho_Plugin::$_plugins 的呢?那就得回到上一篇文章提到的 Widget_Init 类的 execute()方法了。其中截取一部分如下:

......

Typecho_Router::setPathInfo($pathInfo);

/** 初始化路由器 */
Typecho_Router::setRoutes($options->routingTable);

/** 初始化插件 */
Typecho_Plugin::init($options->plugins);

/** 初始化回执 */
$this->response->setCharset($options->charset);
$this->response->setContentType($options->contentType);

/** 初始化时区 */
Typecho_Date::setTimezoneOffset($options->timezone);
......

可见很多细致而又必须的组件的初始化操作是在这里进行。其中就有路由和插件的初始化操作,且都是根据用户的设置来的。这里跟进去 Typecho_init 如下:

/**
 * 插件初始化
 *
 * @access public
 * @param array $plugins 插件列表
 * @return void
 */
public static function init(array $plugins)
{
    $plugins['activated'] = array_key_exists('activated', $plugins) ? $plugins['activated'] : array();
    $plugins['handles'] = array_key_exists('handles', $plugins) ? $plugins['handles'] : array();

    /** 初始化变量 */
    self::$_plugins = $plugins;
}

内容其实和想的一样简洁 ---- 根据设置把激活的和所有的插件信息,置入 Typecho_Plugin::$plugins 数组。对于这里的操作,其实看一下数据库里面的设置会更加清晰(这里是安装并且激活了两个插件 ---- HelloWord 插件 和 ViewsCounter):

a:2:{s:9:
    "activated";a:2:
        {s:10:"HelloWorld";a:1:
            {s:7:"handles";a:1:
                {s:21:"admin/menu.php:navBar";a:1:
                    {i:0;a:2:
                        {i:0;s:17:"HelloWorld_Plugin";i:1;s:6:"render";}
                    }
                }
            }
        s:5:"ViewsCounter";a:1:
            {s:7:"handles";a:1:
                {s:27:"Widget_Archive:beforeRender";a:1:
                    {i:0;a:2:
                        {i:0;s:12:"ViewsCounter_Plugin";i:1;s:12:"count";}
                    }
                }
            }
        }
    s:7:"handles";a:2:
        {s:21:
        "admin/menu.php:navBar";a:1:
            {i:0;a:2:
                {i:0;s:17:"HelloWorld_Plugin";i:1;s:6:"render";}
            }s:27:
        "Widget_Archive:beforeRender";a:1:
            {i:0;a:2:
                {i:0;s:12:"ViewsCounter_Plugin";i:1;s:12:"count";}
            }
        }
}

Typecho_ViewsCounter 插件流程分析

下面以一个具体的插件的加载流程进行分析,以其中一小部分配置为例:

        s:5:"ViewsCounter";a:1:
            {s:7:"handles";a:1:
                {s:27:"Widget_Archive:beforeRender";a:1:
                    {i:0;a:2:
                        {i:0;s:12:"ViewsCounter_Plugin";i:1;s:12:"count";}

可见挂载的点是 Widget_Archive:beforeRender ,显然这是一个类名挂载点,那么按照之前说的命名规则进去 var/Widget/Archive.php ,搜索 "beforeRender" 可见:

...
...

/** 文件不存在 */
if (!$validated) {
    Typecho_Common::error(500);
}

/** 挂接插件 */
$this->pluginHandle()->beforeRender($this);

/** 输出模板 */
require_once $this->_themeDir . $this->_themeFile;

/** 挂接插件 */
$this->pluginHandle()->afterRender($this);

很明显挂载在这个点的插件将会在每次载入模板文件之前运行。但是这里的挂载并不是用 factory 函数,有什么区别呢?跟进去 puginHandle() 函数可见:

/**
 * 获取对象插件句柄
 *
 * @access public
 * @param string $handle 句柄
 * @return Typecho_Plugin
 */
public function pluginHandle($handle = NULL)
{
    return Typecho_Plugin::factory(empty($handle) ? get_class($this) : $handle);
}

可能类名挂载点用的也很多,所以组件继承的基类 Typecho_Widget 直接封装了一个函数,就不用每次采用裸调用 Typecho_Plugin::factory(get_class($this)) 的方式了。根据配置,这里将会调用的是 ViewsCounter_Plugin 类的 count 方法,并且此时的 Widget_Archive 实例将作为参数传入这个方法,方法里面根据需要进行操作即可对 Typecho 原有的功能进行扩展。

Typecho 插件分析总结

总的来说,在 Typecho 中就是通过 Typecho_Plugin 类来存储插件类单例、配置信息,然后在预留扩展的地方调用 Typecho_Plugin 类的方法进行埋(挂载)点,然后通过 __call() 魔术方法的配合对对应的插件方法进行调用,并且每个点可以映射调用多个插件的方法(依次调用),同时可以通过参数的形式在关键地方的将需要的信息暴露给插件以提升插件的灵活性,也便于通过插件的形式提供更多的功能。