Passed
Push — 8.0 ( 279f5c...cd7e8a )
by liu
13:53
created

RuleGroup::setFullName()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 7.3329

Importance

Changes 0
Metric Value
cc 6
eloc 8
c 0
b 0
f 0
nc 12
nop 0
dl 0
loc 14
ccs 2
cts 3
cp 0.6667
crap 7.3329
rs 9.2222
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 Closure;
16
use think\Container;
17
use think\Exception;
18
use think\helper\Str;
19
use think\Request;
20
use think\Route;
21
22
/**
23
 * 路由分组类
24
 */
25
class RuleGroup extends Rule
26
{
27
    /**
28
     * 分组路由(包括子分组)
29
     * @var Rule[]
30
     */
31
    protected $rules = [];
32
33
    /**
34
     * MISS路由
35
     * @var RuleItem
36
     */
37
    protected $miss;
38
39
    /**
40
     * 完整名称
41
     * @var string
42
     */
43
    protected $fullName;
44
45
    /**
46
     * 分组别名
47
     * @var string
48
     */
49
    protected $alias;
50
51
    /**
52
     * 分组绑定
53
     * @var string
54
     */
55
    protected $bind;
56
57
    /**
58
     * 是否已经解析
59
     * @var bool
60
     */
61
    protected $hasParsed;
62
63
    /**
64
     * 架构函数
65
     * @access public
66
     * @param  Route     $router 路由对象
67
     * @param  RuleGroup $parent 上级对象
68
     * @param  string    $name   分组名称
69
     * @param  mixed     $rule   分组路由
70
     * @param  bool      $lazy   延迟解析
71
     */
72
    public function __construct(Route $router, ?RuleGroup $parent = null, string $name = '', $rule = null, bool $lazy = false)
73
    {
74
        $this->router = $router;
75
        $this->parent = $parent;
76
        $this->rule   = $rule;
77
        $this->name   = trim($name, '/');
78
79
        $this->setFullName();
80 6
81
        if ($this->parent) {
82 6
            $this->domain = $this->parent->getDomain();
83 6
            $this->parent->addRuleItem($this);
84 6
        }
85 6
86
        if (!$lazy) {
87 6
            $this->parseGroupRule($rule);
88
        }
89 6
    }
90 3
91 3
    /**
92
     * 设置分组的路由规则
93
     * @access public
94 6
     * @return void
95 6
     */
96
    protected function setFullName(): void
97
    {
98
        if (str_contains($this->name, ':')) {
99
            $this->name = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $this->name);
100
        }
101
102
        if ($this->parent && $this->parent->getFullName()) {
103
            $this->fullName = $this->parent->getFullName() . ($this->name ? '/' . $this->name : '');
104 6
        } else {
105
            $this->fullName = $this->name;
106 6
        }
107
108
        if ($this->name) {
109
            $this->router->getRuleName()->setGroup($this->name, $this);
110 6
        }
111
    }
112
113 6
    /**
114
     * 获取所属域名
115
     * @access public
116 6
     * @return string
117
     */
118
    public function getDomain(): string
119
    {
120
        return $this->domain ?: '-';
121
    }
122
123
    /**
124
     * 获取分组别名
125
     * @access public
126 15
     * @return string
127
     */
128 15
    public function getAlias(): string
129
    {
130
        return $this->alias ?: '';
131
    }
132
133
    /**
134
     * 检测分组路由
135
     * @access public
136
     * @param  Request $request       请求对象
137
     * @param  string  $url           访问地址
138
     * @param  bool    $completeMatch 路由是否完全匹配
139
     * @return Dispatch|false
140
     */
141
    public function check(Request $request, string $url, bool $completeMatch = false)
142
    {
143
        // 检查分组有效性
144
        if (!$this->checkOption($this->option, $request) || !$this->checkUrl($url)) {
145
            return false;
146
        }
147
148
        // 解析分组路由
149 27
        if (!$this->hasParsed) {
150
            $this->parseGroupRule($this->rule);
151
        }
152 27
153
        // 获取当前路由规则
154
        $method = strtolower($request->method());
155
        $rules  = $this->getRules($method);
156
        $option = $this->getOption();
157 27
158 24
        if (isset($option['complete_match'])) {
159
            $completeMatch = $option['complete_match'];
160
        }
161
162 27
        if (!empty($option['merge_rule_regex'])) {
163 27
            // 合并路由正则规则进行路由匹配检查
164 27
            $result = $this->checkMergeRuleRegex($request, $rules, $url, $completeMatch);
165
166 27
            if (false !== $result) {
167
                return $result;
168
            }
169
        }
170 27
171
        // 检查分组路由
172
        foreach ($rules as $item) {
173
            $result = $item->check($request, $url, $completeMatch);
174
175
            if (false !== $result) {
176
                return $result;
177
            }
178
        }
179
180 27
        if ($this->bind) {
181 24
            // 检查分组绑定
182
            return $this->checkBind($request, $url, $option);
183 24
        }
184 24
185
        if ($miss = $this->getMissRule($method)) {
186
            // MISS路由
187
            return $miss->parseRule($request, '', $miss->getRoute(), $url, $miss->getOption());
188 6
        }
189
190
        return false;
191
    }
192
193 6
    /**
194
     * 分组URL匹配检查
195
     * @access protected
196
     * @param  string $url URL
197
     * @return bool
198
     */
199
    protected function checkUrl(string $url): bool
200 6
    {
201
        if ($this->fullName) {
202 3
            $pos = strpos($this->fullName, '<');
203
204
            if (false !== $pos) {
205 3
                $str = substr($this->fullName, 0, $pos);
206
            } else {
207
                $str = $this->fullName;
208
            }
209
210
            if ($str && 0 !== stripos(str_replace('|', '/', $url), $str)) {
211
                return false;
212
            }
213
        }
214 27
215
        return true;
216 27
    }
217
218
    /**
219
     * 设置路由分组别名
220
     * @access public
221
     * @param  string $alias 路由分组别名
222
     * @return $this
223
     */
224
    public function alias(string $alias)
225
    {
226
        $this->alias = $alias;
227
        $this->router->getRuleName()->setGroup($alias, $this);
228
229
        return $this;
230 27
    }
231
232
    /**
233
     * 解析分组和域名的路由规则及绑定
234
     * @access public
235
     * @param  mixed $rule 路由规则
236
     * @return void
237
     */
238
    public function parseGroupRule($rule): void
239
    {
240
        if (is_string($rule) && is_subclass_of($rule, Dispatch::class)) {
241
            $this->dispatcher($rule);
242
            return;
243
        }
244
245
        $origin = $this->router->getGroup();
246
        $this->router->setGroup($this);
247
248
        if ($rule instanceof Closure) {
249
            Container::getInstance()->invokeFunction($rule);
250
        } elseif (is_string($rule) && $rule) {
251
            $this->router->bind($rule, $this->domain);
0 ignored issues
show
Bug introduced by
The method bind() does not exist on think\Route. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

251
            $this->router->/** @scrutinizer ignore-call */ 
252
                           bind($rule, $this->domain);
Loading history...
252
        }
253 27
254
        $this->router->setGroup($origin);
255 27
        $this->hasParsed = true;
256
    }
257
258
    /**
259
     * 检测分组路由
260 27
     * @access public
261 27
     * @param  Request $request       请求对象
262
     * @param  array   $rules         路由规则
263 27
     * @param  string  $url           访问地址
264 6
     * @param  bool    $completeMatch 路由是否完全匹配
265 24
     * @return Dispatch|false
266
     */
267
    protected function checkMergeRuleRegex(Request $request, array &$rules, string $url, bool $completeMatch)
268
    {
269 27
        $depr  = $this->config('pathinfo_depr');
270 27
        $url   = $depr . str_replace('|', $depr, $url);
271
        $regex = [];
272
        $items = [];
273
274
        foreach ($rules as $key => $item) {
275
            if ($item instanceof RuleItem) {
276
                $rule = $depr . str_replace('/', $depr, $item->getRule());
277
                if ($depr == $rule && $depr != $url) {
278
                    unset($rules[$key]);
279
                    continue;
280
                }
281
282
                $complete = $item->getOption('complete_match', $completeMatch);
283
284
                if (!str_contains($rule, '<')) {
285
                    if (0 === strcasecmp($rule, $url) || (!$complete && 0 === strncasecmp($rule, $url, strlen($rule)))) {
286
                        return $item->checkRule($request, $url, []);
287
                    }
288
289
                    unset($rules[$key]);
290
                    continue;
291
                }
292
293
                $slash = preg_quote('/-' . $depr, '/');
294
295
                if ($matchRule = preg_split('/[' . $slash . ']<\w+\??>/', $rule, 2)) {
296
                    if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) {
297
                        unset($rules[$key]);
298
                        continue;
299
                    }
300
                }
301
302
                if (preg_match_all('/[' . $slash . ']?<?\w+\??>?/', $rule, $matches)) {
303
                    unset($rules[$key]);
304
                    $pattern = array_merge($this->getPattern(), $item->getPattern());
305
                    $option  = array_merge($this->getOption(), $item->getOption());
306
307
                    $regex[$key] = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $complete, '_THINK_' . $key);
308
                    $items[$key] = $item;
309
                }
310
            }
311
        }
312
313
        if (empty($regex)) {
314
            return false;
315
        }
316
317
        try {
318
            $result = preg_match('~^(?:' . implode('|', $regex) . ')~u', $url, $match);
319
        } catch (\Exception $e) {
320
            throw new Exception('route pattern error');
321
        }
322
323
        if ($result) {
324
            $var = [];
325
            foreach ($match as $key => $val) {
326
                if (is_string($key) && '' !== $val) {
327
                    [$name, $pos] = explode('_THINK_', $key);
328
329
                    $var[$name] = $val;
330
                }
331
            }
332
333
            if (!isset($pos)) {
334
                foreach ($regex as $key => $item) {
335
                    if (str_starts_with(str_replace(['\/', '\-', '\\' . $depr], ['/', '-', $depr], $item), $match[0])) {
336
                        $pos = $key;
337
                        break;
338
                    }
339
                }
340
            }
341
342
            $rule  = $items[$pos]->getRule();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $pos does not seem to be defined for all execution paths leading up to this point.
Loading history...
343
            $array = $this->router->getRule($rule);
344
345
            foreach ($array as $item) {
346
                if (in_array($item->getMethod(), ['*', strtolower($request->method())])) {
347
                    $result = $item->checkRule($request, $url, $var);
348
349
                    if (false !== $result) {
350
                        return $result;
351
                    }
352
                }
353
            }
354
        }
355
356
        return false;
357
    }
358
359
    /**
360
     * 注册MISS路由
361
     * @access public
362
     * @param  string|Closure $route  路由地址
363
     * @param  string         $method 请求类型
364
     * @return RuleItem
365
     */
366
    public function miss(string | Closure $route, string $method = '*'): RuleItem
367
    {
368
        // 创建路由规则实例
369
        $method   = strtolower($method);
370
        $ruleItem = new RuleItem($this->router, $this, null, '', $route, $method);
371
372
        $this->miss[$method] = $ruleItem->setMiss();
373
374
        return $ruleItem;
375
    }
376
377
    /**
378
     * 获取分组下的MISS路由
379
     * @access public
380
     * @param  string $method 请求类型
381
     * @return RuleItem|null
382
     */
383
    public function getMissRule(string $method = '*'): ?RuleItem
384
    {
385
        if (isset($this->miss[$method])) {
386
            $miss = $this->miss[$method];
387
        } elseif (isset($this->miss['*'])) {
388
            $miss = $this->miss['*'];
389
        }
390
        return $miss ?? null;
391
    }
392
393 3
    /**
394
     * 分组绑定 默认绑定到当前分组名所在的控制器分级
395 3
     * 绑定规则 class @controller :namespace /layer
396
     * @access public
397
     * @param  string $bind 绑定资源
398 3
     * @return $this
399 3
     */
400 3
    public function bind(string $bind = '')
401 3
    {
402
        $this->bind = $bind ?: '/' . $this->getFullName();
403
        return $this;
404 3
    }
405
406 3
    /**
407 3
     * 分组绑定到类
408 3
     * @access public
409 3
     * @param  string $class
410 3
     * @return $this
411 3
     */
412 3
    public function class (string $class)
413
    {
414
        $this->bind = '\\' . $class;
415
        return $this;
416
    }
417
418
    /**
419
     * 分组绑定到控制器
420
     * @access public
421
     * @param  string $controller
422 27
     * @return $this
423
     */
424
    public function controller(string $controller)
425 27
    {
426 27
        $this->bind = '@' . $controller;
427
        return $this;
428 27
    }
429
430 27
    /**
431
     * 分组绑定到命名空间
432
     * @access public
433
     * @param  string $namespace
434
     * @return $this
435
     */
436
    public function namespace(string $namespace)
437
    {
438
        $this->bind = ':' . $namespace;
439 6
        return $this;
440
    }
441 6
442 3
    /**
443 3
     * 分组绑定到控制器分级
444
     * @access public
445
     * @param  string $namespace
446 6
     * @return $this
447
     */
448
    public function layer(string $layer)
449
    {
450
        $this->bind = '/' . $layer;
451
        return $this;
452
    }
453
454
    /**
455
     * 检测URL绑定
456
     * @access private
457
     * @param  Request   $request
458
     * @param  string    $url URL地址
459
     * @param  array     $option 分组参数
460
     * @return Dispatch
461
     */
462
    public function checkBind(Request $request, string $url, array $option = []): Dispatch
463
    {
464
        $bind = $this->parseBindAppendParam($this->bind);
465
466
        [$call, $bind] = match (substr($bind, 0, 1)) {
467
            '\\'    => ['bindToClass', substr($bind, 1)],
468
            '@'     => ['bindToController', substr($bind, 1)],
469
            '/'     => ['bindToLayer', substr($bind, 1)],
470
            ':'     => ['bindToNamespace', substr($bind, 1)],
471
            default => ['bindToClass', $bind],
472
        };
473
474
        $groupName = $this->getFullName();
475
        $checkUrl  = trim(substr(str_replace('|', '/', $url), strlen($groupName)), '/');
476
477
        return $this->$call($request, $checkUrl, $bind, $option);
478
    }
479
480
    protected function parseBindAppendParam(string $bind)
481
    {
482
        if (str_contains($bind, '?')) {
483
            [$bind, $query] = explode('?', $bind);
484
            parse_str($query, $vars);
485
            $this->append($vars);
486
        }
487
        return $bind;
488
    }
489
490
    /**
491
     * 绑定到类
492
     * @access protected
493
     * @param  Request   $request
494
     * @param  string    $url URL地址
495
     * @param  string    $class 类名(带命名空间)
496
     * @param  array     $option 分组参数
497
     * @return CallbackDispatch
498
     */
499
    protected function bindToClass(Request $request, string $url, string $class, array $option = []): CallbackDispatch
0 ignored issues
show
Bug introduced by
The type think\route\CallbackDispatch was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
500
    {
501
        $array  = explode('/', $url, 2);
502
        $action = !empty($array[0]) ? $array[0] : $this->config('default_action');
503
        $param  = [];
504
505
        if (!empty($array[1])) {
506
            $this->parseUrlParams($array[1], $param);
507
        }
508
509
        return new CallbackDispatch($request, $this, [$class, $action], $param, $option);
510
    }
511
512
    /**
513
     * 绑定到命名空间
514
     * @access protected
515
     * @param  Request   $request
516
     * @param  string    $url URL地址
517
     * @param  string    $namespace 命名空间
518
     * @param  array     $option 分组参数
519
     * @return CallbackDispatch
520
     */
521
    protected function bindToNamespace(Request $request, string $url, string $namespace, array $option = []): CallbackDispatch
522
    {
523
        $array  = explode('/', $url, 3);
524
        $class  = !empty($array[0]) ? $array[0] : $this->config('default_controller');
525
        $method = !empty($array[1]) ? $array[1] : $this->config('default_action');
526
        $param  = [];
527
528
        if (!empty($array[2])) {
529
            $this->parseUrlParams($array[2], $param);
530
        }
531
532
        return new CallbackDispatch($request, $this, [trim($namespace, '\\') . '\\' . Str::studly($class), $method], $param, $option);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type null; however, parameter $value of think\helper\Str::studly() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

532
        return new CallbackDispatch($request, $this, [trim($namespace, '\\') . '\\' . Str::studly(/** @scrutinizer ignore-type */ $class), $method], $param, $option);
Loading history...
533
    }
534
535
    /**
536
     * 绑定到控制器
537
     * @access protected
538
     * @param  Request   $request
539
     * @param  string    $url URL地址
540
     * @param  string    $controller 控制器名
541
     * @param  array     $option 分组参数
542
     * @return ControllerDispatch
543
     */
544
    protected function bindToController(Request $request, string $url, string $controller, array $option = []): ControllerDispatch
0 ignored issues
show
Bug introduced by
The type think\route\ControllerDispatch was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
545
    {
546
        $array  = explode('/', $url, 2);
547
        $action = !empty($array[0]) ? $array[0] : $this->config('default_action');
548
        $param  = [];
549
550
        if (!empty($array[1])) {
551
            $this->parseUrlParams($array[1], $param);
552
        }
553
554
        return new ControllerDispatch($request, $this, $controller . '/' . $action, $param, $option);
555
    }
556
557
    /**
558
     * 绑定到控制器分级
559
     * @access protected
560
     * @param  Request   $request
561
     * @param  string    $url URL地址
562
     * @param  string    $controller 控制器名
563
     * @param  array     $option 分组参数
564
     * @return ControllerDispatch
565
     */
566
    protected function bindToLayer(Request $request, string $url, string $layer, array $option = []): ControllerDispatch
567
    {
568
        $array      = explode('/', $url, 3);
569
        $controller = !empty($array[0]) ? $array[0] : $this->config('default_controller');
570
        $action     = !empty($array[1]) ? $array[1] : $this->config('default_action');
571
        $param      = [];
572
573
        if (!empty($array[2])) {
574
            $this->parseUrlParams($array[2], $param);
575
        }
576
577
        return new ControllerDispatch($request, $this, $layer . '/' . $controller . '/' . $action, $param, $option);
578
    }
579
580
    /**
581
     * 添加分组下的路由规则
582
     * @access public
583
     * @param  string $rule   路由规则
584
     * @param  mixed  $route  路由地址
585
     * @param  string $method 请求类型
586
     * @return RuleItem
587
     */
588
    public function addRule(string $rule, $route = null, string $method = '*'): RuleItem
589
    {
590
        // 读取路由标识
591
        if (is_string($route)) {
592
            $name = $route;
593
        } else {
594
            $name = null;
595
        }
596
597
        $method = strtolower($method);
598
599
        if ('' === $rule || '/' === $rule) {
600
            $rule .= '$';
601
        }
602
603
        // 创建路由规则实例
604
        $ruleItem = new RuleItem($this->router, $this, $name, $rule, $route, $method);
605
606
        $this->addRuleItem($ruleItem);
607
608 24
        return $ruleItem;
609
    }
610
611 24
    /**
612 9
     * 注册分组下的路由规则
613
     * @access public
614 15
     * @param  Rule   $rule   路由规则
615
     * @return $this
616
     */
617 24
    public function addRuleItem(Rule $rule)
618
    {
619 24
        $this->rules[] = $rule;
620 3
        return $this;
621
    }
622
623
    /**
624 24
     * 设置分组的路由前缀
625
     * @access public
626 24
     * @param  string $prefix 路由前缀
627
     * @return $this
628 24
     */
629
    public function prefix(string $prefix)
630
    {
631
        if ($this->parent && $this->parent->getOption('prefix')) {
632
            $prefix = $this->parent->getOption('prefix') . $prefix;
633
        }
634
635
        return $this->setOption('prefix', $prefix);
636
    }
637 24
638
    /**
639 24
     * 合并分组的路由规则正则
640 24
     * @access public
641
     * @param  bool $merge 是否合并
642
     * @return $this
643
     */
644
    public function mergeRuleRegex(bool $merge = true)
645
    {
646
        return $this->setOption('merge_rule_regex', $merge);
647
    }
648
649
    /**
650
     * 设置分组的Dispatch调度
651
     * @access public
652
     * @param  string $dispatch 调度类
653
     * @return $this
654
     */
655
    public function dispatcher(string $dispatch)
656
    {
657
        return $this->setOption('dispatcher', $dispatch);
658
    }
659
660
    /**
661
     * 获取完整分组Name
662
     * @access public
663
     * @return string
664 6
     */
665
    public function getFullName(): string
666 6
    {
667
        return $this->fullName ?: '';
668
    }
669
670
    /**
671
     * 获取分组的路由规则
672
     * @access public
673
     * @param  string $method 请求类型
674
     * @return array
675
     */
676
    public function getRules(string $method = ''): array
677
    {
678
        if ('' === $method) {
679
            return $this->rules;
680
        }
681
682
        return array_filter($this->rules, function ($item) use ($method) {
683
            $ruleMethod = $item->getMethod();
684
            return '*' == $ruleMethod || str_contains($ruleMethod, $method);
685 27
        });
686
    }
687 27
688
    /**
689
     * 清空分组下的路由规则
690
     * @access public
691
     * @return void
692
     */
693
    public function clear(): void
694
    {
695
        $this->rules = [];
696 27
    }
697
}
698