众所周知,trim 系列(包括 ltrimrtrim)函数在 PHP 中被用于删除字符串两边的空白字符,其中 ltrim 用于删除字符串开头的特殊字符,rtrim 删除字符串末尾的特殊字符。

ltrim 为例,不少资料给出的用法通常是:

ltrim($str[, $character_mask])

其中,第一个参数用于指定要操作的字符串,后面一个参数指定需要删除的“特殊字符“,一次在项目中如下使用 ltrim 函数:

$str = '/public/storage/app/file';
$del = '/public';
$ret = ltrim($str, $del);

按照往常的理解,上面一段代码执行后 $ret 值应该是字符串 /storage/app/file,然而实际使用中 $ret 的值是 storage/app/file ,也就是说实际处理过程中,ltrim 函数多删除了一个 / 符号!

其实这并不是 PHP 出现的 “bug”,在官方手册中,ltrim 函数提供了如下的使用案例:

When using a $character_mask the trimming stops at the first character that is not on that mask. So in the $string = "Hello world" example with $character_mask = "Hdle", ltrim($hello, $character_mask) goes like this:

  1. Check H from "Hello world" => it is in the $character_mask, so remove it
  2. Check e from "ello world" => it is in the $character_mask, so remove it
  3. Check l from "llo world" => it is in the $character_mask, so remove it
  4. Check l from "lo world" => it is in the $character_mask, so remove it
  5. Check o from "o world" => it is NOT in the $character_mask, exit the function Remaining string is "o world".

也就是说,第二个参数 $character_mask 在函数执行过程中,并不会被当做完整的字符串,而是作为“字符列表”,PHP 会从开头位置逐个遍历,如果发现有字符出现在“字符列表”中,就删除,直到发现第一个不在列表中的字符为止!

在之前的项目案例中,/public 作为字符列表,/字符包含在其中,因此当 ltrim 函数删除遇到 storage 之前·/ 字符时候,同样也会删除,而 storage 中的 s 并不在 /public 参数列表中,因此删除过程会在此停止,最后原始字符串就只剩下 storage/app/file 这部分了。