Passed
Push — 8.0 ( 88b01c...7e38fd )
by liu
02:26
created

Url::parseUrl()   B

Complexity

Conditions 9
Paths 11

Size

Total Lines 33
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 9
eloc 23
c 4
b 0
f 0
nc 11
nop 2
dl 0
loc 33
ccs 0
cts 23
cp 0
crap 90
rs 8.0555
1
<?php
2
// +----------------------------------------------------------------------
3
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
4
// +----------------------------------------------------------------------
5
// | Copyright (c) 2006~2023 http://thinkphp.cn All rights reserved.
6
// +----------------------------------------------------------------------
7
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
8
// +----------------------------------------------------------------------
9
// | Author: liu21st <[email protected]>
10
// +----------------------------------------------------------------------
11
declare(strict_types=1);
12
13
namespace think\route;
14
15
use think\App;
16
use think\Route;
17
18
/**
19
 * 路由地址生成
20
 */
21
class Url
22
{
23
    /**
24
     * URL 根地址
25
     * @var string
26
     */
27
    protected $root = '';
28
29
    /**
30
     * HTTPS
31
     * @var bool
32
     */
33
    protected $https;
34
35
    /**
36
     * URL后缀
37
     * @var string|bool
38
     */
39
    protected $suffix = true;
40
41
    /**
42
     * URL域名
43
     * @var string|bool
44
     */
45
    protected $domain = false;
46
47
    /**
48
     * 架构函数
49
     * @access public
50
     * @param  Route  $route 路由对象
51
     * @param  App    $app App对象
52
     * @param  string $url URL地址
53
     * @param  array  $vars 参数
54
     */
55
    public function __construct(protected Route $route, protected App $app, protected string $url = '', protected array $vars = [])
56
    {
57
    }
58
59
    /**
60
     * 设置URL参数
61
     * @access public
62
     * @param  array $vars URL参数
63
     * @return $this
64
     */
65
    public function vars(array $vars = [])
66
    {
67
        $this->vars = $vars;
68
        return $this;
69
    }
70
71
    /**
72
     * 设置URL后缀
73
     * @access public
74
     * @param  string|bool $suffix URL后缀
75
     * @return $this
76
     */
77
    public function suffix(string|bool $suffix)
78
    {
79
        $this->suffix = $suffix;
80
        return $this;
81
    }
82
83
    /**
84
     * 设置URL域名(或者子域名)
85
     * @access public
86
     * @param  string|bool $domain URL域名
87
     * @return $this
88
     */
89
    public function domain(string|bool $domain)
90
    {
91
        $this->domain = $domain;
92
        return $this;
93
    }
94
95
    /**
96
     * 设置URL 根地址
97
     * @access public
98
     * @param  string $root URL root
99
     * @return $this
100
     */
101
    public function root(string $root)
102
    {
103
        $this->root = $root;
104
        return $this;
105
    }
106
107
    /**
108
     * 设置是否使用HTTPS
109
     * @access public
110
     * @param  bool $https
111
     * @return $this
112
     */
113
    public function https(bool $https = true)
114
    {
115
        $this->https = $https;
116
        return $this;
117
    }
118
119
    /**
120
     * 检测域名
121
     * @access protected
122
     * @param  string      $url URL
123
     * @param  string|true $domain 域名
124
     * @return string
125
     */
126
    protected function parseDomain(string &$url, string|bool $domain): string
127
    {
128
        if (!$domain) {
129
            return '';
130
        }
131
132
        $request    = $this->app->request;
133
        $rootDomain = $request->rootDomain();
134
135
        if (true === $domain) {
136
            // 自动判断域名
137
            $domain  = $request->host();
138
            $domains = $this->route->getDomains();
139
140
            if (!empty($domains)) {
141
                $routeDomain = array_keys($domains);
142
                foreach ($routeDomain as $domainPrefix) {
143
                    if (str_starts_with($domainPrefix, '*.') && str_contains($domain, ltrim($domainPrefix, '*.')) !== false) {
144
                        foreach ($domains as $key => $rule) {
145
                            $rule = is_array($rule) ? $rule[0] : $rule;
146
                            if (is_string($rule) && !str_contains($key, '*') && str_starts_with($url, $rule)) {
147
                                $url    = ltrim($url, $rule);
148
                                $domain = $key;
149
150
                                // 生成对应子域名
151
                                if (!empty($rootDomain)) {
152
                                    $domain .= $rootDomain;
153
                                }
154
                                break;
155
                            } elseif (str_contains($key, '*')) {
156
                                if (!empty($rootDomain)) {
157
                                    $domain .= $rootDomain;
158
                                }
159
160
                                break;
161
                            }
162
                        }
163
                    }
164
                }
165
            }
166
        } elseif (!str_contains($domain, '.') && !str_starts_with($domain, $rootDomain)) {
167
            $domain .= '.' . $rootDomain;
168
        }
169
170
        if (str_contains($domain, '://')) {
171
            $scheme = '';
172
        } else {
173
            $scheme = $this->https || $request->isSsl() ? 'https://' : 'http://';
174
        }
175
176
        return $scheme . $domain;
177
    }
178
179
    /**
180
     * 解析URL后缀
181
     * @access protected
182
     * @param  string|bool $suffix 后缀
183
     * @return string
184
     */
185
    protected function parseSuffix(string|bool $suffix): string
186
    {
187
        if ($suffix) {
188
            $suffix = true === $suffix ? $this->route->config('url_html_suffix') : $suffix;
189
190
            if (is_string($suffix) && $pos = strpos($suffix, '|')) {
191
                $suffix = substr($suffix, 0, $pos);
192
            }
193
        }
194
195
        return (empty($suffix) || str_starts_with($suffix, '.')) ? (string) $suffix : '.' . $suffix;
196
    }
197
198
    /**
199
     * 直接解析URL地址
200
     * @access protected
201
     * @param  string      $url URL
202
     * @param  string|bool $domain Domain
203
     * @return string
204
     */
205
    protected function parseUrl(string $url, string | bool &$domain): string
206
    {
207
        $request = $this->app->request;
208
209
        if (str_starts_with($url, '/')) {
210
            // 直接作为路由地址解析
211
            $url = substr($url, 1);
212
        } elseif (str_contains($url, '\\')) {
213
            // 解析到类
214
            $url = ltrim(str_replace('\\', '/', $url), '/');
215
        } elseif (str_starts_with($url, '@')) {
216
            // 解析到控制器
217
            $url = substr($url, 1);
218
        } elseif ('' === $url) {
219
            $url  = $request->controller() . '/' . $request->action();
220
            $auto = $this->route->getName('__think_auto_route__');
221
            if (!empty($auto)) {
222
                $url = $request->layer() . '/' . $url;
223
            }
224
        } else {
225
            $controller = $request->controller();
226
            $path       = explode('/', $url);
227
            $action     = array_pop($path);
228
            $controller = empty($path) ? $controller : array_pop($path);
229
            $url        = $controller . '/' . $action;
230
            $auto       = $this->route->getName('__think_auto_route__');
231
            if (!empty($auto)) {
232
                $module = empty($path) ? $request->layer() : array_pop($path);
233
                $url    = $module . '/' . $url;
234
            }
235
        }
236
237
        return $url;
238
    }
239
240
    /**
241
     * 分析路由规则中的变量
242
     * @access protected
243
     * @param  string $rule 路由规则
244
     * @return array
245
     */
246
    protected function parseVar(string $rule): array
247
    {
248
        // 提取路由规则中的变量
249
        $var = [];
250
251
        if (preg_match_all('/<\w+\??>/', $rule, $matches)) {
252
            foreach ($matches[0] as $name) {
253
                $optional = false;
254
255
                if (str_contains($name, '?')) {
256
                    $name     = substr($name, 1, -2);
257
                    $optional = true;
258
                } else {
259
                    $name = substr($name, 1, -1);
260
                }
261
262
                $var[$name] = $optional ? 2 : 1;
263
            }
264
        }
265
266
        return $var;
267
    }
268
269
    /**
270
     * 匹配路由地址
271
     * @access protected
272
     * @param  array $rule 路由规则
273
     * @param  array $vars 路由变量
274
     * @param  string|bool $allowDomain 允许域名
275
     * @return array
276
     */
277
    protected function getRuleUrl(array $rule, array &$vars = [], string|bool $allowDomain = ''): array
278
    {
279
        $request = $this->app->request;
280
        if (is_string($allowDomain) && !str_contains($allowDomain, '.')) {
281
            $allowDomain .= '.' . $request->rootDomain();
282
        }
283
        $port = $request->port();
284
285
        foreach ($rule as $item) {
286
            $url     = $item['rule'];
287
            $pattern = $this->parseVar($url);
288
            $domain  = $item['domain'];
289
            $suffix  = $item['suffix'];
290
291
            if ('-' == $domain) {
292
                $domain = is_string($allowDomain) ? $allowDomain : $request->host(true);
293
            }
294
295
            if (is_string($allowDomain) && $domain != $allowDomain) {
296
                continue;
297
            }
298
299
            if ($port && !in_array($port, [80, 443])) {
300
                $domain .= ':' . $port;
301
            }
302
303
            if (empty($pattern)) {
304
                return [rtrim($url, '?-'), $domain, $suffix];
305
            }
306
307
            $type = $this->route->config('url_common_param');
308
            $keys = [];
309
310
            foreach ($pattern as $key => $val) {
311
                if (isset($vars[$key])) {
312
                    $url    = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key, '<' . $key . '>'], $type ? (string) $vars[$key] : urlencode((string) $vars[$key]), $url);
313
                    $keys[] = $key;
314
                    $url    = str_replace(['/?', '-?'], ['/', '-'], $url);
315
                    $result = [rtrim($url, '?-'), $domain, $suffix];
316
                } elseif (2 == $val) {
317
                    $url    = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url);
318
                    $url    = str_replace(['/?', '-?'], ['/', '-'], $url);
319
                    $result = [rtrim($url, '?-'), $domain, $suffix];
320
                } else {
321
                    $result = null;
322
                    $keys   = [];
323
                    break;
324
                }
325
            }
326
327
            $vars = array_diff_key($vars, array_flip($keys));
328
329
            if (isset($result)) {
330
                return $result;
331
            }
332
        }
333
334
        return [];
335
    }
336
337
    /**
338
     * 生成URL地址
339
     * @access public
340
     * @return string
341
     */
342
    public function build(): string
343
    {
344
        // 解析URL
345
        $url     = $this->url;
346
        $suffix  = $this->suffix;
347
        $domain  = $this->domain;
348
        $request = $this->app->request;
349
        $vars    = $this->vars;
350
351
        if (str_starts_with($url, '[') && $pos = strpos($url, ']')) {
352
            // [name] 表示使用路由命名标识生成URL
353
            $name = substr($url, 1, $pos - 1);
354
            $url  = 'name' . substr($url, $pos + 1);
355
        }
356
357
        if (!str_contains($url, '://') && !str_starts_with($url, '/')) {
358
            $info = parse_url($url);
359
            $url  = !empty($info['path']) ? $info['path'] : '';
360
361
            if (isset($info['fragment'])) {
362
                // 解析锚点
363
                $anchor = $info['fragment'];
364
365
                if (str_contains($anchor, '?')) {
366
                    // 解析参数
367
                    [$anchor, $info['query']] = explode('?', $anchor, 2);
368
                }
369
370
                if (str_contains($anchor, '@')) {
371
                    // 解析域名
372
                    [$anchor, $domain] = explode('@', $anchor, 2);
373
                }
374
            } elseif (str_contains($url, '@') && !str_contains($url, '\\')) {
375
                // 解析域名
376
                [$url, $domain] = explode('@', $url, 2);
377
            }
378
        }
379
380
        if ($url) {
381
            $checkName   = isset($name) ? $name : $url . (isset($info['query']) ? '?' . $info['query'] : '');
382
            $checkDomain = $domain && is_string($domain) ? $domain : null;
383
384
            $rule = $this->route->getName($checkName, $checkDomain);
385
386
            if (empty($rule) && isset($info['query'])) {
387
                $rule = $this->route->getName($url, $checkDomain);
388
                // 解析地址里面参数 合并到vars
389
                parse_str($info['query'], $params);
390
                $vars = array_merge($params, $vars);
391
                unset($info['query']);
392
            }
393
        }
394
395
        if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) {
396
            // 匹配路由命名标识
397
            $url = $match[0];
398
399
            if ($domain && !empty($match[1])) {
400
                $domain = $match[1];
401
            }
402
403
            if (!is_null($match[2])) {
404
                $suffix = $match[2];
405
            }
406
        } elseif (!empty($rule) && isset($name)) {
407
            throw new \InvalidArgumentException('route name not exists:' . $name);
408
        } else {
409
            // 检测URL绑定
410
            $bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
411
412
            if ($bind && str_starts_with($url, $bind)) {
413
                $url = substr($url, strlen($bind) + 1);
414
            }
415
416
            // 路由标识不存在 直接解析
417
            $url = $this->parseUrl($url, $domain);
418
419
            if (isset($info['query'])) {
420
                // 解析地址里面参数 合并到vars
421
                parse_str($info['query'], $params);
422
                $vars = array_merge($params, $vars);
423
            }
424
        }
425
426
        // 还原URL分隔符
427
        $depr = $this->route->config('pathinfo_depr');
428
        $url  = str_replace('/', $depr, $url);
429
430
        $file = $request->baseFile();
431
        if ($file && !str_starts_with($request->url(), $file)) {
432
            $file = str_replace('\\', '/', dirname($file));
433
        }
434
435
        $url = rtrim($file, '/') . '/' . $url;
436
437
        // URL后缀
438
        if (str_ends_with($url, '/') || '' == $url) {
439
            $suffix = '';
440
        } else {
441
            $suffix = $this->parseSuffix($suffix);
442
        }
443
444
        // 锚点
445
        $anchor = !empty($anchor) ? '#' . $anchor : '';
446
447
        // 参数组装
448
        if (!empty($vars)) {
449
            // 添加参数
450
            if ($this->route->config('url_common_param')) {
451
                $vars = http_build_query($vars);
452
                $url .= $suffix . ($vars ? '?' . $vars : '') . $anchor;
453
            } else {
454
                foreach ($vars as $var => $val) {
455
                    $val = (string) $val;
456
                    if ('' !== $val) {
457
                        $url .= $depr . $var . $depr . urlencode($val);
458
                    }
459
                }
460
461
                $url .= $suffix . $anchor;
462
            }
463
        } else {
464
            $url .= $suffix . $anchor;
465
        }
466
467
        // 检测域名
468
        $domain = $this->parseDomain($url, $domain);
469
470
        // URL组装
471
        return $domain . rtrim($this->root, '/') . '/' . ltrim($url, '/');
472
    }
473
474
    public function __toString()
475
    {
476
        return $this->build();
477
    }
478
479
    public function __debugInfo()
480
    {
481
        return [
482
            'url'    => $this->url,
483
            'vars'   => $this->vars,
484
            'suffix' => $this->suffix,
485
            'domain' => $this->domain,
486
        ];
487
    }
488
}
489