在PHP项目开发过程中,我们经常会遇到需要将格式化的字符串路径转换为多维数组的需求,比如用户传入的a.b.c需要转换为['a'=>['b'=>['c'=>null]]]的结构,这种场景常见于配置项解析、动态数据赋值、表单数据映射等场景。如果采用不安全的实现方式,很容易引入代码注入风险,或者无法处理各种边界情况。

常见不安全实现的问题
很多开发者会直接使用eval函数来实现路径转换,比如下面的代码:
<?php
function unsafePathToArray($path, $value = null) {
$keys = explode('.', $path);
$code = '$result';
foreach ($keys as $key) {
$code .= "['$key']";
}
$code .= " = $value;";
eval($code);
return $result;
}
// 调用示例
$res = unsafePathToArray('a.b.c', 'test');
print_r($res);
?>
这种实现存在两个明显问题:第一,如果路径中包含单引号等特殊字符,会直接破坏eval的语法结构导致报错;第二,如果路径是用户可控的输入,恶意用户传入类似a'];phpinfo();//的内容,就会执行任意代码,造成严重的安全漏洞。另外这种实现也没有对键名的合法性做校验,比如路径中包含空字符串、非法字符等情况都无法处理。
安全实现的核心思路
安全的实现需要遵循以下原则:
- 不使用
eval等可执行动态代码的函数,避免代码注入风险 - 对路径字符串做严格的格式校验,过滤非法内容
- 逐层解析路径,手动构建多维数组,不依赖动态代码生成
- 处理各种边界情况,比如空路径、重复路径、键名包含特殊字符等
完整安全实现代码
下面是经过验证的安全实现方案,包含路径校验、逐层解析、异常处理等逻辑:
<?php
/**
* 将字符串路径安全转换为多维数组
* @param string $path 路径字符串,如 a.b.c
* @param mixed $value 路径对应的最终值
* @param string $separator 路径分隔符,默认是点号
* @return array 转换后的多维数组
* @throws InvalidArgumentException 路径格式非法时抛出异常
*/
function safePathToArray($path, $value = null, $separator = '.') {
// 校验路径是否为空
if (empty($path) || !is_string($path)) {
throw new InvalidArgumentException('路径参数必须是非空字符串');
}
// 校验分隔符是否合法
if (empty($separator) || !is_string($separator)) {
throw new InvalidArgumentException('分隔符必须是非空字符串');
}
// 分割路径得到所有键名
$keys = explode($separator, $path);
// 校验每个键名是否合法
foreach ($keys as $key) {
// 键名不能是空字符串
if ($key === '') {
throw new InvalidArgumentException('路径中不能包含空的键名');
}
// 键名不能包含分隔符本身,避免解析歧义
if (strpos($key, $separator) !== false) {
throw new InvalidArgumentException('键名不能包含路径分隔符');
}
}
// 从最内层开始构建数组
$result = $value;
// 倒序遍历键名,逐层包裹数组
for ($i = count($keys) - 1; $i >= 0; $i--) {
$currentKey = $keys[$i];
$result = [$currentKey => $result];
}
return $result;
}
// 基础使用示例
try {
$test1 = safePathToArray('a.b.c', 'hello');
print_r($test1);
// 输出:Array ( [a] => Array ( [b] => Array ( [c] => hello ) ) )
$test2 = safePathToArray('user.info.name', '张三');
print_r($test2);
// 输出:Array ( [user] => Array ( [info] => Array ( [name] => 张三 ) ) )
} catch (InvalidArgumentException $e) {
echo '路径错误:' . $e->getMessage();
}
?>
扩展:支持合并已有数组的实现
上面的基础版本是每次生成全新的数组,如果需要在已有的多维数组基础上赋值,可以扩展实现如下:
<?php
/**
* 将路径赋值到已有的多维数组中
* @param array $target 目标数组,会被直接修改
* @param string $path 路径字符串
* @param mixed $value 要赋的值
* @param string $separator 路径分隔符
* @throws InvalidArgumentException 路径格式非法时抛出异常
*/
function assignPathToArray(&$target, $path, $value, $separator = '.') {
if (empty($path) || !is_string($path)) {
throw new InvalidArgumentException('路径参数必须是非空字符串');
}
$keys = explode($separator, $path);
foreach ($keys as $key) {
if ($key === '') {
throw new InvalidArgumentException('路径中不能包含空的键名');
}
if (strpos($key, $separator) !== false) {
throw new InvalidArgumentException('键名不能包含路径分隔符');
}
}
// 引用传递,逐层定位到目标位置
$current = &$target;
foreach ($keys as $key) {
// 如果当前层不是数组,先转为数组
if (!is_array($current)) {
$current = [];
}
$current = &$current[$key];
}
// 赋值
$current = $value;
// 解除引用
unset($current);
}
// 使用示例
$existing = ['a' => ['x' => 1]];
try {
assignPathToArray($existing, 'a.b.c', 2);
print_r($existing);
// 输出:Array ( [a] => Array ( [x] => 1 [b] => Array ( [c] => 2 ) ) )
} catch (InvalidArgumentException $e) {
echo '错误:' . $e->getMessage();
}
?>
边界情况处理说明
该实现已经覆盖了常见的边界场景:
- 路径为空或者不是字符串时直接抛出异常
- 路径中包含连续分隔符(比如
a..b)会检测到空键名并抛出异常 - 键名包含分隔符的情况会被拦截,避免解析歧义
- 赋值到已有数组时,如果中间层不是数组会自动初始化为数组,避免报错
如果项目中有特殊需求,比如允许键名包含特定特殊字符,可以在键名校验逻辑中调整规则,比如增加正则校验,只允许字母、数字、下划线等合法字符作为键名,进一步提升安全性。