最近在重构一个老项目,发现前任写了个神一样的display函数。这东西看起来简单,用起来坑多得像蜂窝煤。今天就来聊聊这个让人又爱又恨的display。
先说个真实案例。上周五晚上11点,我正在撸串,突然收到报警短信说网站500了。查了半天发现是某个display函数把数组当字符串输出了,直接把内存撑爆。这种低级错误都能犯,气得我把手里的羊肉串都捏碎了。
最基本的display用法是这样的:
<?php
function display($content) {
echo $content;
}
?>
看起来人畜无害是?但问题就出在这个echo上。你知道echo和print的区别吗?print是个函数,echo是个语言结构。这意味着print有返回值,而echo可以同时输出多个参数。比如:
<?php
echo 'hello', 'world'; // 合法
print 'hello', 'world'; // 报错
在display函数里用echo而不是print,这是有讲究的。因为echo的性能比print高那么一丢丢,在循环里用print的话,每次调用都要创建返回值,浪费资源。
但display真正坑爹的地方在于类型处理。看看这个例子:
<?php
display(['name' => '张三', 'age' => 25]);
?>
你猜会发生什么?直接给你报个Warning说数组不能转换成字符串。正确的做法应该是:
if(is_array($content)) {
print_r($content);
} else {
echo $content;
}
}
?>
你以为这就完了?太天真。print_r输出的数组格式丑得像被门夹过一样。于是我又加了个美化选项:
<?php
function display($content, $pretty = false) {
echo $pretty ? '<pre>' . print_r($content, true) . '</pre>' : print_r($content, true);
}
}
?>
这时候产品经理跑过来说:"这个显示不够直观,能不能加个表格形式?"我内心一万只草泥马奔腾而过,但还是写了个增强版:
<?php
function display($content, $format = 'raw') {
switch($format) {
case 'table':
if(!is_array($content)) {
trigger_error('表格格式需要数组数据', E_USER_WARNING);
return;
}
echo '<table border="1">';
foreach($content as $key => $value) {
echo "<tr><td>$key</td><td>$value</td></tr>";
echo '</table>';
break;
case 'json':
echo json_encode($content, JSON_PRETTY_PRINT);
default:
if(is_array($content)) {
echo '<pre>' . print_r($content, true) . '</pre>';
} else {
echo $content;
}
}
?>
你以为这就完美了?Too young too simple。有天我发现这个函数在API接口里被调用了,直接把HTML标签输出到了JSON响应里。于是又加了个环境检测:
<?php
function display($content, $format = null) {
if($format === null) {
$format = php_sapi_name() === 'cli' ? 'raw' : 'html';
}
// 剩下的逻辑...
}
?>
最坑的是性能问题。有次我用display输出一个10万条记录的查询结果,页面直接卡死了。后来发现print_r处理大数据时性能极差,于是改成了分批输出:
<?php
function displayLargeArray($array, $chunkSize = 1000) {
echo '[';
$chunks = array_chunk($array, $chunkSize);
foreach($chunks as $i => $chunk) {
if($i > 0) echo ',';
echo json_encode($chunk, JSON_PARTIAL_OUTPUT_ON_ERROR);
flush();
}
echo ']';
}
?>
说到flush,这里有个坑。有些PHP配置下flush不生效,需要同时调用ob_flush:
<?php
while (@ob_end_flush());
flush();
安全性也是个老大难问题。直接echo用户输入就是XSS的温床。所以display函数必须要有转义选项:
function display($content, $options = []) {
$defaults = [
'escape' => true,
'format' => null,
'chunkSize' => 1000
];
$options = array_merge($defaults, $options);
if($options['escape'] && is_string($content)) {
$content = htmlspecialchars($content, ENT_QUOTES);
}
?>
你以为这就完了?还没。有次我用display输出一个包含二进制数据的字符串,结果页面乱码了。于是又加了编码处理:
<?php
if(!mb_check_encoding($content, 'UTF-8')) {
$content = mb_convert_encoding($content, 'UTF-8');
}
?>
最搞笑的是,有天我发现这个display函数被调用了2000多次,每次调用都输出一个3KB的CSS文件。查了半天发现是某个菜鸟在循环里调用了display。于是又加了个缓存机制:
<?php
function display($content, $cacheKey = null) {
static $cache = [];
if($cacheKey !== null) {
if(isset($cache[$cacheKey])) {
return;
}
$cache[$cacheKey] = true;
}
?>
现在这个display函数已经膨胀到300多行代码了,比某些框架的渲染引擎还复杂。每次看到它我都想重写,但想想那些依赖它的老代码,还是算了。
最后说个真事:上周面试一个5年经验的PHPer,我问他知道display函数要注意什么,他张口就来"不就是echo吗"。我微笑着把他简历放进了碎纸机。
所以,别小看这个简单的display函数,里面的坑比你前女友的心思还多。记住:在PHP里,越是看起来简单的东西,用起来越要小心。