2022-01-07 14:43:42 +08:00

518 lines
15 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
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2021 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace think\route;
use think\App;
use think\Route;
/**
* 路由地址生成
*/
class Url
{
/**
* 应用对象
* @var App
*/
protected $app;
/**
* 路由对象
* @var Route
*/
protected $route;
/**
* URL变量
* @var array
*/
protected $vars = [];
/**
* 路由URL
* @var string
*/
protected $url;
/**
* URL 根地址
* @var string
*/
protected $root = '';
/**
* HTTPS
* @var bool
*/
protected $https;
/**
* URL后缀
* @var string|bool
*/
protected $suffix = true;
/**
* URL域名
* @var string|bool
*/
protected $domain = false;
/**
* 架构函数
* @access public
* @param string $url URL地址
* @param array $vars 参数
*/
public function __construct(Route $route, App $app, string $url = '', array $vars = [])
{
$this->route = $route;
$this->app = $app;
$this->url = $url;
$this->vars = $vars;
}
/**
* 设置URL参数
* @access public
* @param array $vars URL参数
* @return $this
*/
public function vars(array $vars = [])
{
$this->vars = $vars;
return $this;
}
/**
* 设置URL后缀
* @access public
* @param string|bool $suffix URL后缀
* @return $this
*/
public function suffix($suffix)
{
$this->suffix = $suffix;
return $this;
}
/**
* 设置URL域名或者子域名
* @access public
* @param string|bool $domain URL域名
* @return $this
*/
public function domain($domain)
{
$this->domain = $domain;
return $this;
}
/**
* 设置URL 根地址
* @access public
* @param string $root URL root
* @return $this
*/
public function root(string $root)
{
$this->root = $root;
return $this;
}
/**
* 设置是否使用HTTPS
* @access public
* @param bool $https
* @return $this
*/
public function https(bool $https = true)
{
$this->https = $https;
return $this;
}
/**
* 检测域名
* @access protected
* @param string $url URL
* @param string|true $domain 域名
* @return string
*/
protected function parseDomain(string &$url, $domain): string
{
if (!$domain) {
return '';
}
$request = $this->app->request;
$rootDomain = $request->rootDomain();
if (true === $domain) {
// 自动判断域名
$domain = $request->host();
$domains = $this->route->getDomains();
if (!empty($domains)) {
$routeDomain = array_keys($domains);
foreach ($routeDomain as $domainPrefix) {
if (0 === strpos($domainPrefix, '*.') && strpos($domain, ltrim($domainPrefix, '*.')) !== false) {
foreach ($domains as $key => $rule) {
$rule = is_array($rule) ? $rule[0] : $rule;
if (is_string($rule) && false === strpos($key, '*') && 0 === strpos($url, $rule)) {
$url = ltrim($url, $rule);
$domain = $key;
// 生成对应子域名
if (!empty($rootDomain)) {
$domain .= $rootDomain;
}
break;
} elseif (false !== strpos($key, '*')) {
if (!empty($rootDomain)) {
$domain .= $rootDomain;
}
break;
}
}
}
}
}
} elseif (false === strpos($domain, '.') && 0 !== strpos($domain, $rootDomain)) {
$domain .= '.' . $rootDomain;
}
if (false !== strpos($domain, '://')) {
$scheme = '';
} else {
$scheme = $this->https || $request->isSsl() ? 'https://' : 'http://';
}
return $scheme . $domain;
}
/**
* 解析URL后缀
* @access protected
* @param string|bool $suffix 后缀
* @return string
*/
protected function parseSuffix($suffix): string
{
if ($suffix) {
$suffix = true === $suffix ? $this->route->config('url_html_suffix') : $suffix;
if (is_string($suffix) && $pos = strpos($suffix, '|')) {
$suffix = substr($suffix, 0, $pos);
}
}
return (empty($suffix) || 0 === strpos($suffix, '.')) ? (string) $suffix : '.' . $suffix;
}
/**
* 直接解析URL地址
* @access protected
* @param string $url URL
* @param string|bool $domain Domain
* @return string
*/
protected function parseUrl(string $url, &$domain): string
{
$request = $this->app->request;
if (0 === strpos($url, '/')) {
// 直接作为路由地址解析
$url = substr($url, 1);
} elseif (false !== strpos($url, '\\')) {
// 解析到类
$url = ltrim(str_replace('\\', '/', $url), '/');
} elseif (0 === strpos($url, '@')) {
// 解析到控制器
$url = substr($url, 1);
} elseif ('' === $url) {
$url = $request->controller() . '/' . $request->action();
} else {
$controller = $request->controller();
$path = explode('/', $url);
$action = array_pop($path);
$controller = empty($path) ? $controller : array_pop($path);
$url = $controller . '/' . $action;
}
return $url;
}
/**
* 分析路由规则中的变量
* @access protected
* @param string $rule 路由规则
* @return array
*/
protected function parseVar(string $rule): array
{
// 提取路由规则中的变量
$var = [];
if (preg_match_all('/<\w+\??>/', $rule, $matches)) {
foreach ($matches[0] as $name) {
$optional = false;
if (strpos($name, '?')) {
$name = substr($name, 1, -2);
$optional = true;
} else {
$name = substr($name, 1, -1);
}
$var[$name] = $optional ? 2 : 1;
}
}
return $var;
}
/**
* 匹配路由地址
* @access protected
* @param array $rule 路由规则
* @param array $vars 路由变量
* @param mixed $allowDomain 允许域名
* @return array
*/
protected function getRuleUrl(array $rule, array &$vars = [], $allowDomain = ''): array
{
$request = $this->app->request;
if (is_string($allowDomain) && false === strpos($allowDomain, '.')) {
$allowDomain .= '.' . $request->rootDomain();
}
$port = $request->port();
foreach ($rule as $item) {
$url = $item['rule'];
$pattern = $this->parseVar($url);
$domain = $item['domain'];
$suffix = $item['suffix'];
if ('-' == $domain) {
$domain = is_string($allowDomain) ? $allowDomain : $request->host(true);
}
if (is_string($allowDomain) && $domain != $allowDomain) {
continue;
}
if ($port && !in_array($port, [80, 443])) {
$domain .= ':' . $port;
}
if (empty($pattern)) {
return [rtrim($url, '?-'), $domain, $suffix];
}
$type = $this->route->config('url_common_param');
$keys = [];
foreach ($pattern as $key => $val) {
if (isset($vars[$key])) {
$url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key, '<' . $key . '>'], $type ? (string) $vars[$key] : urlencode((string) $vars[$key]), $url);
$keys[] = $key;
$url = str_replace(['/?', '-?'], ['/', '-'], $url);
$result = [rtrim($url, '?-'), $domain, $suffix];
} elseif (2 == $val) {
$url = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url);
$url = str_replace(['/?', '-?'], ['/', '-'], $url);
$result = [rtrim($url, '?-'), $domain, $suffix];
} else {
$result = null;
$keys = [];
break;
}
}
$vars = array_diff_key($vars, array_flip($keys));
if (isset($result)) {
return $result;
}
}
return [];
}
/**
* 生成URL地址
* @access public
* @return string
*/
public function build()
{
// 解析URL
$url = $this->url;
$suffix = $this->suffix;
$domain = $this->domain;
$request = $this->app->request;
$vars = $this->vars;
if (0 === strpos($url, '[') && $pos = strpos($url, ']')) {
// [name] 表示使用路由命名标识生成URL
$name = substr($url, 1, $pos - 1);
$url = 'name' . substr($url, $pos + 1);
}
if (false === strpos($url, '://') && 0 !== strpos($url, '/')) {
$info = parse_url($url);
$url = !empty($info['path']) ? $info['path'] : '';
if (isset($info['fragment'])) {
// 解析锚点
$anchor = $info['fragment'];
if (false !== strpos($anchor, '?')) {
// 解析参数
[$anchor, $info['query']] = explode('?', $anchor, 2);
}
if (false !== strpos($anchor, '@')) {
// 解析域名
[$anchor, $domain] = explode('@', $anchor, 2);
}
} elseif (strpos($url, '@') && false === strpos($url, '\\')) {
// 解析域名
[$url, $domain] = explode('@', $url, 2);
}
}
if ($url) {
$checkName = isset($name) ? $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);
// 解析地址里面参数 合并到vars
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];
}
} elseif (!empty($rule) && isset($name)) {
throw new \InvalidArgumentException('route name not exists:' . $name);
} else {
// 检测URL绑定
$bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
if ($bind && 0 === strpos($url, $bind)) {
$url = substr($url, strlen($bind) + 1);
} else {
$binds = $this->route->getBind();
foreach ($binds as $key => $val) {
if (is_string($val) && 0 === strpos($url, $val) && substr_count($val, '/') > 1) {
$url = substr($url, strlen($val) + 1);
$domain = $key;
break;
}
}
}
// 路由标识不存在 直接解析
$url = $this->parseUrl($url, $domain);
if (isset($info['query'])) {
// 解析地址里面参数 合并到vars
parse_str($info['query'], $params);
$vars = array_merge($params, $vars);
}
}
// 还原URL分隔符
$depr = $this->route->config('pathinfo_depr');
$url = str_replace('/', $depr, $url);
$file = $request->baseFile();
if ($file && 0 !== strpos($request->url(), $file)) {
$file = str_replace('\\', '/', dirname($file));
}
$url = rtrim($file, '/') . '/' . $url;
// URL后缀
if ('/' == substr($url, -1) || '' == $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;
}
// 检测域名
$domain = $this->parseDomain($url, $domain);
// URL组装
return $domain . rtrim($this->root, '/') . '/' . ltrim($url, '/');
}
public function __toString()
{
return $this->build();
}
public function __debugInfo()
{
return [
'url' => $this->url,
'vars' => $this->vars,
'suffix' => $this->suffix,
'domain' => $this->domain,
];
}
}