Files
YwxAppThink/ywxapp/utils/UrlBuild.php
T

312 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace ywxapp\utils;
use think\App;
use think\route\Url;
/**
* 多根模式 URL 生成
*
* 用法差异(对比 think\app\Url):
* - url('app2:manage/user/read') → 跨应用跳转 /app2/manage/user/read
* - url('manage/user/read') → 当前应用内(自动加 prefix:name 前缀)
* - url('app:admin/user/list') → 强制 /app/admin/user/list
* - url('app2:manage\Blog::read') → 类解析 +跨应用
* - url('[blog_read]') → 仅当前应用(官方限制)
*/
class UrlBuild extends Url
{
/** prefix:name之间的分隔符 */
protected string $prefixSep = ':';
// ============================================================
// 1. 工具方法:prefix解析与反推
// ============================================================
/**
* 从 namespace 反推当前应用的 prefix
* app\manage → 'app'
* app2\manage → 'app2'
*/
protected function getCurrentPrefix(): string
{
$ns = trim($this->app->getNamespace(), '\\');
if ($ns === '') {
return '';
}
$root = explode('\\', $ns)[0] ?? '';
$namespaces = $this->app->config->get('app.app_namespaces', []);
foreach ($namespaces as $key => $nsPrefix) {
if ($root === trim($nsPrefix, '\\')) {
return $key;
}
}
return '';
}
/**
* 获取当前应用的完整名(prefix:name
*/
protected function getCurrentAppName(): string
{
$name = $this->app->http->getName();
$prefix = $this->getCurrentPrefix();
return $prefix !== '' ? ($prefix . $this->prefixSep . $name) : $name;
}
/**
* 拆分 URL 字符串中的 prefix:name/...
* "app2:manage/user/read" → ["app2", "manage/user/read"]
* "manage/user/read" → ["", "manage/user/read"]
* "blog/list" → ["", "blog/list"]
*/
protected function splitPrefix(string $url): array
{
if (!str_contains($url, $this->prefixSep)) {
return ['', $url];
}
$firstSlash = strpos($url, '/');
$firstColon = strpos($url, $this->prefixSep);
// colon 必须在第一段(/之前)
if ($firstSlash === false || $firstColon < $firstSlash) {
return [
substr($url, 0, $firstColon),
substr($url, $firstColon + 1),
];
}
return ['', $url];
}
/**
* 覆盖父类 getAppName(用于 build() 内部拼接)
*官方 think\app\Url 的实现会套 app_map 反向替换 key
* 多根模式: 直接返回 prefix:name 完整形式,反向替换由 parseUrl 自己处理
*/
protected function getAppName(): string
{
return $this->getCurrentAppName();
}
// ============================================================
// 2. 核心:parseUrl 重写
// ============================================================
/**
* 直接解析 URL 地址
*/
protected function parseUrl(string $url, string|bool &$domain): string
{
$request = $this->app->request;
// (1) 以 / 开头 → 直接作为路由地址
if (str_starts_with($url, '/')) {
return substr($url, 1);
}
// (2) 包含 \\ → 解析到类
// 支持: app2:manage\Blog\Article::read
if (str_contains($url, '\\')) {
return ltrim(str_replace('\\', '/', $url), '/');
}
// (3) 以 @ 开头 → 解析到控制器
if (str_starts_with($url, '@')) {
return substr($url, 1);
}
// (4) 空 URL → 当前 controller/action
if ($url === '') {
$url = $request->controller() . '/' . $request->action();
if (!$this->app->http->isBind()) {
$url = $this->getAppName() . '/' . $url;
}
return $url;
}
// ====== 多根模式特有逻辑 ======
// 拆分 prefix:name
[$prefix, $rest] = $this->splitPrefix($url);
$effectiveUrl = $prefix !== '' ? $rest : $url;
// 拆分 controller/action/app
$controller = $request->controller();
$path = explode('/', $effectiveUrl);
$action = array_pop($path);
$controller = empty($path) ? $controller : array_pop($path);
// 应用名
if ($prefix !== '') {
// 跨应用: prefix 已指定,path 最后一段就是应用名
$appName = empty($path) ? $this->getCurrentAppName() : array_pop($path);
} else {
// 当前应用内
$appName = empty($path) ? $this->getCurrentAppName() : array_pop($path);
}
$url = $controller . '/' . $action;
// ====== 域名绑定处理 ======
$bind = $this->app->config->get('app.domain_bind', []);
if ($prefix !== '') {
// 跨应用跳转 → 找目标应用绑定的域名
$targetApp = $prefix . $this->prefixSep . $appName;
if ($key = array_search($targetApp, $bind)) {
// 用户没显式传 domain 时,才用绑定域名
$domain = $domain ?: $key;
}
} elseif (!$this->app->http->isBind()) {
// 当前应用内 → 检查当前应用是否绑定域名
$currentApp = $this->app->http->getName();
if ($key = array_search($currentApp, $bind)) {
// 当前域名就是绑定的 → 强制使用
if (isset($bind[$_SERVER['SERVER_NAME'] ?? ''])) {
$domain = $_SERVER['SERVER_NAME'];
}
$domain = is_bool($domain) ? $key : $domain;
} else {
// 非绑定模式,处理 app_map
$map = $this->app->config->get('app.app_map', []);
if ($key = array_search($appName, $map)) {
// 反向映射: app_name → URL段
$url = $key . '/' . $url;
} else {
$url = $appName . '/' . $url;
}
}
}
return $url;
}
// ============================================================
// 3. build() 重写:处理跨应用域名拼接
// ============================================================
public function build(): string
{
$url = $this->url;
$suffix = $this->suffix;
$domain = $this->domain;
$request = $this->app->request;
$vars = $this->vars;
// [name] 路由名
if (str_starts_with($url, '[') && $pos = strpos($url, ']')) {
$name = substr($url, 1, $pos - 1);
$url = 'name' . substr($url, $pos + 1);
}
if (!str_contains($url, '://') && !str_starts_with($url, '/')) {
$info = parse_url($url);
$url = !empty($info['path']) ? $info['path'] : '';
if (isset($info['fragment'])) {
$anchor = $info['fragment'];
if (str_contains($anchor, '?')) {
[$anchor, $info['query']] = explode('?', $anchor, 2);
}
if (str_contains($anchor, '@')) {
[$anchor, $domain] = explode('@', $anchor, 2);
}
} elseif (str_contains($url, '@') && !str_contains($url, '\\')) {
[$url, $domain] = explode('@', $url, 2);
}
}
if ($url) {
$checkName = $name ?? $url . (isset($info['query']) ? '?' . $info['query'] : '');
$checkDomain = $domain && is_string($domain) ? $domain : null;
$rule = $this->route->getName($checkName, $checkDomain);
if (empty($rule) && isset($info['query'])) {
$rule = $this->route->getName($url, $checkDomain);
parse_str($info['query'], $params);
$vars = array_merge($params, $vars);
unset($info['query']);
}
}
if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) {
// 路由名命中
$url = $match[0];
if ($domain && !empty($match[1])) {
$domain = $match[1];
}
if (!is_null($match[2])) {
$suffix = $match[2];
}
// 未绑定域名 → 加当前应用前缀
if (!$this->app->http->isBind()) {
$url = $this->getAppName() . '/' . $url;
}
} elseif (!empty($rule) && isset($name)) {
throw new \InvalidArgumentException('route name not exists:' . $name);
} else {
// URL 绑定
$bind = (string) $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
if ($bind && str_starts_with($url, $bind)) {
$url = substr($url, strlen($bind) + 1);
}
$url = $this->parseUrl($url, $domain);
if (isset($info['query'])) {
parse_str($info['query'], $params);
$vars = array_merge($params, $vars);
}
}
// 还原分隔符
$depr = $this->route->config('pathinfo_depr');
$url = str_replace('/', $depr, $url);
$file = $request->baseFile();
if ($file && !str_starts_with($request->url(), $file)) {
$file = str_replace('\\', '/', dirname($file));
}
$url = rtrim($file, '/') . '/' . $url;
// 后缀
if (str_ends_with($url, '/') || '' == $url) {
$suffix = '';
} else {
$suffix = $this->parseSuffix($suffix);
}
// 锚点
$anchor = !empty($anchor) ? '#' . $anchor : '';
// 参数
if (!empty($vars)) {
if ($this->route->config('url_common_param')) {
$vars = http_build_query($vars);
$url .= $suffix . ($vars ? '?' . $vars : '') . $anchor;
} else {
foreach ($vars as $var => $val) {
$val = (string) $val;
if ('' !== $val) {
$url .= $depr . $var . $depr . urlencode($val);
}
}
$url .= $suffix . $anchor;
}
} else {
$url .= $suffix . $anchor;
}
// 域名(★ 多根模式下,parseDomain 会按当前域名处理)
$domain = $this->parseDomain($url, $domain);
return $domain . rtrim($this->root, '/') . '/' . ltrim($url, '/');
}
/**
* parseDomain 重写:跨应用跳转时,使用 parseUrl 已选定的 $domain
*官方实现依赖 Route::getDomains,多根场景下不够灵活
*/
protected function parseDomain(string &$url, string|bool $domain): string
{
// parseUrl 已经根据 domain_bind 选好了 $domain,这里直接信任
return parent::parseDomain($url, $domain);
}
}