chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+312
View File
@@ -0,0 +1,312 @@
<?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);
}
}