FV.Zone Project Code

Dissect WordPress Plugin

一直对软件的组件、插件架构非常感兴趣,我认为现在开发任何应用程序,一定要有一个架构良好的插件机制,这样可以吸引其他人来开发插件,极大的扩充系统的功能。Eclipse就是一个非常棒的例子,几乎你想什么功能,都可以从社区中找到相应的
第三方插件。Firefox也同样如此,它的插件机制非常灵活(尤其是相对IE而言),因此Firefox社区中可以找到各种各样的插件。

就像Nat在Open Source Summit中的一句话:

You have to give people work to do.
Create an “architecture of participation” as Tim O’Reilly says.

自从开始使用WordPress,就想学习学习WordPress强大的插件机制。以前曾经研究过的COM、XPCOM、Firefox Extensions、Eclipse Plugin都是桌面应用级别的组件结构,而对Web应用中的插件确实知之甚少,于是,Dissect了一下WordPress(1.5.1.3)。虽然对PHP不是很熟悉,不过看看源码还是问题不大。

1)WordPress读取所有可用的插件

在文件“admin-functions.php”中,函数

<?php function get_plugins() ?> ?>

用来从文件系统得到所有的插件。原理很简单,就是读取’wp-content/plugins’目录下的所有PHP文件。这个函数允许一级的子文件夹,也就是说在’wp-content/plugins’下面的PHP文件,以及所以在此目录下的一级子文件夹内部的PHP文件被列作插件的候选,用下面的函数去进一步提取插件信息。这样的好处是方便用户利用文件夹来对插件进行管理和组织。

而函数

<?php function get_plugin_data() ?>

用来得到插件的描述(Plugin Descriptor),主要包括插件的版本、名称、作者,等信息,而这些其实是以注释的方式存在的。用WordPress中自带的Hello插件来举例:

 <?php  
Plugin Name: Hello Dolly 
Plugin URI: http://wordpress.org/# 
Description: This is not just a plugin, it symbolizes the hope and enthusiasm of an entire ... 
Author: Matt Mullenweg 
Version: 1.0 
Author URI: http://photomatt.net/ 
 ?>

这样,在get_plugin_data函数中,就可以来得到插件的详细信息。

<?php  
function get_plugin_data($plugin_file) { 
    $plugin_data = implode('', file($plugin_file)); 
    preg_match("|Plugin Name:(.*)|i", $plugin_data, $plugin_name); 
    preg_match("|Plugin URI:(.*)|i", $plugin_data, $plugin_uri); 
    preg_match("|Description:(.*)|i", $plugin_data, $description); 
    preg_match("|Author:(.*)|i", $plugin_data, $author_name); 
    preg_match("|Author URI:(.*)|i", $plugin_data, $author_uri); 
 ?>

2)Active & Deactive Plugin

Active(Deactive)插件的操作都在Plugins.php中,比如我要Deactive “Hello”这个插件,最后的URL其实是这个样子:

<?php http://localhost/blog/wp-admin/plugins.php?action=deactivate&amp;plugin=hello.php ?>

其中,“Action”表示动作,值为“active”或者“deactivate”,而“Plugin”表示动作的对象插件,此处为“hello.php”。得到动作指令后,首先从数据库中取出当前已经激活的插件。

<?php $current = get_settings('active_plugins'); ?>

然后根据动作,重新生成已激活插件数组,存入数据库,并重新加载此页。加载的时候就需要考虑这些已经激活的插件是怎么工作的了。

BTW:附上数据库的Options表中0插件和只有1个插件的值:

没有插件:’a:1:{i:0;s:0:””;}’

只有Hello插件:’a:2:{i:0;s:0:””;i:1;s:9:”hello.php”;}’

3)插件(Active)如何加载到系统中

WordPress中的每页都会包含“wp-config.php”文件,而在“wp-config.php”的最后有这样一句:

<?php require_once(ABSPATH.'wp-settings.php'); ?>

在“wp-settings.php”文件中,可以找到以下与插件相关的代码片断:

 <?php  
if ( get_settings('active_plugins') ) { 
    $current_plugins = get_settings('active_plugins'); 
    if ( is_array($current_plugins) ) { 
        foreach ($current_plugins as $plugin) { 
            if ('' != $plugin && file_exists(ABSPATH . 'wp-content/plugins/' . $plugin)) 
                include_once(ABSPATH . 'wp-content/plugins/' . $plugin); 
        } 
    } 
} 
 ?>

可见,这段代码会取出系统中所有Active的插件,并Include进来。所以在每页加载的时候,都会首先Include这些插件代码。那么,这些插件自己在加载的时候都做了什么呢?

4)插件的加载

插件的加载其实最重要的一个部分就是插件的事件注册机制,WordPress插件中的事件注册其实和Eclipse中的扩展点(Extension-Point)机制非常相像,而这种类似“插销”、“插销座”的软件插拔方式也成为了最近软件组件架构方面应用最多的实践。

事件注册过程中比较重要的几个函数分别是:do_action、add_action、add_filter。

WordPress中默认定义了很多扩展点(也可以叫做“钩子”),或者说注册了很多系统事件(WP中的正规叫法应该是“Action Tag”),比如“admin_head”表示Admin页面的Head输出事件,“publish_post”表示发布一篇帖子的事件等等。而插件要做的就是扩展这些扩展点,或者说挂接这些钩子,从而实现系统的扩展功能。add_action就是通常插件扩展某个扩展点用到的函数,而do_action是扩展点本身开始执行的函数。

刚才说过WordPress中的每一页执行前都会Include所有Active的插件代码,而这些代码通常都会用“add_action”来将自己的函数注册到系统的扩展点中。这样,在扩展点执行的时候,就会找到系统中所有已经挂接到这个扩展点上的插件的函数来执行之,从而扩充系统的功能。

WordPress中的很多功能也都是通过这种插件结构来实现的,默认注册了很多系统事件,都在’default-filter.php’中。比如:

<?php add_action('publish_post', 'generic_ping'); ?>

这个是用来在发布每篇帖子的时候发送XML-RPC Ping的。再比如:

<?php add_filter('the_content', 'convert_smilies'); ?>

用来将正文(content)中的笑脸符号转换为图像。

还是举“Hello”插件来说。Hello插件会随机的在Admin Page的右上角显示一段话,它的工作原理是这样的:

在每一个Admin page的前面都有

<?php require_once('admin-header.php'); ?>

而在“admin-head.php”中将会执行扩展点“admin_head”的所有扩展:

 <?php  
do_action('admin_head', ''); 
 ?>

这样,就会执行所有挂接到admin_head的函数

Admin Page的Footer部分同样如此,

 <?php  
do_action('admin_footer', ''); 
 ?>

这样,就会执行所有挂接到admin_footer的函数

再来看看Hello插件的初始化过程中:

<?php  
  
// This just echoes the chosen line, we'll position it later 
function hello_dolly() { 
    global $chosen; 
    echo "<p id='dolly'>$chosen</p>"; 
} 
  
// Now we set that function up to execute when the admin_footer action is called 
add_action('admin_footer', 'hello_dolly'); 
  
// We need some CSS to position the paragraph 
function dolly_css() { 
    echo " 
    <style type='text/css'> 
    #dolly { 
        position: absolute; 
        top: 5px; 
margin: 0; padding: 0; 
        right: 3em; 
        font-size: 20px; 
        color: #f66; 
    } 
    </style> 
    "; 
} 
  
add_action('admin_head', 'dolly_css'); 
  
?>

可见,将dolly的CSS部分插到Header部分,而在footer部分插入显示代码,这样,在AdminPage的右上角就会随机显示一段话。

再来看看曾经说过的Google Sitemap插件,插件中有一项功能是在发布、修改或者删除文章的时候重新Build Sitemap文件,这个功能就是通过注册系统的三个事件(“publish_post”,“edit_post”,“delete_post”)来完成的。

<?php  
//Register to various events... @WordPress Dev Team: I wish me a 'public_content_changed' action 🙂 
if(defined("SM_ACTIVE") && SM_ACTIVE===true) { 
    //If a new post gets published 
    add_action('publish_post', 'sm_buildSitemap'); 
  
    //Existing post gets edited (published or not) 
    add_action('edit_post', 'sm_buildSitemap'); 
  
    //Existing posts gets deleted (published or not) 
    add_action('delete_post', 'sm_buildSitemap'); 
} 
#endregion 
 ?>

BTW:其中的注释也很有意思,我也觉得确实需要有“public_content_changed”这样一个事件,就不必分别订阅三个单独的事件了。

对了,就在我们刚才说的wp-settings.php中的插件启动后,有

<?php do_action('plugins_loaded'); ?>

这样一个系统事件,可以注册这个事件来做一些希望在所有插件Load完毕而做的事儿。

5)如果插件中涉及UI

其实是一样的。以WordPress FeedBurner Plugin中添加菜单为例:

如果想添加一个菜单,就需要注册“admin_menu”这个Action Tag(系统事件):

<?php add_action('admin_menu', 'ol_add_feedburner_options_page');  ?>

即可,插件中的这个函数为:

 <?php function ol_add_feedburner_options_page() { 
  if (function_exists('add_options_page')) { 
      add_options_page('FeedBurner', 'FeedBurner', 8, basename(__FILE__), 'ol_feedburner_options_subpanel'); 
  }<br>} 
 ?>

“add_options_page”这个函数就会在系统的“Options”菜单中添加“FeedBurner”这样一个子菜单。

而这个函数其实就是增加 Menu 或者 SubMenu,

<?php  
function add_submenu_page($parent, $page_title, $menu_title, $access_level, $file, $function = '') { 
    global $submenu; 
    global $menu; 
  
    $parent = plugin_basename($parent); 
    $file = plugin_basename($file); 
  
    // If the parent doesn't already have a submenu, add a link to the parent 
    // as the first item in the submenu.  If the submenu file is the same as the 
    // parent file someone is trying to link back to the parent manually.  In 
    // this case, don't automatically add a link back to avoid duplication. 
    if (! isset($submenu[$parent]) && $file != $parent) { 
        foreach ($menu as $parent_menu) { 
            if ($parent_menu[2] == $parent) { 
                $submenu[$parent][] = $parent_menu; 
            } 
        } 
    } 
    
    $submenu[$parent][] = array($menu_title, $access_level, $file, $page_title); 
  
    $hookname = get_plugin_page_hookname($file, $parent); 
    if ( !empty($function) && !empty($hookname) ) 
        add_action($hookname, $function); 
  
    return $hookname; 
} 
  
function add_options_page($page_title, $menu_title, $access_level, $file, $function = '') { 
    return add_submenu_page('options-general.php', $page_title, $menu_title, $access_level, $file, $function); 
} 
 ?>

6)其它

还有一些简单的插件就是只提供一些API函数。比如Most_Commented Plugin,它提供一个API “mdv_most_commented”:通过数据库查询得到评论最多的文章,并加以显示。因为这个插件已经被Include过,所以可以用这个API来进行显示。

历史:

2005.07.14 – 创建

2005.07.16 – 新增get_plugins的子文件夹解析部分

Leave a Reply

You must be logged in to post a comment.