分类 代码分析 下的文章

PHP里的try/catch和set_exception_handler的执行顺序

在PHP里,try/catch和set_exception_handler都是用来捕获异常的,但是如果同时定义了这两个的话,谁会先执行呢?

如下代码:

set_exception_handler('myException');

function test($a) {
	if ($a > 1) {
		throw new Exception('the param is illegal !', 123);
	}

	echo $a;
}

try {
	test(2);
} catch (Exception $e) {
	echo 'I am try/catch.';
}

function myException($e) {
	echo 'I am set_exception_handler.';
}

输出结果:

I am try/catch.

结论:

set_exception_handler是用来处理所有未被捕获的异常。

simplexml_load_string解析的问题

在微信公众平台开发中,发现一个simplexml_load_string解析后的问题。

分析

微信给的PHP官方示例,代码如下:

$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
$fromUsername = $postObj->FromUserName;
$toUsername = $postObj->ToUserName;
$keyword = trim($postObj->Content);

这里假设fromUsername = 'openid';
后面往数据库里插入fromUsername数据的时候报错,提示openid字段不存在。

奇了怪了,openid明明是值,怎么会提示字段不存在呢。

在数据库插入前,打印fromUsername,输出如下:

SimpleXMLElement Object ( [0] => openid )

这里居然还是个对象,而不是字符串!!!

搜索一番,在php手册找到个同样的问题。

There seems to be a lot of talk about SimpleXML having a "problem" with CDATA, and writing functions to rip it out, etc. I thought so too, at first, but it's actually behaving just fine under PHP 5.2.6 

The key is noted above example #6 here: 
http://uk2.php.net/manual/en/simplexml.examples.php 

"To compare an element or attribute with a string or pass it into a function that requires a string, you must cast it to a string using (string). Otherwise, PHP treats the element as an object." 

If a tag contains CDATA, SimpleXML remembers that fact, by representing it separately from the string content of the element. So some functions, including print_r(), might not show what you expect. But if you explicitly cast to a string, you get the whole content. 

Text1 & XML entities'); 
print_r($xml); 
/* 
SimpleXMLElement Object 
( 
    [0] => Text1 & XML entities 
) 
*/ 

$xml2 = simplexml_load_string(''); 
print_r($xml2); 
/* 
SimpleXMLElement Object 
( 
) 
*/ 
// Where's my CDATA? 

// Let's try explicit casts 
print_r( (string)$xml ); 
print_r( (string)$xml2 ); 
/* 
Text1 & XML entities 
Text2 & raw data 
*/ 
// Much better 
?>

解决方法

同样的是在php手册下面找到解决方法,代码如下:

$postObj = (array)simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
$fromUsername = $postObj['FromUserName'];
$toUsername = $postObj['ToUserName'];
$keyword = trim($postObj['Content']);

将对象转为数组后,报错消失。

Discuz! 嵌套回复实现

最近支援一个项目,采用discuz!程序,需要实现嵌套回复,效果类似WordPress的效果,某条回复的回复需要排在一起显示。

嵌套回复效果

没有使用discuz!的点评功能,而是通过改造post表实现嵌套功能。

数据库结构

在discuz!的post表增加字段:

alter table pre_forum_post add parentid int(10) unsigned NOT NULL DEFAULT '0',topid int(10) unsigned NOT NULL DEFAULT '0';

- parentid 父级回复id
- topid 根回复id

添加回复

- 需要传递父级回复pid.
- 如果是根回复,parentid = 0,topid = 0.
- 如果是子回复,parentid = 父级回复的pid,topid = 父级回复的topid. 父级回复的pid和topid需要根据传递的pid进行查询.

查询一个回复下的所有嵌套回复

取一个根回复下的所有子回复,执行sql:

select * from pre_forum_post where toppid = xxx order by qpid desc, pid desc;

php处理成树形结构。

// $rows 为执行上述sql的查询结果
foreach ($rows as $k => $row) {
    if ($row['toppid'] == $row['parentid']) {
        $key = $row['parentid'] . '-' . $row['pid'];
    } else {
        $key = $map[$row['parentid']] . '-'. $row['pid'];
    }
    $map[$k] = $key;
}

ksort($map);
// 排序后的结果:
// 1
// 1-4
// 1-4-7
// 1-5
// 1-2

foreach ($map as $pid) {
   $result[$pid] = $rows[$pid];
}
// result 即为排序好后的回复列表

缓存

- 对所有topid的嵌套回复建立缓存。
pid => array(), array()中存储其下所有嵌套回复。
- 列表页直接查询缓存显示。

缺点

- 更新时触发缓存更新,需要一次取出topid下的所有回复进行php排序处理。
- 回复过多时,缓存过大或无法缓存。

其他

- 因为项目需求特殊,一条回复的嵌套回复只显示前50条,所以缓存不会太大。上面这种方案应该可以满足需求,但是需要考虑查询如何优化。
- 网上搜索了相关的资料,也没有找到太合适的方案。网上的方案是取出数据后进行递归遍历,后续比较下两种方式的效率差别。

参考资料

- 无限分类设计方案 http://www.ccvita.com/315.html
- 在数据库中存储层次数据 http://shiningray.cn/hierarchical-data-database.html

升级Discuz! X3.1后QQ互联提示connect_error_code_0的问题排查

问题表现

升级后,点击QQ互联登录按钮,提示下面的错误信息。

抱歉,当前存在网络问题或服务器繁忙,详细错误:connect_error_code_0,错误代码:,请您稍候再试。

原因分析

Discuz! X3.1的QQ互联集成了OAuth1.0和OAuth2.0的接口,升级脚本判断如果服务器支持ssl,就会使用OAuth2.0接口。
出现此问题的站点属于切换到了OAuth2.0接口后导致的。

source\plugin\manyou\Service\Client\OAuth.php

	public function dfsockopen($requestURL, $queryString = array(), $files = false) {
		return dfsockopen($requestURL, 0, $queryString, '', false, $this->_apiIp, 15, TRUE, !$files ? 'URLENCODE' : 'FORMDATA', true, 0, $files);
	}

注意这里请求url的使用,使用了$this->_apiIp参数,看下这个参数是在哪里定义的。

source\plugin\manyou\Service\Client\ConnectOAuth.php

	public function __construct($connectAppId = '', $connectAppKey = '', $apiIp = '') {

		if(!$connectAppId || !$connectAppKey) {
			global $_G;
			$connectAppId = $_G['setting']['connectappid'];
			$connectAppKey = $_G['setting']['connectappkey'];
		}
		$this->setAppkey($connectAppId, $connectAppKey);
		if(!$this->_appKey || !$this->_appSecret) {
			throw new Exception('connectAppId/connectAppKey Invalid', __LINE__);
		}

		if(!$apiIp) {
			global $_G;
			$apiIp = $_G['setting']['connect_api_ip'] ? $_G['setting']['connect_api_ip'] : '';
		}

		if($apiIp) {
			$this->setApiIp($apiIp);
		}
	}

这里可以看到使用的是后台设置的互联接口IP。

Discuz! 后台诊断工具里的互联接口IP是设置的OAuth1.0接口的域名,即http://openapi.qzone.qq.com。
而OAuth2.0接口的域名变更为了https://graph.qq.com,但是接口里使用的IP仍为OAuth1.0的,所以就导致无法请求,继而导致上述报错。

解决方法

上诊断工具里,去掉设置的互联接口IP即可。

2013年12月17日更新:
看到Discuz! 官网很多人反馈出现网络繁忙的问题,记日志看了下是请求用户openId这步报错了,应该是空间的接口有问题,静待官方解决。

2013年12月18日更新:
已经修复。