URL 字符串的解析在 PHP 项目里几乎无处不在,回调地址校验、路由参数提取、分享链接生成、日志记录中的请求来源分析,都离不开对 URL 结构的拆解。PHP 本身并没有一个函数能像“解析整个 URL 并返回所有参数”那样一步到位,但把 parse_url 和 parse_str 组合起来,再配合 Web 环境下的 $_GET 与 $_SERVER,就能覆盖绝大多数需求。本文会从 URL 的结构拆分讲起,逐步说明查询字符串转数组、当前请求参数获取以及复杂场景下重新拼接 URL 的做法。

理解这些函数的行为差异很重要,因为同样是获取参数,$_GET 已经经过 PHP 解析,而 parse_str 可以处理任意来源的查询串,两者的变量名转换规则和安全边界并不完全相同。下面按功能模块展开。
一、parse_url:先拆出URL的协议、主机和路径
parse_url 是 PHP 内置函数,作用是接收一个 URL 字符串,返回包含多个组件的关联数组。数组里通常会出现 scheme、host、port、user、pass、path、query 和 fragment 等键。比如一个带端口、用户信息和锚点的链接,可以直接被拆成独立部分,后续提取域名或查询串就非常方便。
<?php $url = 'https://user:pass@www.ipipp.com:8443/path/to/page?a=1&b=hello&c[]=x&c[]=y#frag'; $parts = parse_url($url); var_dump($parts); /* array( 'scheme' => 'https', 'host' => 'www.ipipp.com', 'port' => 8443, 'user' => 'user', 'pass' => 'pass', 'path' => '/path/to/page', 'query' => 'a=1&b=hello&c[]=x&c[]=y', 'fragment' => 'frag', ) */ echo parse_url($url, PHP_URL_HOST); // www.ipipp.com ?>
如果只关心某一个组件,可以传入第二个常量参数,例如 PHP_URL_SCHEME、PHP_URL_HOST、PHP_URL_PATH 或 PHP_URL_QUERY。这样函数会直接返回字符串,而不是数组。需要特别注意的是,parse_url 只负责拆分,不会验证域名是否真实存在、协议是否可访问,也不会对查询参数做解码。所以拿到的 query 依然是一个原始字符串,还需要后续用 parse_str 处理。
另一个容易忽略的问题是,parse_url 对相对 URL 的表现并不一致。如果传入的字符串没有协议头,PHP 可能会把一部分内容识别成 path,甚至在格式严重错误时返回 false。例如 //ipipp.com/path 这种以双斜杠开头的地址,不同 PHP 版本可能返回 host,而 www.ipipp.com/path 可能被当成路径。因此在解析用户输入的 URL 时,最好先确认它是否包含合法协议,或者手动补上临时协议再调用。
二、parse_str:把查询字符串安全地转成变量
如果已经拿到查询字符串,例如 name=Alice&age=30,下一步就是把它转换成键值对。PHP 提供了 parse_str 函数,可以把这个字符串解析为变量。推荐始终传入第二个数组参数,这样解析结果会写入该数组,避免直接污染当前作用域中的变量。
<?php
$query = 'name=Alice&age=30&tags[]=php&tags[]=laravel&redirect=/user/profile';
$params = [];
parse_str($query, $params);
var_dump($params);
/*
array(
'name' => 'Alice',
'age' => '30',
'tags' => array('php', 'laravel'),
'redirect' => '/user/profile',
)
*/
?>
上面的例子中,tags[] 这种写法会被解析成数组,这是 PHP 解析查询串的常见机制。需要特别留意,parse_str 会自动把参数名中的点号和空格转换成下划线。例如 user.name=Tom 解析后的键会变成 user_name。如果你的接口字段名本身含有点号,就不能直接依赖这个函数,需要自己使用 explode 和 urldecode 来处理。
安全方面还有一个非常关键的细节:如果调用 parse_str 时不传第二个参数,它会像 extract 一样把解析出来的键值对写入当前符号表。这样外部传入的 action、debug 等参数,可能覆盖脚本里已有的同名变量,造成严重安全隐患。因此无论什么场景,都应该使用 parse_str($query, $output) 这种双参数形式。
当遇到重复参数时,parse_str 默认保留最后一个值。比如查询串为 page=1&page=2,最终得到的 page 是字符串 2。如果业务上需要保留全部重复值,就必须绕过默认解析,手动按 & 分割后再逐项处理。
<?php
$query = 'tag=php&tag=web&tag=security';
$rawPairs = explode('&', $query);
$values = [];
foreach ($rawPairs as $pair) {
if ($pair === '') continue;
[$key, $value] = array_pad(explode('=', $pair, 2), 2, null);
$key = urldecode($key);
$value = urldecode($value);
$values[$key][] = $value;
}
var_dump($values['tag']); // array('php', 'web', 'security')
?>
这样的手动处理虽然代码多一点,但能保留参数出现顺序和重复内容,在签名校验、日志还原等场景非常有用。
三、结合$_GET与$_SERVER获取当前请求参数
在常规的 Web 请求中,PHP 已经自动把当前 URL 中的查询字符串解析到了 $_GET 数组,大部分情况下并不需要自己调用 parse_str。例如访问 /article.php?id=18&lang=zh,直接读取 $_GET['id'] 和 $_GET['lang'] 即可。这种方式代码更短,也能享受 PHP 对请求参数的统一处理。
<?php
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$name = $_GET['name'] ?? '';
$name = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
if ($id === false || $id === null) {
http_response_code(400);
exit('参数 id 需要是整数');
}
echo '当前ID:' . $id;
echo '名称:' . $name;
?>
不过 $_GET 只能用在 Web 请求环境,如果脚本运行在命令行,或者需要解析外部传入的完整 URL,就必须回到 parse_url 加 parse_str 的组合。另一个常见的做法是从 $_SERVER 中读取原始请求地址,再提取查询串。比如 $_SERVER['REQUEST_URI'] 会包含路径和查询参数,$_SERVER['QUERY_STRING'] 则直接保存原始查询串。
获取当前完整 URL 时,通常要结合 HTTPS、HTTP_HOST 和 REQUEST_URI 来拼接。这里不能盲目信任 HTTP_HOST,因为它来自客户端请求头,可能被伪造。输出到页面时也要对参数值做 htmlspecialchars 转义,避免 XSS 攻击。对于数字类型参数,使用 filter_input 或 filter_var 做严格校验,是最基本的防线。
四、完整示例:从URL中移除参数并追加签名
在实际业务中,经常需要先解析 URL,再修改查询参数,最后重新拼接成新的链接。典型需求包括移除 debug、ref 等敏感参数,或者追加一个 token 签名。下面这个函数展示了如何把 parse_url、parse_str 和 http_build_query 串联起来完成任务。
<?php
function transformUrl(string $url, array $removeKeys = []): string {
if (strpos($url, '//') === 0) {
$url = 'http:' . $url;
}
$parts = parse_url($url);
if ($parts === false || !isset($parts['scheme'], $parts['host'])) {
return '';
}
$query = $parts['query'] ?? '';
parse_str($query, $params);
foreach ($removeKeys as $key) {
unset($params[$key]);
}
$params['token'] = substr(hash_hmac('sha256', http_build_query($params), 'secret'), 0, 16);
$parts['query'] = http_build_query($params);
$newUrl = $parts['scheme'] . '://';
if (isset($parts['user'])) {
$newUrl .= $parts['user'];
if (isset($parts['pass'])) {
$newUrl .= ':' . $parts['pass'];
}
$newUrl .= '@';
}
$newUrl .= $parts['host'];
if (isset($parts['port'])) {
$newUrl .= ':' . $parts['port'];
}
if (isset($parts['path'])) {
$newUrl .= $parts['path'];
}
$newUrl .= '?' . $parts['query'];
if (isset($parts['fragment'])) {
$newUrl .= '#' . $parts['fragment'];
}
return $newUrl;
}
echo transformUrl('https://ipipp.com/path?user=1&ref=ad&debug=1', ['ref', 'debug']);
?>
这个函数先检查 URL 是否以 // 开头,如果是就补上临时协议再解析,避免相对地址导致 parse_url 返回结果不完整。然后读取 query 并转换成数组,移除不需要的键,追加签名参数,最后用 http_build_query 重新生成查询串。重新拼接时保留了用户信息、端口和锚点,能胜任大多数后台生成链接的场景。
http_build_query 的好处是会自动进行 URL 编码,中文字符会被转换成 %E4%BD%A0%E5%A5%BD 这样的形式,数组参数也会转换成 key[]=value。在生成回调链接或跳转地址时,这种方式比手动拼接更不容易出错。
五、容易忽略的边界与错误处理
解析外部 URL 时不能直接信任输入内容。比如协议部分可能不是 http 或 https,而是 javascript、data 等危险协议。使用 parse_url 之后,应该先检查 scheme 是否在白名单中,再继续业务处理。否则攻击者可能提交一个看似正常的链接,诱导程序跳转到不可控的位置。
<?php
function safeParseUrl(string $url): array {
$scheme = strtolower(parse_url($url, PHP_URL_SCHEME) ?? '');
if (!in_array($scheme, ['http', 'https'], true)) {
return ['error' => '仅支持 http/https 协议'];
}
$parts = parse_url($url);
if ($parts === false) {
return ['error' => 'URL 格式无法解析'];
}
return $parts;
}
?>
另外,parse_url 遇到严重格式错误时会返回 false,例如包含非法端口或换行符的字符串。因此在调用后必须做 false 判断,不能直接把返回值当成数组使用,否则后续的 $parts['query'] 会触发错误。对于 parse_str,空字符串会得到空数组,不会报错,但空查询串与缺失查询串在业务上可能需要区分。
参数编码也是常见坑点。浏览器提交的查询参数中,空格通常会被编码为加号 +,而 parse_str 会自动把加号解码成空格。如果某些接口使用 %20 表示空格,两种形式最终都能得到正确结果。但如果参数值里本身包含加号字符,客户端必须先使用 urlencode 或 rawurlencode 进行编码,否则服务端解析后会丢失加号。处理复杂参数时,建议统一约定使用 rawurlencode 生成查询串,服务端用 rawurldecode 还原,这样空格和处理特殊字符的规则更明确。
最后一个容易出错的地方是数组参数和重复参数。很多框架对 a[]=1&a[]=2 会解析成数组,但如果参数名不带方括号,重复出现时只会保留最后一个值。接口设计时最好明确参数是单值还是多值,多值场景统一使用方括号形式,避免客户端与服务端解析结果不一致。掌握这些边界情况之后,再结合 parse_url、parse_str 以及 $_GET,就能比较全面地处理 PHP 中的 URL 解析和参数获取需求。