URL 處理是 Web 開發高頻需求:解析元件、取得/設定查詢參數、建立重導向 URL、合併多組參數、移除追蹤參數。本文對比原生函數、PSR-7 `UriInterface`、第三方庫 (league/uri) 的適用場景,並提供可重用的 `UrlQuery` 工具類。
| 函數 | 用途 | 回傳 | 注意 |
|---|---|---|---|
parse_url() | 分解 URL 為關聯陣列 | ['scheme','host','path','query','fragment',...] | 不解析 query 字串,僅回傳原始字串 |
parse_str() | 解析 query 字串填入陣列 | void (填入參考變數) | 會覆蓋同名鍵,建議用 `parse_str($q, $arr)` |
http_build_query() | 陣列編碼為 query 字串 | string | 支援巢狀陣列 `foo[bar]=baz`,`PHP_QUERY_RFC3986` 空格編碼為 %20 |
urldecode()/rawurldecode() | 解碼 | string | `raw` 版不將 + 轉空格,符合 RFC 3986 |
<?php
$url = 'https://example.com/path?a=1&b=2&c=3#section';
// 1. 取得查詢參數陣列
parse_str(parse_url($url, PHP_URL_QUERY), $query);
// $query = ['a'=>'1','b'=>'2','c'=>'3']
// 2. 修改/新增參數
$query['b'] = '20';
$query['d'] = '4';
// 3. 重建 URL
$newUrl = parse_url($url, PHP_URL_SCHEME).'://'.parse_url($url, PHP_URL_HOST).
parse_url($url, PHP_URL_PATH).'?'.http_build_query($query).
(parse_url($url, PHP_URL_FRAGMENT) ? '#'.parse_url($url, PHP_URL_FRAGMENT) : '');
// https://example.com/path?a=1&b=20&c=3&d=4#section
// 4. 刪除參數
unset($query['c']);
$cleanUrl = ... // 同理重建
?>
composer require nyholm/psr7
<?php
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\UriInterface;
$factory = new Psr17Factory();
$uri = $factory->createUri('https://example.com/path?a=1&b=2');
// 取得查詢參數 (需自行 parse_str)
parse_str($uri->getQuery(), $query);
// 修改參數 -> 產生新 Uri 物件
$newUri = $uri->withQuery(http_build_query(array_merge($query, ['b' => '20', 'd' => '4'])));
// 刪除參數
$newUri = $uri->withQuery(http_build_query(array_diff_key($query, ['c' => ''])));
// 解析主機/路徑
echo $uri->getHost(); // example.com
echo $uri->getPath(); // /path
?>
<?php
declare(strict_types=1);
namespace App\Http;
class UrlQuery {
private array $params = [];
private string $baseUrl = '';
private string $fragment = '';
public static function fromString(string $url): self {
$self = new self();
$parts = parse_url($url);
$self->baseUrl = ($parts['scheme'] ?? '').'://'.($parts['host'] ?? '').($parts['path'] ?? '');
if (!empty($parts['query'])) parse_str($parts['query'], $self->params);
$self->fragment = $parts['fragment'] ?? '';
return $self;
}
public static function fromCurrentRequest(): self {
$url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http')
.'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
return self::fromString($url);
}
public function get(string $key, mixed $default = null): mixed {
return $this->params[$key] ?? $default;
}
public function set(string $key, mixed $value): self {
$clone = clone $this;
$clone->params[$key] = $value;
return $clone;
}
public function setMultiple(array $data): self {
$clone = clone $this;
$clone->params = array_merge($clone->params, $data);
return $clone;
}
public function remove(string ...$keys): self {
$clone = clone $this;
foreach ($keys as $k) unset($clone->params[$k]);
return $clone;
}
public function removeMatching(string $pattern): self {
$clone = clone $this;
$clone->params = array_filter($clone->params, fn($k) => !preg_match($pattern, $k), ARRAY_FILTER_USE_KEY);
return $clone;
}
public function toString(): string {
$query = http_build_query($this->params, '', '&', PHP_QUERY_RFC3986);
return $this->baseUrl . ($query ? '?'.$query : '') . ($this->fragment ? '#'.$this->fragment : '');
}
public function __toString(): string { return $this->toString(); }
}
// 使用
$url = UrlQuery::fromString('https://shop.com/list?cat=12&page=3&sort=price#top')
->set('page', 5)
->remove('sort')
->removeMatching('/^utm_/'); // 移除追蹤參數
echo $url; // https://shop.com/list?cat=12&page=5#top
?>
parse_str 不支援巢狀陣列 `a[b][c]=1` (會變成 `a_b_c`),需手動解析或用 `league/uri`。http_build_query 預設空格編碼為 `+`,API 簽名需 `PHP_QUERY_RFC3986` 產生 `%20`。