PHP| 开发拾遗 0x01

date: 2018-2-4 17:35:11
title: PHP| 开发拾遗 0x01

记录 PHP 开发中的二三事

提纲:

  • 请使用 const 常量
  • 再说 = == ===
  • 原生函数 json_encode()/array_merge()/preg_match_all()
  • 一次「压平」 if 的踩坑记录
  • 简单的「频次限制」, 常见场景比如重复点击重复请求

请使用 const 常量

其实开始我是「拒绝」的, 理由是增加了一层 映射, 就是变相的增加了一层 复杂度, 比如最常见的常量应用场景, 表示各种状态:

const STATUS_UNDO = 'undo';
const STATUS_DOING = 'doing';
const STATUS_SUCCESS = 'success';
const STATUS_FAIL = 'fail';

之前一直抱有的观点是, 记忆一次 undo/doing/success/fail 就够了, 没必要 常量 再来一层, 用的地方保持就好了.

但最近几次密集的使用常量, 让我改变了这个想法 -- 常量是可以 IDE 提示的. 由于编程是一件 精确 的活, 原来的方式需要 精确记忆细节 或者 复制粘贴, 有了 IDE 提示后就简单多了.

同时再想想下面的场景:

  • 如果字符串不是这么简单, 有点长(这种情况太多了), 经常在 array key 类似的场景用到
  • 如果这个字段是数据表中映射出来的, 有十多个类似字段
  • 如果这个表示状态用的数字, 比如 0-undo, 1-doing, 2-success, 3-fail, 就需要在使用的地方带上注释了
if (3 == $status) { // 失败状态
    // fail case
} else if (2 == $status) {
    // success case
}

综上, 常量其实一件 省事 的事儿

再说 = == ===

上面的代码其实已经示范了一个例子, === 是初学很容易遇到的 困惑, 这里再简单重申一下定义:

  • =: 赋值语句, 给变量赋值
  • ==: 判断是否 相等
  • ===: 判断是否 全等, 区别与 == 的是要求数据类型一致

理解清楚定义, 然后再看 2 个场景:

if ($status = 1) { // 如果这里把 == 少写了
    //
} else {
    //
}

上面的错误基本每个人都犯过吧, 尤其是是使用 if ($var = xxx) 确实有另外一个用途:

$var = 'xxx'; // 给 $var 赋值
if ($var) {
    //
}

if ($var = 'xxx') { // 常见的缩写方式
    //
}

有效避免这种错误的方式:

if (1 == $status) { // 如果少写了 =, IDE会自动提示
    //
} else {
    //
}

推荐这样的写法, 因为最近一次 bug 就是这个问题导致的, 指不定哪个夜黑风高的晚上, 又写出这种 bug 出来.

再来说说 =====:

if (strpos('abc', 'a')) { // 判断字符串是否存在
    echo 'yes';
} else {
    echo 'no';
}

这里明显是个错误的例子, 因为 strpos() 函数返回的是匹配到的 起始位置, 即 int 0, 不匹配时返回 bool false, 正确的做法应该是:

if (strpos('abc', 'a') !== false) {
    echo 'yes';
} else {
    echo 'no';
}

===== 的关键点就在于数据类型上, 弱类型是 对人友好, 强类型是 对机器友好.

原生函数 json_encode()/array_merge()/preg_match_all()

接着上面的 strpos() 继续聊几个原生函数.

因为 json 的大行其道, json_encode()/json_encode() 就会经常使用到了, 直接说几个要点:

  • 需要 ext-json 扩展支持, 不过 PHP 默认是开启这个扩展的, 所以发现用不了的时候不要大呼 见鬼了
  • json 数据类型: bool int string array object, 因为 PHP 的弱类型, 带来几个需要注意的类型转换的问题:
json_encode(1); // int <-> string
json_encode('1');

json_encode(true); // bool <-> string
json_encode('true');

echo json_encode(['a' => '']); // 空字符串: {"a":""}
echo json_encode(['a' => []]); // 空数组: {"a":[]}
echo json_encode(['a' => new \Stdclass()]); // 空对象: {"a":{}}

尤其要注意这里的 new \Stdclass(), 毕竟 PHP 编程中, 经常是只使用 array 这一种数据结构.

这也是为什么要使用 json_decode($str, true) 的原因, 默认返回 Stdclass 类型, 带 true 参数才是 array 类型

  • Unicode转义
echo json_encode('中国'); // "\u4e2d\u56fd"
echo json_encode('中国', JSON_UNESCAPED_UNICODE); // "中国"

这样就不用拿到之后还要 decode 一下了, 而且不转义下需要传输的字节数减少了其实性能更好, 具体可以参考鸟哥的 blog

array_merge() 的坑不知道有多少人踩过, 在 PHP manual 上是有说的: 不能递归合并, 所以很多框架都提供了辅助函数来处理:

// 比如 yii 框架的 \yii\helpers\BaseArrayHelper::merge()
public static function merge($a, $b)
{
    $args = func_get_args();
    $res = array_shift($args);
    while (!empty($args)) {
        $next = array_shift($args);
        foreach ($next as $k => $v) {
            if ($v instanceof UnsetArrayValue) {
                unset($res[$k]);
            } elseif ($v instanceof ReplaceArrayValue) {
                $res[$k] = $v->value;
            } elseif (is_int($k)) {
                if (isset($res[$k])) {
                    $res[] = $v;
                } else {
                    $res[$k] = $v;
                }
            } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
                $res[$k] = self::merge($res[$k], $v);
            } else {
                $res[$k] = $v;
            }
        }
    }

    return $res;
}

preg_match_all() 是正则匹配, 比较适合日常使用了, 这里简单mark一下:

preg_match_all("/href='(.*?)'/", $str, $output);
// $output[0] 返回所有满足整段正则的字符串
// $output[1] 开始以此返回 () 中匹配到的值, 类似 perl 中的 $1, $2...

一次「压平」 if 的踩坑记录

首先是一个简单风格的对比:

// if-else
if ('a' == $a) {
    //
} else {
    //
}

// 压平一点
$a = 'xxx';
if ('a' == $a) {
    //
}

个人倾向于后一种, 这样可以只有考虑一次 if, 当然具体情况要具体分析.

来看看具体的采坑记:

function checkStatus() { // 读取配置请检查条件是否满足
    $a = getConfig(); // 获取配置

    // 基础条件: 任一一个不满足就返回 false
    if ($a['base']) {
        foreach ($a['base'] as $v) {
            if (!$v) {
                return false;
            }
        }
    }

    // 附加条件: 满足基础条件的情况下, 还需要满足附加中的一项
    if ($a['option']) {
        foreach ($a['option'] as $v) {
            if (!$v) {
                return true;
            }
        }
    }

    return false;
}

这是初版的代码, 基础条件附加调价 都是可配置的, 如果没有配置 附加条件, 就出现了 2 个问题:

  • 没有对变量值进行检测, 尤其是 array 的 key, 这是 PHP 中 非常常见 的错误
  • 只是简单的改为 if (isset($a['option']}), 恭喜你, 逻辑错误, 这也是 根据报错改代码 容易遇到的问题

正确的版本:

function checkStatus() { // 读取配置请检查条件是否满足
    $a = getConfig(); // 获取配置

    // 基础条件: 任一一个不满足就返回 false
    if ($a['base']) {
        foreach ($a['base'] as $v) {
            if (!$v) {
                return false;
            }
        }
    }

    // 附加条件: 满足基础条件的情况下, 还需要满足附加中的一项
    if (empty($a['option'])) {
        return true; // 通过了基础条件, 到这里就需要返回 true
    }
    foreach ($a['option'] as $v) { // 判断附加条件
        if (!$v) {
            return true;
        }
    }

    return false;
}

这里提醒 2 点:

  • 使用 isset() / empty() 来进行变量检测
  • 尽管大部分情况下, 写业务 看起来就是写 if-else, 但是请务必小心, 随着复杂度提升, 很容易出错的

简单的「频次限制」

常见场景: 防止页面重复点击后端重复处理, 加入 60s 点击限制

先来看最终结果:

if (1 == MyRedis::incr(MyRedis::CLICK_ITEM_A)) {
    MyRedis::expire(MyRedis::CLICK_ITEM_A, 60); // 60s 过期时间
    // 业务逻辑
}

MyRedis() 类是使用 facade 设计模式, 对 exe-redis 扩展的封装, 这样业务不用关心 redis client 初始化的相关的细节:

// ext-redis 初始化的相关细节
$redis = new \Reids();
$redis->connct('127.0.0.1');
$redis->auth('password')
$redis->select(1);

// 方法参数和返回值 和 ext-redis 扩展保持一致
$redis->incr($key);
MyRedis::incr(MyRedis::CLICK_ITEM_A);

而且自己封装的 MyRedis() 类还可以使用常量, 有效标识出 具体业务

写在最后

细节出魔鬼
practice make perfect

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • PHP7 已经出来1年了,PHP7.1也即将和大家见面,这么多好的特性,好的方法,为什么不使用呢,也希望PHP越来...
    梦幻_78af阅读 2,069评论 1 10
  • PHP 学习目录 ├─PHP视频教程 1 LAMP网站构建 │ ├─PHP教程 1.1.1 新版视频形式介绍│ ...
    曹渊说创业阅读 16,143评论 29 417
  • 把当前目录作为Root Document只需要这条命令即可:php -S localhost:3300 也可以指定...
    绚烂的时光阅读 724评论 0 1
  • 上一章 曲临身受重伤,但是总算活着离开危城了,正面对抗惊怖大将军果然凶险异常。差一点点,就差一点点,他就得把命留在...
    天涯叶开阅读 499评论 4 6
  • 曼舞轻歌人生路, 殊途同归简村落。 兆丰年皆缘久寒, 兰幽香是因笃守。 注1:曼殊兆兰,网名,简村文友,诗书画皆佳...
    亮靓_27d5阅读 600评论 16 39