其他
WordPress的可拓展性初探
WordPress是一个时下非常流行的网络信息发布平台,它的特性之一便是极强的可拓展性。然而在这样一个工程中,它的可拓展性是从何而来的呢?下面的篇幅尝试从两个方面简单介绍WordPress的可拓展性。这两个方面不仅可以帮助我们编写WordPress的插件,同样可以帮助我们设计具有可拓展性的架构。
1. 数据库
在原版WordPress中,每一篇文章有哪些元信息(meta-data)是已经定好了的,例如一篇文章会有“作者”、“标题”、“发布时间”、“文章内容”等。这些元信息会直接体现在数据表的结构(schema)上。例如,存放文章的表中,就会有诸如“post_author”、“post_title”、“post_date”、“post_content”等列。
但是作为插件来说,我们不可避免地要为文章添加元信息。例如发布优惠券的插件需要记录优惠码、需要记录过期时间,地图插件需要记录经纬度坐标等等。元信息是灵活的,可是数据表的结构一旦定下就很难修改。那么我们怎样做才可以实现灵活的数据表结构呢?
我们可以尝试使用行列转换的思路,把原来表中的行转换成列,把原来表中的列转换成行。
在WordPress中有一个表专门用于存储文章的元信息,名称叫做 wp_postmeta 。它只有四列,分别为 meta_id 、 post_id 、 meta_key 、 meta_value 。其中 meta_id 只是一行记录唯一的ID, post_id 表示该记录属于哪一篇文章, meta_key 为元信息的名称, meta_value 为元信息的值。
下面我们用记录地理位置信息中的经纬度举一个例子。我们需要给文章存储 latitude 和 longitude 这两个信息,首先我们需要知道,我们文章的 post_id 是多少,这是可以从 wp_posts 数据表中获取的。接下来,我们要向 wp_postmeta 中添加两条记录,分别存储精度和纬度。下面以添加纬度为例(添加经度的方法类似):
INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (<实际的文章ID>, "latitude", <实际的纬度>);
我们可以从中发现,我们并没有对数据表的结构进行任何改动,而只是向一个表中添加了信息,就达成了扩展元信息类型的目的。
需要读取元信息的思路也很简单,只要通过 post_id 在 wp_postmeta 中找到相应的记录,并且再次根据 meta_key 进行筛选就可以了。下面举例获取纬度:
SELECT meta_value FROM wp_postmeta WHERE post_id=<实际的文章ID> and meta_key="latitude";
如果要同时获取多个信息,这样写比较紧凑:
SELECT
MAX(CASE WHEN pm.meta_key='latitude' THEN pm.meta_value END) AS latitude,
MAX(CASE WHEN pm.meta_key='longitude' THEN pm.meta_value END) AS longitude
FROM
wp_posts as p,
wp_postmeta as pm
WHERE
p.ID=pm.post_id AND
p.ID=<实际的文章ID>
GROUP BY p.ID;
虽然这样的设计可以极大地提高数据库的可拓展性,但是同样它也带来了一些问题:
首当其冲的是效率问题,因为这样在存储一篇文章时,就不可避免地要向两个表中添加信息,查询时也要牵扯到两个表的结合,会拖慢数据库的执行效率。
其次是类型检查,一般情况下,表中每一列都有其数据类型,在向表中插入数据时,SQL会依据数据类型对其进行检查,如果采用上面的方式,那么 meta_value 只能为字符串类型,这样从某种程度降低了数据的可靠程度。
最后是数据库结构检查,一般情况下,我们可以利用 NOT NULL 来规定某一列必须有一个值,而使用上面提到的方式,就必须由Web应用程序来进行这样的检查了。
综上所述,利用这样的方式,我们确实可以提高数据库的可拓展性,但是可拓展性也不可避免地带来一些小问题。所以我们需要根据工程的具体需求,灵活地应用这种方式。
下面的篇幅将介绍在PHP的代码方面,如何设计才能达到可拓展性。
2. 钩子(hook)方法
在给一个系统书写插件的时候,我们往往需要在原来工程的某个特定位置加入一段代码。如果原来的工程没有任何特殊的设计,那么我们的做法肯定是找出原来工程的相关代码,然后在里面插入我们想要实现的功能,但是这样就毫无扩展性可言了。例如,原工程如果有升级,那么我们拿到了原工程的代码之后,又要找到原来的地方,又要重新插入一遍自己想要实现的功能,费时费力。
在WordPress中,为了避免这样的问题,引入了一个钩子方法的概念。所谓“钩子”,其实可以理解为代码的插入点。这也就是说,如果原工程在设计时加入了钩子方法的调用,我们便可以在调用钩子方法的地方插入代码。WordPress在设计的时候考虑得相当周到,可以说插件开发者需要插入代码的地方,都已经有了钩子方法了。
钩子方法的本质其实就是是动态调用函数。每一个钩子方法都有特定的名称以及定义,例如 save_post ,表示当一篇文章被保存时需要执行的代码,WordPress在调用这个方法时,会给我们传入被保存的文章ID。
如果我需要在文章被保存的时候执行一些特殊功能,比如说给某个指定的邮箱发邮件。那么我首先需要在插件中实现发邮件的功能,并且在特定的钩子方法中注册我的函数。例如:
function sent_email_to_myself($post_id) {
// 这里略过实现发送Email的功能……
}
add_action('save_post', 'send_email_to_myself', 10, 1);
add_action 是由WordPress定义的函数。其中前两个参数,第一个为指定需要插入代码的位置,第二个为指定要插入那个函数。最后的两个参数一个表明执行的优先级,一个表明接受的参数数量,与下文内容并无太大关系,故此处略过。
当我们打开一个WordPress页面时,WordPress会遍历插件列表,执行每一个插件的入口PHP文件。这样上述代码就会被执行,我们的插件就利用 add_action 函数的调用,把发送邮件的功能注册在了 save_post 这个钩子方法上。到了保存文章的时候,WordPress会调用 save_post 这个钩子方法——它首先查找钩子方法的注册列表,找出有哪些函数注册在了 save_post 上,然后根据优先级顺序依次调用执行。这样就达到了在特定的位置执行插件定义的代码的目的。
这样做的好处是,无论WordPress如何升级,只要钩子方法的定义没有改变,那么原有插件就可以不加修改正常工作。这样就相当于降低了原工程与插件的耦合度。
然而有利必有弊,这样做的代码执行速度肯定是没有直接调用函数来得快。不过对于WordPress这样的强调高可扩展性的工程,使用这样的技巧显然是利大于弊的。我们亦可以在我们的工程中借鉴这样的思路,从而使我们的工程也具有高可扩展性。
参考资料:
https://developer.wordpress.org/reference/functions/add_action/
https://codex.wordpress.org/Plugin_API/Action_Reference/save_post
作者:西瓜玩偶(racnil070512 at hotmail dot com)