Passed
Push — 8.0 ( fd65e0...efbc40 )
by liu
02:49
created

RuleGroup::check()   B

Complexity

Conditions 11
Paths 33

Size

Total Lines 50
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 13.1488

Importance

Changes 7
Bugs 0 Features 0
Metric Value
cc 11
eloc 23
c 7
b 0
f 0
nc 33
nop 3
dl 0
loc 50
ccs 17
cts 23
cp 0.7391
crap 13.1488
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use think\route\dispatch\Callback as CallbackDispatch;
22
use think\route\dispatch\Controller as ControllerDispatch;
23
24
/**
25
 * 路由分组类
26
 */
27
class RuleGroup extends Rule
28
{
29
    /**
30
     * 分组路由(包括子分组)
31
     * @var Rule[]
32
     */
33
    protected $rules = [];
34
35
    /**
36
     * MISS路由
37
     * @var RuleItem
38
     */
39
    protected $miss;
40
41
    /**
42
     * 完整名称
43
     * @var string
44
     */
45
    protected $fullName;
46
47
    /**
48
     * 分组别名
49
     * @var string
50
     */
51
    protected $alias;
52
53
    /**
54
     * 分组绑定
55
     * @var string
56
     */
57
    protected $bind;
58
59
    /**
60
     * 是否已经解析
61
     * @var bool
62
     */
63
    protected $hasParsed;
64
65
    /**
66
     * 架构函数
67
     * @access public
68
     * @param  Route     $router 路由对象
69
     * @param  RuleGroup $parent 上级对象
70
     * @param  string    $name   分组名称
71
     * @param  mixed     $rule   分组路由
72
     * @param  bool      $lazy   延迟解析
73
     */
74 3
    public function __construct(Route $router, ?RuleGroup $parent = null, string $name = '', $rule = null, bool $lazy = false)
75
    {
76 3
        $this->router = $router;
77 3
        $this->parent = $parent;
78 3
        $this->rule   = $rule;
79 3
        $this->name   = trim($name, '/');
80
81 3
        $this->setFullName();
82
83 3
        if ($this->parent) {
84 3
            $this->domain = $this->parent->getDomain();
85 3
            $this->parent->addRuleItem($this);
86
        }
87
88 3
        if (!$lazy) {
89 3
            $this->parseGroupRule($rule);
90
        }
91
    }
92
93
    /**
94
     * 设置分组的路由规则
95
     * @access public
96
     * @return void
97
     */
98 3
    protected function setFullName(): void
99
    {
100 3
        if (str_contains($this->name, ':')) {
101
            $this->name = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $this->name);
102
        }
103
104 3
        if ($this->parent && $this->parent->getFullName()) {
105
            $this->fullName = $this->parent->getFullName() . ($this->name ? '/' . $this->name : '');
106
        } else {
107 3
            $this->fullName = $this->name;
108
        }
109
110 3
        if ($this->name) {
111
            $this->router->getRuleName()->setGroup($this->name, $this);
112
        }
113
    }
114
115
    /**
116
     * 获取所属域名
117
     * @access public
118
     * @return string
119
     */
120 12
    public function getDomain(): string
121
    {
122 12
        return $this->domain ?: '-';
123
    }
124
125
    /**
126
     * 获取分组别名
127
     * @access public
128
     * @return string
129
     */
130
    public function getAlias(): string
131
    {
132
        return $this->alias ?: '';
133
    }
134
135
    /**
136
     * 检测分组路由
137
     * @access public
138
     * @param  Request $request       请求对象
139
     * @param  string  $url           访问地址
140
     * @param  bool    $completeMatch 路由是否完全匹配
141
     * @return Dispatch|false
142
     */
143 27
    public function check(Request $request, string $url, bool $completeMatch = false)
144
    {
145
        // 检查分组有效性
146 27
        if (!$this->checkOption($this->option, $request) || !$this->checkUrl($url)) {
147
            return false;
148
        }
149
150
        // 解析分组路由
151 27
        if (!$this->hasParsed) {
152 24
            $this->parseGroupRule($this->rule);
153
        }
154
155
        // 获取当前路由规则
156 27
        $method = strtolower($request->method());
157 27
        $rules  = $this->getRules($method);
158 27
        $option = $this->getOption();
159
160 27
        if (isset($option['complete_match'])) {
161
            $completeMatch = $option['complete_match'];
162
        }
163
164 27
        if (!empty($option['merge_rule_regex'])) {
165
            // 路由合并检查
166
            $result = $this->checkMergeRuleRegex($request, $rules, $url, $completeMatch);
167
168
            if (false !== $result) {
169
                return $result;
170
            }
171
        } else {
172
            // 检查分组路由
173 27
            foreach ($rules as $item) {
174 24
                $result = $item->check($request, $url, $completeMatch);
175
176 24
                if (false !== $result) {
177 24
                    return $result;
178
                }
179
            }
180
        }
181
182 6
        if ($this->bind) {
183
            // 检查分组绑定
184
            return $this->checkBind($request, $url, $option);
185
        }
186
187 6
        if ($miss = $this->getMissRule($method)) {
188
            // MISS路由
189 3
            return $miss->parseRule($request, '', $miss->getRoute(), $url, $miss->getOption());
190
        }
191
192 3
        return false;
193
    }
194
195
    /**
196
     * 分组URL匹配检查
197
     * @access protected
198
     * @param  string $url URL
199
     * @return bool
200
     */
201 27
    protected function checkUrl(string $url): bool
202
    {
203 27
        if ($this->fullName) {
204
            $pos = strpos($this->fullName, '<');
205
206
            if (false !== $pos) {
207
                $str = substr($this->fullName, 0, $pos);
208
            } else {
209
                $str = $this->fullName;
210
            }
211
212
            if ($str && 0 !== stripos(str_replace('|', '/', $url), $str)) {
213
                return false;
214
            }
215
        }
216
217 27
        return true;
218
    }
219
220
    /**
221
     * 设置路由分组别名
222
     * @access public
223
     * @param  string $alias 路由分组别名
224
     * @return $this
225
     */
226
    public function alias(string $alias)
227
    {
228
        $this->alias = $alias;
229
        $this->router->getRuleName()->setGroup($alias, $this);
230
231
        return $this;
232
    }
233
234
    /**
235
     * 解析分组和域名的路由规则及绑定
236
     * @access public
237
     * @param  mixed $rule 路由规则
238
     * @return void
239
     */
240 27
    public function parseGroupRule($rule): void
241
    {
242 27
        if (is_string($rule) && is_subclass_of($rule, Dispatch::class)) {
243
            $this->dispatcher($rule);
244
            return;
245
        }
246
247 27
        $origin = $this->router->getGroup();
248 27
        $this->router->setGroup($this);
249
250 27
        if ($rule instanceof Closure) {
251 6
            Container::getInstance()->invokeFunction($rule);
252 24
        } elseif (is_string($rule) && $rule) {
253
            $this->bind($rule);
254
        }
255
256 27
        $this->router->setGroup($origin);
257 27
        $this->hasParsed = true;
258
    }
259
260
    /**
261
     * 检测分组路由
262
     * @access public
263
     * @param  Request $request       请求对象
264
     * @param  array   $rules         路由规则
265
     * @param  string  $url           访问地址
266
     * @param  bool    $completeMatch 路由是否完全匹配
267
     * @return Dispatch|false
268
     */
269
    protected function checkMergeRuleRegex(Request $request, array &$rules, string $url, bool $completeMatch)
270
    {
271
        $depr  = $this->config('pathinfo_depr');
272
        $url   = $depr . str_replace('|', $depr, $url);
273
        $regex = [];
274
        $items = [];
275
276
        foreach ($rules as $key => $item) {
277
            if ($item instanceof RuleItem) {
278
                $rule = $depr . str_replace('/', $depr, $item->getRule());
279
                if ($depr == $rule && $depr != $url) {
280
                    unset($rules[$key]);
281
                    continue;
282
                }
283
284
                $complete = $item->getOption('complete_match', $completeMatch);
285
286
                if (!str_contains($rule, '<')) {
287
                    if (0 === strcasecmp($rule, $url) || (!$complete && 0 === strncasecmp($rule, $url, strlen($rule)))) {
288
                        return $item->checkRule($request, $url, []);
289
                    }
290
291
                    unset($rules[$key]);
292
                    continue;
293
                }
294
295
                $slash = preg_quote('/-' . $depr, '/');
296
297
                if ($matchRule = preg_split('/[' . $slash . ']<\w+\??>/', $rule, 2)) {
298
                    if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) {
299
                        unset($rules[$key]);
300
                        continue;
301
                    }
302
                }
303
304
                if (preg_match_all('/[' . $slash . ']?<?\w+\??>?/', $rule, $matches)) {
305
                    unset($rules[$key]);
306
                    $pattern = array_merge($this->getPattern(), $item->getPattern());
307
                    $option  = array_merge($this->getOption(), $item->getOption());
308
309
                    $regex[$key] = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $complete, '_THINK_' . $key);
310
                    $items[$key] = $item;
311
                }
312
            }
313
        }
314
315
        if (empty($regex)) {
316
            return false;
317
        }
318
319
        try {
320
            $result = preg_match('~^(?:' . implode('|', $regex) . ')~u', $url, $match);
321
        } catch (\Exception $e) {
322
            throw new Exception('route pattern error');
323
        }
324
325
        if ($result) {
326
            $var = [];
327
            foreach ($match as $key => $val) {
328
                if (is_string($key) && '' !== $val) {
329
                    [$name, $pos] = explode('_THINK_', $key);
330
331
                    $var[$name] = $val;
332
                }
333
            }
334
335
            if (!isset($pos)) {
336
                foreach ($regex as $key => $item) {
337
                    if (str_starts_with(str_replace(['\/', '\-', '\\' . $depr], ['/', '-', $depr], $item), $match[0])) {
338
                        $pos = $key;
339
                        break;
340
                    }
341
                }
342
            }
343
344
            $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...
345
            $array = $this->router->getRule($rule);
346
347
            foreach ($array as $item) {
348
                if (in_array($item->getMethod(), ['*', strtolower($request->method())])) {
349
                    $result = $item->checkRule($request, $url, $var);
350
351
                    if (false !== $result) {
352
                        return $result;
353
                    }
354
                }
355
            }
356
        }
357
358
        return false;
359
    }
360
361
    /**
362
     * 注册MISS路由
363
     * @access public
364
     * @param  string|Closure $route  路由地址
365
     * @param  string         $method 请求类型
366
     * @return RuleItem
367
     */
368 27
    public function miss(string | Closure $route, string $method = '*'): RuleItem
369
    {
370
        // 创建路由规则实例
371 27
        $method   = strtolower($method);
372 27
        $ruleItem = new RuleItem($this->router, $this, null, '', $route, $method);
373
374 27
        $this->miss[$method] = $ruleItem->setMiss();
375
376 27
        return $ruleItem;
377
    }
378
379
    /**
380
     * 获取分组下的MISS路由
381
     * @access public
382
     * @param  string $method 请求类型
383
     * @return RuleItem|null
384
     */
385 6
    public function getMissRule(string $method = '*'): ?RuleItem
386
    {
387 6
        if (isset($this->miss[$method])) {
388 3
            $miss = $this->miss[$method];
389 3
        } elseif (isset($this->miss['*'])) {
390
            $miss = $this->miss['*'];
391
        }
392 6
        return $miss ?? null;
393
    }
394
395
    /**
396
     * 分组自动URL调度
397
     * @access public
398
     * @param  string       $bind 绑定资源
399
     * @param  string|array $middleware 中间件
400
     * @return $this
401
     */
402 3
    public function auto(string $bind = '', string | array $middleware = '')
403
    {
404 3
        $this->bind = $bind ?: '/' . $this->getFullName();
405 3
        if ($middleware) {
406
            $this->middleware($middleware);
407
        }
408
409 3
        return $this;
410
    }
411
412
    /**
413
     * 分组绑定 默认绑定到当前分组名所在的控制器分级
414
     * 绑定规则 class @controller :namespace /layer
415
     * @access public
416
     * @param  string $bind 绑定资源
417
     * @return $this
418
     */
419
    public function bind(string $bind)
420
    {
421
        $this->bind = $bind;
422
        return $this;
423
    }
424
425
    /**
426
     * 分组绑定到类
427
     * @access public
428
     * @param  string $class
429
     * @return $this
430
     */
431
    public function class (string $class)
432
    {
433
        $this->bind = '\\' . $class;
434
        return $this;
435
    }
436
437
    /**
438
     * 分组绑定到控制器
439
     * @access public
440
     * @param  string $controller
441
     * @return $this
442
     */
443
    public function controller(string $controller)
444
    {
445
        $this->bind = '@' . $controller;
446
        return $this;
447
    }
448
449
    /**
450
     * 分组绑定到命名空间
451
     * @access public
452
     * @param  string $namespace
453
     * @return $this
454
     */
455
    public function namespace(string $namespace)
456
    {
457
        $this->bind = ':' . $namespace;
458
        return $this;
459
    }
460
461
    /**
462
     * 分组绑定到控制器分级
463
     * @access public
464
     * @param  string $namespace
465
     * @return $this
466
     */
467
    public function layer(string $layer)
468
    {
469
        $this->bind = '/' . $layer;
470
        return $this;
471
    }
472
473
    /**
474
     * 获取分组绑定信息
475
     * @access public
476
     * @return string
477
     */
478
    public function getBind()
479
    {
480
        return $this->bind ?? '';
481
    }
482
483
    /**
484
     * 检测URL绑定
485
     * @access private
486
     * @param  Request   $request
487
     * @param  string    $url URL地址
488
     * @param  array     $option 分组参数
489
     * @return Dispatch
490
     */
491 3
    public function checkBind(Request $request, string $url, array $option = []): Dispatch
492
    {
493 3
        [$bind, $param] = $this->parseBindAppendParam($this->bind);
494
495 3
        [$call, $bind] = match (substr($bind, 0, 1)) {
496 3
            '\\' => ['bindToClass', substr($bind, 1)],
497 3
            '@' => ['bindToController', substr($bind, 1)],
498 3
            '/' => ['bindToLayer', substr($bind, 1)],
499 3
            ':' => ['bindToNamespace', substr($bind, 1)],
500 3
            default => ['bindToClass', $bind],
501 3
        };
502
503 3
        $name = $this->getFullName();
504 3
        $url  = trim(substr(str_replace('|', '/', $url), strlen($name)), '/');
505
506 3
        return $this->$call($request, $url, $bind, $param, $option);
507
    }
508
509 3
    protected function parseBindAppendParam(string $bind)
510
    {
511 3
        $vars = [];
512 3
        if (str_contains($bind, '?')) {
513
            [$bind, $query] = explode('?', $bind);
514
            parse_str($query, $vars);
515
        }
516 3
        return [$bind, $vars];
517
    }
518
519
    /**
520
     * 绑定到类
521
     * @access protected
522
     * @param  Request   $request
523
     * @param  string    $url URL地址
524
     * @param  string    $class 类名(带命名空间)
525
     * @param  array     $param  路由变量
526
     * @param  array     $option 分组参数
527
     * @return CallbackDispatch
528
     */
529
    protected function bindToClass(Request $request, string $url, string $class, array $param = [], array $option = []): CallbackDispatch
530
    {
531
        $array  = explode('/', $url, 2);
532
        $action = !empty($array[0]) ? $array[0] : $this->config('default_action');
533
534
        if (!empty($array[1])) {
535
            $this->parseUrlParams($array[1], $param);
536
        }
537
538
        return new CallbackDispatch($request, $this, [$class, $action], $param, $option);
539
    }
540
541
    /**
542
     * 绑定到命名空间
543
     * @access protected
544
     * @param  Request   $request
545
     * @param  string    $url URL地址
546
     * @param  string    $namespace 命名空间
547
     * @param  array     $param  路由变量
548
     * @param  array     $option 分组参数
549
     * @return CallbackDispatch
550
     */
551
    protected function bindToNamespace(Request $request, string $url, string $namespace, array $param = [], array $option = []): CallbackDispatch
552
    {
553
        $array  = explode('/', $url, 3);
554
        $class  = !empty($array[0]) ? $array[0] : $this->config('default_controller');
555
        $method = !empty($array[1]) ? $array[1] : $this->config('default_action');
556
557
        if (!empty($array[2])) {
558
            $this->parseUrlParams($array[2], $param);
559
        }
560
561
        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

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