WordPress 使用 wpdb 类创建的全局对象 $wpdb 实现对底层 MySQL 数据库的 DML 操作。如果你想将 WordPress 用作涉及数据库交互的开发框架,那么面临的第一个问题是 $wpdb 不支持事务,没有事务的支持,就无法保证业务的一致性。对于事务的概念这里就不做介绍了,如果你找到这篇文章,那你自然是理解的。

本插件是 wpdb 类的子类,以 wpdb 为基础实现了对事务的支持,做法是在 wpdb 类上扩展出一个子类 conn,在子类 conn 上实现对事务的支持(不受影响 wpdb 的实例 $wpdb)。为方便使用已做成插件,项目地址在这里:https://github.com/yusn/wp_conn下载地址在这里

使用方法

插件已定义了全局对象 $conn,在您的程序里只需要加入global $conn;即可通过 $conn 调用那些从 wpdb 类继承来的方法,以及 $conn 自有的一些方法。换句话说在需要时你完全可以用 $conn 来替代 $wpdb。

特性介绍

这是一个试验性的项目,仅供交流。

  • 仅支持 REST API;
  • 自动开启事务;
  • 异常自动回滚;
  • 运行结束自动提交(返回 REST API 请求时);
  • 支持多行插入;
  • 支持自动提交模式;
  • 支持手动提交;
  • 支持手动回滚;

仅支持 REST API

之所以仅仅支持 REST API,是因为开启事务/自动提交/自动回滚的实现是在 REST API 提供的 filter 上实现的(与 $wpdb 不同,$conn 默认运行在手动提交模式下)。

自动开启事务

接收到 REST API 请求时自动开启事务,无需手动开启。

运行结束自动提交

API 程序运行结束,如未有异常发生,响应返回前将自动提交事务。

异常自动回滚

当 API 程序抛出异常,或返回 WP_ERROR 类实例时,将自动执行事务回滚。
若在程序抛出异常前执行了手动提交 $conn->commit();,则提交之前的 DML 操作不会被回滚;若异常发生在手动开启事务之后,或执行$conn->commit();之后返回 WP_ERROR,则回滚仅限于手动开启事务之后的部分。

支持自动提交模式

也可以像 $wpdb 那样,让你的代码运行在自提提交的模式下,只需要在程序中执行执行数据库操作前执行$conn->set_autocommit();即开启自动提交模式,此后若再发生异常或错误,则无法回滚。

支持手动提交

默认情况下(未开启自动提交)在程序执行过程中,你可以在需要时随时手动提交之前的操作,只需要执行$conn->commit();方法。执行$conn->commit();将自动开启一个新的事务,这会让你之后的代码运行在一个新事务里,这个事务在正常响应返回前会自动被提交,若后续代码发生了异常或返回 WP_ERROR 类的实例,则这部分代码会被回滚。
开启自动提交后($conn->set_autocommit();),若再执行手动提交($conn->commit();),则手动提交不会生效(也不会产生新事务),因为当前已经运行在自动提交模式下,无需手动提交。

支持手动回滚

在一个事务里,你可以随时执行$conn->rollback();方法以回滚之前的操作;若在此之前已开启了自动提交模式($conn->set_autocommit();),则回滚操作不会生效。

支持多行插入

$wpdb 的插入方法只支持单行插入,使用$conn->insert_rows();方法能够实现一次插入多行,该方法的返回结果是成功插入的总行数。

$res = $conn->insert_rows(
            'test', // 写入的表名称
            array('name'), // 写入的表字段
            array('string'), // 写入字段的字段类型
            array(
                array('aa'), // 第一行数据
                array('bb'), // 第二行数据
                array('cc'), // 第三行数据
            ),
        );

注意事项

MySQL 的 DDL 操作是无法被回滚的,即 create/drop databases,create/drop/alter table 这些命令无法被回滚,应慎重考虑在事务中包含 DDL 语句,若无法避免应考虑做相应的容错处理。

图一是去年栽的平枝栒子,特点是果子大,心型的叶子入秋后变红。
春季·栒子·开花

这盆栒子今天刚刚开花,初花时间比去年提前了一天。
春季·阳光·栒子·开花

珍珠黄杨,买花得的赠品,买的花没活赠品却活下来了。今年是来我这的第五个年头了,今年第一次开花结果,之前以为它不会开花呢。
春季·阳光·珍珠黄杨

小草不知其名,繁殖能力极强,去年首次栽种就泛滥了,大规模清除后,仅剩此两棵,差点被灭族。
春季·阳光·花盆·小草·苔藓·石头

木香,香味很淡。
春季·木香·花朵·绿叶

WordPress save_post 这个钩子有三个参数分别是:$post_ID$post$update,本篇对第三个参数 $update 总结一下。

$update 的定义

$update 是一个布尔型参数,它要么是 true,要么是 false,WordPress 开发文档对 $update 参数的定义是:

"Whether this is an existing post being updated."

字面上理解理解这句话:是否是对现有日志的更新。

上述定义带有一定的歧义和误导,事实上在日志编辑界面,并不是点更新按钮 $update 就是 true,点保存或发布按钮 $update 就是 false 。 WordPress 是根据编辑界面的标题或内容字段(仅限这两个字段)较上一次保存的版本是否发生变更来确定 $update 其值的:不管点什么按钮,只要标题或内容这两个字段有变更,$update 就是 false,否则,如果未发生变更,不管点什么按钮,点多少次,$update 的值都是 true 。

有趣的是当 $update 为 false 时,会生成修订版本,否则不生成修订版本。因此,推断 $update 这个字段可能是用来判断是否生成修订版本的。

简单一句话总结 $update 参数: 标题或内容无变动时, 点操作按钮(保存草稿/发布/更新)$update 为 true; 反之 $update 为 false 。

save_post 的触发时机

关于 save_post 这个钩子在什么情况下会触发,WordPress 开发文档给的解释如下:

save_post is an action triggered whenever a post or page is created or updated, which could be from an import, post/page edit form, xmlrpc, or post by email

大意是当我们通过编辑界面、 xmlrpc(如 wordpress APP)、 email 、导入工具,创建或更新日志或页面时,就会触发 save_post 钩子。

通过实验总结,以上解释也不是很具体。以发布一篇日志为例,从创建到保存、发布、更新,期间会多次触发 save_post 这个钩子,在此总结一下(可能不全,甚至有误)。

  1. 点 “写文章”,生成 $post_id,并立即触发 save_post 钩子,$update 值是 false
  2. 撰写新文章界面,不填任何内容, 直接点保存草稿/发布,均不触发 save_post, 发布不会成功
  3. 自动保存不触发 save_post
  4. 除以上情况,手动点保存草稿/发布/更新,都将触发 save_post

总结

如果希望借助 save_post 钩子做很细的控制操作,仅仅凭借 $update 一个参数是不够的,可能还需要通过 wp_get_post_parent_id() 借助 $post_ID 判断是否为修订版本,甚至是 save_post 第二个参数 $post 中的 post_datepost_statuspost_modified 来辅助。

wp_get_post_parent_id()会返回当前日志的父 ID,如果当前日志是修订版本,wp_get_post_parent_id() 返回它所附属日志的 post_id(大于 0);否则,返回 0 。下面结合编辑界面的按钮动作将 $update 和 wp_get_post_parent_id() 的对应值做一下总结,简短起见且将 wp_get_post_parent_id() 的返回结果命名为 parent_id

有变更 动作 $update parent_id
/ 新建 false 0
保存 true 0
发布 true 0
更新 true 0
保存 false >0
发布 false >0
更新 false >0

一直想将自己在用的主题分享出来,只因有一些私人信息不好处理,一直作罢。最近一段时间,将主题里相关的配置信息剥离出来,统一放到配置文件中,使得分享得以实现,等于现在我只需要维护一个私有的配置文件即可。现在将主题托管在了 GitHub 上,点这里直接下载。

功能介绍

  • 响应式布局;
  • 自动暗黑模式;
  • 支持 schemas 标记;
  • 支持部分 web app 特性;
  • 日志和评论支持无限滚动加载;
  • 标准(standard)格式支持视频背景;
  • 状态(status)格式支持喜欢按钮、 支持地理位置坐标;
  • 日志、评论自动记录发布者终端设备信息;
  • 上传附件附加随机字符串的安全机制;
  • 相对完善的垃圾评论抑制机制;
  • 丰富多样的参数化配置;
  • 原生 JavaScript 支持;
  • 支持代码高亮。

前天晚间博客遭受了一波垃圾评论攻击,攻击者大概是看了我之前的一篇介绍在用的防垃圾评论的日志,知道我这里评论文本框的名字叫 little_star,因此哥们一夜一间就制造了几百条垃圾评论。真是出乎意料,我一直觉得没人会对我的无名小站感兴趣,没想到这一天还是来了。

因此,这两天对在用的防垃圾评论机制进行了一些改进,总体思路是生成动态的评论文本框名称。方法大概入如下:
在主题 function.php 文件中加入以下几个函数

// 生成随机字符串
function get_brave_hash($hash_length = NULL) {
	$hash_length = !is_int($hash_length) ? rand(8,14) : abs($hash_length);
	return substr(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"), -$hash_length);
}

session_start(); // 开启 session

// 生成评论框名称
function get_comment_text_name() {
	$comment_text_name = 'real_comment';
	$comment_text_name_hash = get_array_key('_SESSION', $comment_text_name);
	if ( isset($comment_text_name_hash) ) {
		// session 里存在时直接返回 $_SESSION 中存储的值
		return $comment_text_name_hash;
	}
	$sep = '_';
	$hash_length = 8; // 8 位长度
	$comment_text_name_hash = $comment_text_name . $sep . get_brave_hash($hash_length);
	// 加入到 session
	$_SESSION[$comment_text_name] = $comment_text_name_hash;
	return $comment_text_name_hash;
}

// 获取数组键值
function get_array_key($array, $key) {
	if (is_string($array)) {
		global $$array;
		return array_key_exists($key, $$array) ? $$array[$key] : null;
	}
	return array_key_exists($key, $array) ? $array[$key] : null;
}

在主题评论文件中(我这里是 comments.php)调用 get_comment_text_name() 函数,生成动态的名称,用作评论文本框名称:

<?php
$comment_text_name = get_comment_text_name();
comment_form( array(
'fields' => array(
'author' => '<p class="commentform"><input type="text" name="author" id="author" class="inp cmt-input" placeholder="昵称 [必填]" aria-required="true" size="30" value="' . esc_attr( $commenter['comment_author'] ) . '"></p>',
'email' => '<p class="commentform"><input type="text" name="email" id="email" class="inp cmt-input" placeholder="邮箱 [必填·保密]" aria-required="true" size="30" value="' . esc_attr( $commenter['comment_author_email'] ) . '"></p>',
'url' => '<p class="commentform"><input type="text" name="url" id="url" class="inp cmt-input" placeholder="网址 [选填]" size="30" value="'.$comment_author_url.'"></p>',
'cookies' => '<p class="comment-form-cookies-consent"><input id="wp-comment-cookies-consent" name="wp-comment-cookies-consent" type="checkbox" value="yes"' . $consent . ' />' .
'<label for="wp-comment-cookies-consent" class="c4"> 记住我的个人信息</label></p>'
),
'comment_field' => '<textarea id="comment" name="comment"></textarea>
<textarea name= ' . '"' .$comment_text_name . '"' . ' class="inp text-bg comment-textarea" placeholder="添加评论..." aria-required="true" cols="45" rows="8"></textarea>',
'title_reply' => '发表评论',
'cancel_reply_link' => '取消回复',
'label_submit' => '发表评论',
'comment_notes_before' => '',
'comment_notes_after' => '',
'class_submit' => 'btn submit',
'action' => '/wp-stop-spam.php'
));
?>

在 wp-stop-spam.php 文件中通过 session 获取评论框名称,并进行置换

<?php
/** Sets up the WordPress Environment. */
require_once __DIR__ . '/wp-load.php';
nocache_headers();

$comment = trim($_POST['comment']);
if ( empty( $comment ) ) {
// 置换评论内容
$comment_text_name = get_comment_text_name();
$_POST['comment'] = trim($_POST[$comment_text_name]);
require_once( __DIR__ . '/wp-comments-post.php' );
}
if ( ! empty( $comment ) ) {
	// 返回错误
	wp_die('放过我吧!');
}