V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
medz
V2EX  ›  PHP

在 ThinkSNS+ 中,如何利用 Laravel 表单验证来验证用户名的(我朝独有需求,两个字母占一个汉字。。。)

  •  
  •   medz · 2017-06-13 10:03:33 +08:00 · 4188 次点击
    这是一个创建于 2755 天前的主题,其中的信息可能已经有所发展或是发生改变。

    需求

    先说下需求吧,这是一个挺操蛋的需求,大多数 PHPer 接到真需求就不想满足它,嗯,国内的 PM 经常会提的,就是用户的用户名或者发表文章的内容或者标题,两个单字节字符才能算一个长度单位。

    例如:用户名要求最长 12 位,如果输入英文,可以输入 24 位。开心吧?在 PHP 中 strlen 是按照「字节」计算的,而 mb_strlen 是完整完整字符族算的,都无法满足。

    场景

    因为 ThinkSNS+ 是使用 Laravel 开发的,所以在注册、登录等操作的时候验证用户名字段都应该使用「表单验证」这就涉及到规则的拓展,很明显,又长度限制和字符范围限制,所以应该拓展两个规则,分别是:

    • 用户名字符允许规则
    • 用户名长度规则

    但是长度不只是在用户名场景使用,所以最后修改为「显示长度规则」

    AppServiceProviderboot 方法中增加规则。

    用户名允许字符规则
    Validator::extend('username', function ($attribute, $value) {
        return preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $value);
    });
    

    这是我们定的用户名规则,只是不允许非数字开头,也不允许出现特殊字符。

    显示长度规则
    Validator::extend('display_length', function ($attribute, $value, array $parameters) {
        if (empty($parameters)) {
            throw new \InvalidArgumentException('Parameters must be passed');
        }
    
        $min = 0;
        if (count($parameters) === 1) {
            list($max) = $parameters;
        } elseif (count($parameters) >= 2) {
            list($min, $max) = $parameters;
        }
    
        if (! isset($max) || $max < $min) {
            throw new \InvalidArgumentException('The parameters passed are incorrect');
        }
    
        // 计算单字节.
        preg_match_all('/[a-zA-Z0-9_]/', $value, $single);
        $single = count($single[0]) / 2;
    
        // 多子节长度.
        $double = mb_strlen(preg_replace('([a-zA-Z0-9_])', '', $value));
    
        $length = $single + $double;
    
        return $length >= $min && $length <= $max;
    });
    

    你可能觉额长度这么复杂?不,其实不复杂,使用上:

    $rules = [
    	'foo' => 'display_length:12', // 最长 12 显示长度
        'bar' => 'display_length:4,12', // 显示长度在 4 - 12 之间
    ];
    
    长度计算方法规则解析

    其实一开始想了很多方法,都不理想,例如「转 GBK 」、「(strlen + mb_strlen) / 4 」等,转 GBK 的确也能正确的计算出来,到那时通过测试后发现,转码所消耗的性能比这个方法要更耗性能。字符方法只对中文两万多个常用汉字有用,而且我们希望兼容全球的所有语言以及 emoji。

    最后我们想出来了将单字节字符提取出来单独计算,而多子节字符按照正常字符族计算不就好了吗?

    首先提出单字节计算:

    preg_match_all('/[a-zA-Z0-9_]/', $value, $single);
    $single = count($single[0]) / 2;
    

    这样之后,每一个单字节字符都按照 0.5 的长度进行计算了。

    最后将单字节删除用 mb_strlen 计算除字符族:

    $double = mb_strlen(preg_replace('([a-zA-Z0-9_])', '', $value));
    

    这样我们就满足了「两个英文=一个中文」的需求了。


    上面的打码都是来自于我们团队正在开发的国内开源产品「 ThinkSNS+」,开源不易,大家帮忙点一个 Star 呗!!!

    GitHub:https://github.com/zhiyicx/thinksns-plus

    ThinkSNS+ 是基于 Latavel 而开发,里面涉及了很多 Laravel 知识点,个人觉得也可以作为 PHPer 学习使用 Laravel 开发应用的一个范例。

    19 条回复    2017-06-14 22:21:58 +08:00
    luoyou1014
        1
    luoyou1014  
       2017-06-13 11:03:53 +08:00
    咦,我记得 ThinkSNS 不是用 ThinkPHP 开发的吗?
    ThinkSNS+ 是升级后用 Laravel 重构了还是另外一个开源项目?
    sun019
        2
    sun019  
       2017-06-13 11:16:46 +08:00
    以前一直在用 ThinkSNS 哈哈,后面转 Laravel 了,感谢
    leafans
        3
    leafans  
       2017-06-13 13:15:01 +08:00
    mb_strwidth(),或 mb_strimwidth(),1 个汉字=2 个英文
    medz
        4
    medz  
    OP
       2017-06-13 14:18:52 +08:00
    @luoyou1014 其实不算重构,就是因为 ThinkPHP 已经不适用现在的方向了,所以我们全新开发的,但是后续会增加 ThinkSNS 升级到 ThinkSNS+
    medz
        5
    medz  
    OP
       2017-06-13 14:24:24 +08:00
    @leafans
    | 字符 | 宽度 |
    |----|----|
    |U+0000 - U+0019 | 0 |
    |U+0020 - U+1FFF | 1 |
    |U+2000 - U+FF60 | 2 |
    |U+FF61 - U+FF9F | 1 |
    |U+FFA0 - | 2

    这是 PHP 官方文档给出的范围表。
    medz
        6
    medz  
    OP
       2017-06-13 14:25:39 +08:00
    @leafans

    ```shell
    php -r "var_dump(mb_strwidth('12 哈😄'));"
    ```
    输出值是 5
    medz
        7
    medz  
    OP
       2017-06-13 14:27:19 +08:00
    @leafans

    `12 哈😄` 按照这种需求计算出来应该是 3
    jiangzhuo
        8
    jiangzhuo  
       2017-06-13 14:37:23 +08:00
    还好吧,我见过按照宋体渲染出来的宽度作为长度的需求。
    willywu001
        9
    willywu001  
       2017-06-13 16:26:48 +08:00
    github 上 安装连接打不开了。怎么回事
    KomeijiSatori
        10
    KomeijiSatori  
       2017-06-13 17:49:30 +08:00
    echo iconv_strlen("喵喵喵","UTF-8"); //3
    hst001
        11
    hst001  
       2017-06-13 18:44:19 +08:00
    占宽很常见的需求吧,知道编码很容易计算
    changwei
        12
    changwei  
       2017-06-13 20:22:09 +08:00 via Android
    反正我就是一直用的 iconv 转 gbk 来实现这种计算,话说这得多大的用户量和并发才能体现出这一点性能差距啊。。。
    leafans
        13
    leafans  
       2017-06-13 22:22:03 +08:00
    @medz 特殊情况 特殊处理。。。 - - |||
    $value = '12 哈😄';
    preg_match_all('/[\x{1f600}-\x{1f64f}]/u', $value, $result);
    echo (count($result[0]) + mb_strwidth($value)) / 2;
    leafans
        14
    leafans  
       2017-06-13 22:36:29 +08:00
    @medz 又试了下,mb_strwidth() 对大部分看上去比较宽的特殊符号按 1 个算,适用范围不广。
    medz
        15
    medz  
    OP
       2017-06-14 11:20:50 +08:00
    @changwei 转 gbk 确实最简单的方法,不过我这边不用主要就是因为字符集转换在大量用户的情况下远比正则低。
    medz
        16
    medz  
    OP
       2017-06-14 11:21:25 +08:00
    @leafans 是滴,就是官方文档也给出了范围,所以才选择造轮子。
    medz
        17
    medz  
    OP
       2017-06-14 13:48:01 +08:00
    @hst001 你的想法其实就是知道编码,转 gbk 咯~但是帖子里我也提到了,不用转码计算,是因为字符集转换比正则更耗性能。
    hst001
        18
    hst001  
       2017-06-14 18:21:25 +08:00
    @medz #17 转字符集是?我的意思一个对字符串遍历一下就可以了
    medz
        19
    medz  
    OP
       2017-06-14 22:21:58 +08:00
    @hst001 如果你按照字节码遍历,相当于还是造了一个 mb_strlen 函数出来,php 目前造出来的轮子,远没有底层 c 拓展造出来的轮子性能高,所以,你用 php 遍历字节码,性能也会比正则+mb_strlen 低的~
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2103 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 16:13 · PVG 00:13 · LAX 08:13 · JFK 11:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.