dragon_case():Laravel中snake_case()的替代性方案

在Laravel中的Helpers函数中,有一个很实用的方法 snake_case() ,可以将任何字符串转换为下划线分隔的形式,对于一些常见情况来说,实现的很好,比如:

$this->assertEquals("foo_bar", snake_case("FooBar"));
$this->assertEquals("foo_bar123", snake_case("FooBar123"));
$this->assertEquals("foo123_bar", snake_case("Foo123Bar"));
$this->assertEquals("123_foo_bar", snake_case("123FooBar"));
$this->assertEquals("foo_bar", snake_case("  FooBar  "));
$this->assertEquals("foo_bar", snake_case("fooBar"));
$this->assertEquals("foo_bar", snake_case("Foo Bar"));
$this->assertEquals("foo_bar", snake_case("Foo  Bar"));

在上面这些测试用例中,我们可以看到不管是包含数字,还是前后或者中间包含空格,都可以很好的转化,转化的结果也和预期一样。但是下面这几个例子,看起来就会比较奇怪了:

$this->assertEquals("foo_b_a_r", snake_case("fooBAR"));
$this->assertEquals("f_o_o_bar", snake_case("FOOBar"));
$this->assertEquals("foo._bar", snake_case("Foo.Bar"));
$this->assertEquals("foo__bar", snake_case("Foo_Bar"));
$this->assertEquals("foo-_bar", snake_case("Foo-Bar"));

对于我的期望来说,一个单词有三种情况,要么是完全小写,要么是完全大写,要么是首字母大写,但明显snake_case只认识两种形式,而且如果中间有特殊符号的话,应该就这些符号统一过滤为一个下划线,如果特殊符号就是下划线,那么最终的结果应该只有一个,而不应该是两个。

于是我们来看看修订版 dragon_case() 的测试用例,首先基本的能力要OK:

$this->assertEquals("foobar", dragon_case("foobar"));
$this->assertEquals("foo_bar", dragon_case("foo_bar"));
$this->assertEquals("foo_bar", dragon_case("FooBar"));
$this->assertEquals("foo_bar", dragon_case("fooBar"));
$this->assertEquals("another_foo_bar", dragon_case("AnotherFooBar"));

然后是对完全大写的单词的支持:

$this->assertEquals("foo_bar", dragon_case("FooBAR"));
$this->assertEquals("foo_bar", dragon_case("FOOBar"));
$this->assertEquals("foobar", dragon_case("FOOBAR"));

然后是在各个位置混合上了数字的情况:

$this->assertEquals("foo_bar123", dragon_case("FooBar123"));
$this->assertEquals("foo123_bar", dragon_case("Foo123Bar"));
$this->assertEquals("123_foo_bar", dragon_case("123FooBar"));
$this->assertEquals("foo123bar", dragon_case("Foo123bar"));
$this->assertEquals("123foo_bar", dragon_case("123fooBar"));

最后是在各个位置,混杂了一个或者多个特殊符号的情况:

$this->assertEquals("foo_bar", dragon_case("FOO____BAR"));
$this->assertEquals("foo_bar", dragon_case("FOO#BAR"));
$this->assertEquals("foo_bar", dragon_case("FOO.BAR"));
$this->assertEquals("foo_bar", dragon_case("Foo  Bar"));
$this->assertEquals("foo_bar", dragon_case("   foo  BAR   "));

在这个版本的实现中,我们把数字完全当成小写字母对待,所以先准备两个辅助函数:

function lower_case($chars, $index) {
    return isset($chars[$index]) && (ctype_lower($chars[$index]) || ctype_digit($chars[$index]));
}
function upper_case($chars, $index) {
    return isset($chars[$index]) && ctype_upper($chars[$index]);
}

最后的实现反而没有什么花头,就是将字符串拆解成一个字符数组,然后一个个的判断,是不是需要在字符前面增加一个下划线分隔,实现方式有点繁琐,有待进一步优化。

function dragon_case(string $input): string {
    $input = trim($input);
    $chars = str_split($input);
    $result = '';

    foreach($chars as $index => $char) {

        if( ctype_upper($char) && $index !== 0 ) {
            if( lower_case($chars, $index+1) && upper_case($chars, $index-1) ) {
                $result .= '_';
            }

            if( lower_case($chars, $index+1) && lower_case($chars, $index-1) ) {
                $result .= '_';
            }

            if( upper_case($chars, $index+1) && lower_case($chars, $index-1) ) {
                $result .= '_';
            }
        }

        if( ctype_alnum($char) === false ) {
            if( str_split($result)[count(str_split($result)) - 1] != '_' ) {
                $result .= '_';
            }
        } else {
            $result .= strtolower($char);
        }
    }

    return $result;
}

这样 dragon_case() 就实现成功了,之所以叫 dragon,完全是因为 dragon 是 snake 的进化版么。如果想使用这个 Helpers 方法,大家可以直接加载 cuimingda/laravel-helpers 组件,已经包含在其中了。

composer require cuimingda/laravel-helpers

将PHP数组所有的Key,都从驼峰形式转变成下划线形式

比如有这样一个数组:

[
    "Sku" => "A1B2C3D4",
    "FriendlyUrl" => "http://example.com/1.html"
]

希望能够转化为下面这个形式的数组:

[
    "sku" => "A1B2C3D4",
    "friendly_url" => "http://example.com/1.html"
]

这里借助Laravel的Collection对象和snake_case这个helper函数,可以很方便实现这个效果

function array_snake_case_keys(array $items): array {
    $items = collect($items);
    return $items->keys()->map(function($key) {
        return snake_case($key);
    })->combine($items->values())->all();
}

btw,这个函数已经封装到了 cuimingda/laravel-helpers 中,只要引入进来就可以直接像其他helper函数一样使用:

composer require cuimingda/laravel-helpers

还有需要说明的是,snake_case这个函数有个bug,如果是imgURL这种大写字母组成的字符串,期望转化成img_url,但实际会转化为img_u_r_l,在laravel/framework中提了一个Issue,但被认为没有问题。