很多时候,对于明显的初级的PHP语法,我们一眼就能识别。假设稍微转换一下,这时就需要花点心思才能识破其中的奥妙。最困难的莫过于,微妙的用法与繁杂的业务代码、规则逻辑混在一起,散落在上千行代码时,想要在短时间内发现问题所在则是个巨大的挑战。
简单的判空
大家使用最多的PHP函数之一,也许是empty()这个函数了。而且,大家都知道,什么样的情况下,一个值会判断为空。摘自官方文档的说明,以下的东西被认为是空的:
- “” (空字符串)
- 0 (作为整数的0)
- 0.0 (作为浮点数的0)
- “0” (作为字符串的0)
- NULL
- FALSE
- array() (一个空数组)
- $var; (一个声明了,但是没有值的变量)
非常容易看出,以下代码输出的结果为true。
<?php $var = 0; var_dump(empty($var));
隐晦的判空
但是,结合其他函数一起使用时,并且不再是直接使用empty()函数来判断时,情况就开始变得晦涩了。一起来看下以下这段有问题的代码。看你需要多少时间才能发现里面的BUG?<?php
// 文章内容 $text = '软件开发是根据用户要求建造出软件系统或者系统中的软件部分的过程。……'; // 用户输入的关键字 $keyword = ''; $pos = strpos($text, $keyword); if ($pos) { // 找到了,是我感兴趣的文章 } else { // 未能找到关键字 }
这里的场景是,根据用户输入的关键字,匹配某篇文章是不是读者感兴趣的。如果文章包含关键字就视为是用户感兴趣的,否则就是不感兴趣的。正常情况下这段代码是可以正常工作的,问题在于,如果碰巧用户输入的关键字出现在文章开头时,就会发生意想不到的事情。例如,用户输入关键字“软件”。由于“软件”这个词出现在最前面,所以查找到的位置$pos值为0。最后在判断位置时,0被当作FALSE,即:$pos = !empty($pos) = !empty(0) = !TRUE = FALSE。实际是找到匹配了,却被误判断为未找到。由此就出现了一个故障。
两个引申
说到这里,我们可以引申出两点有意义的讨论。第一点是PHP全等判断,第二点是PHP函数的错误返回。
PHP全等判断
这点很好理解,从我们开始接触PHP编程时,就已经知道这一点了。全等判断是指不仅要求值相等,而且还要求类型也一样。即在不进行隐式类型转换的情况下,待比较的这两个值是否仍然相等。普通的相等,PHP默认会进行隐式的转换,使用两个等号 == 表示。全等判断则用三个等号 === 表示。
针对前面刚才的问题,可以使用全等来修正对是否找到这一逻辑的判断。即:
if ($pos !== FALSE) { // 找到了,是我感兴趣的文章 } else { // 未能找到关键字 }
这一点是大家都知道的,但下面这一点也许大家知道,但容易忽略。
PHP函数的错误返回
PHP底层的代码,可以分为两大类。一类是早期面向过程范式的函数,例如:strpos()、json_decode()、curl_exec()、file_get_contents()等函数。另一类是后期面向对象范式的类与对象,例如:PDO、Memcached、SoapClient等。对于前者,当失败时,例如字符串查找不到、JSON解码失败、URL抓取超时或者文件不存在时,所调用的函数会返回布尔值FALSE表示失败。而对于后者,即若使用的是封装的类,并通过类实例化的对象来操作时,当发生失败或者异常情况时,则会直接抛出异常。例如数据库连接失败、SOAP调用失败。
这可以说是PHP语言的惯例。当调用函数并失败时,返回FALSE,这里需要做好全等判断,避免与正常情况下的数字0、”” (空字符串)或”0″ (作为字符串的0)混淆。前面提到的关键字位置查找就是其中一例,又如文件内容的读取,若文件存在但读取的内容为空字符串,与文件不存在读取到的结果为FALSE,这两者之间是有着微妙的区别的。在进行项目开发时,一定要注意区分。
此外,如果是函数失败了,可以通过提供的错误码查找到对应的错误信息。例如,使用curl失败时,可以结合使用curl_errno()和curl_error()函数来进行判断或者查看错误信息。例如官网提供的示例:
<?php // 创建 cURL 句柄,指向一个不存在的位置 $ch = curl_init('http://404.php.net/'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); if(curl_exec($ch) === false) { echo 'Curl error: ' . curl_error($ch); }
为什么系统崩溃了?
再来看一个更为复杂的真实案例场景。在一个中型社交游戏系统中,有一个业务场景是需要获取每个用户的道具数量。相关代码片段如下:
<?php // 用户名 $user = 'dogstar'; // 缓存KEY $key = 'item_' . $user; // 缓存读取 $cache = new Cache\Memcached(); $num = $cache->get($key); if ($num <= 0) { // 查询数据库:SELECT COUNT(id) FROM user_items WHERE username = 'dogstar' $model = new Model\Item(); $num = $model->getUserItemTotalNum($user); $cache->set($key, $num, 600); }
// 更多业务处理……
在上面代码中, 对于用户名为dogstar的游戏玩家,先会从Memcache缓存中获取他的道具总数,如果之前没有缓存,则再从数据库查询该用户的道具总数。最后写入到缓存,避免下一次重复穿透读取数据库,从而减轻数据库的访问压力。
但该功能上线后,发现整个游戏系统都崩,经排查发现是数据库负载过高导致系统无法正常响应。原来,有很多玩家用户是没有任何道具的,即他们的道具数量为0。当没有缓存时,通过Memcached读取出来的值是FALSE,经隐式转换后也是0。此时,就无法区分是用户真的没有道具,还是没有缓存需要直接查询数据库。最终导致了在高并发的情况下,频繁穿透到数据库,进行聚合的查询,拖垮了数据库服务器。
这时的情况,会比以往都要严峻。首先,这里不仅有前面讨论的空判断,还引入了Memcached缓存和数据库查询。其次,这是确切发生在线上环境的故障问题,每一秒都在影响游戏玩家的用户体验,每一分钟都对我们的产品造成了损失,需要在面临重大压力、在最短的时间内找出原因并修复上线。最后,上面的代码片段是经过简化提炼的代码,实际情况上,代码可能遍布在你的项目里,更为要命的是,你的项目有10万行以上的代码!
对于这种情况,需要提前意识到会发生怎样的状况。改进的方式很简单,一种是沿用前面的全等判断;另一种就是,不要在缓存只保存基本类型的数据,而是保存一个结构体,即数组到缓存中。如:
// 缓存读取 $cache = new Cache\Memcached(); $data = $cache->get($key); if (empty($data)) { // 查询数据库:SELECT COUNT(id) FROM user_items WHERE username = 'dogstar' $model = new Model\Item(); $num = $model->getUserItemTotalNum($user); $data = array('num' => $num); $cache->set($key, $data, 600); } $num = $data['num'];
小结
作为三大程序控制结构之一,选择控制结构是我们平时项目开发过程中接触最多的。这里的条件逻辑判断,又会涉及对空和非空的判断。如果做出准确无误的判