Passed
Push — 8.0 ( cd7e8a...0aac54 )
by liu
11:57 queued 09:21
created

RuleGroup::check()   B

Complexity

Conditions 11
Paths 61

Size

Total Lines 50
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 13.1488

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 11
eloc 22
c 4
b 0
f 0
nc 61
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
        }
172
173
        // 检查分组路由
174 27
        foreach ($rules as $item) {
175 24
            $result = $item->check($request, $url, $completeMatch);
176
177 24
            if (false !== $result) {
178 24
                return $result;
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->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

253
            $this->router->/** @scrutinizer ignore-call */ 
254
                           bind($rule, $this->domain);
Loading history...
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
     * 分组绑定 默认绑定到当前分组名所在的控制器分级
397
     * 绑定规则 class @controller :namespace /layer
398
     * @access public
399
     * @param  string $bind 绑定资源
400
     * @return $this
401
     */
402 3
    public function bind(string $bind = '')
403
    {
404 3
        $this->bind = $bind ?: '/' . $this->getFullName();
405 3
        return $this;
406
    }
407
408
    /**
409
     * 分组绑定到类
410
     * @access public
411
     * @param  string $class
412
     * @return $this
413
     */
414
    public function class (string $class)
415
    {
416
        $this->bind = '\\' . $class;
417
        return $this;
418
    }
419
420
    /**
421
     * 分组绑定到控制器
422
     * @access public
423
     * @param  string $controller
424
     * @return $this
425
     */
426
    public function controller(string $controller)
427
    {
428
        $this->bind = '@' . $controller;
429
        return $this;
430
    }
431
432
    /**
433
     * 分组绑定到命名空间
434
     * @access public
435
     * @param  string $namespace
436
     * @return $this
437
     */
438
    public function namespace(string $namespace)
439
    {
440
        $this->bind = ':' . $namespace;
441
        return $this;
442
    }
443
444
    /**
445
     * 分组绑定到控制器分级
446
     * @access public
447
     * @param  string $namespace
448
     * @return $this
449
     */
450
    public function layer(string $layer)
451
    {
452
        $this->bind = '/' . $layer;
453
        return $this;
454
    }
455
456
    /**
457
     * 检测URL绑定
458
     * @access private
459
     * @param  Request   $request
460
     * @param  string    $url URL地址
461
     * @param  array     $option 分组参数
462
     * @return Dispatch
463
     */
464 3
    public function checkBind(Request $request, string $url, array $option = []): Dispatch
465
    {
466 3
        $bind = $this->parseBindAppendParam($this->bind);
467
468 3
        [$call, $bind] = match (substr($bind, 0, 1)) {
469 3
            '\\'    => ['bindToClass', substr($bind, 1)],
470 3
            '@'     => ['bindToController', substr($bind, 1)],
471 3
            '/'     => ['bindToLayer', substr($bind, 1)],
472 3
            ':'     => ['bindToNamespace', substr($bind, 1)],
473 3
            default => ['bindToClass', $bind],
474 3
        };
475
476 3
        $groupName = $this->getFullName();
477 3
        $checkUrl  = trim(substr(str_replace('|', '/', $url), strlen($groupName)), '/');
478
479 3
        return $this->$call($request, $checkUrl, $bind, $option);
480
    }
481
482 3
    protected function parseBindAppendParam(string $bind)
483
    {
484 3
        if (str_contains($bind, '?')) {
485
            [$bind, $query] = explode('?', $bind);
486
            parse_str($query, $vars);
487
            $this->append($vars);
488
        }
489 3
        return $bind;
490
    }
491
492
    /**
493
     * 绑定到类
494
     * @access protected
495
     * @param  Request   $request
496
     * @param  string    $url URL地址
497
     * @param  string    $class 类名(带命名空间)
498
     * @param  array     $option 分组参数
499
     * @return CallbackDispatch
500
     */
501
    protected function bindToClass(Request $request, string $url, string $class, array $option = []): CallbackDispatch
502
    {
503
        $array  = explode('/', $url, 2);
504
        $action = !empty($array[0]) ? $array[0] : $this->config('default_action');
505
        $param  = [];
506
507
        if (!empty($array[1])) {
508
            $this->parseUrlParams($array[1], $param);
509
        }
510
511
        return new CallbackDispatch($request, $this, [$class, $action], $param, $option);
512
    }
513
514
    /**
515
     * 绑定到命名空间
516
     * @access protected
517
     * @param  Request   $request
518
     * @param  string    $url URL地址
519
     * @param  string    $namespace 命名空间
520
     * @param  array     $option 分组参数
521
     * @return CallbackDispatch
522
     */
523
    protected function bindToNamespace(Request $request, string $url, string $namespace, array $option = []): CallbackDispatch
524
    {
525
        $array  = explode('/', $url, 3);
526
        $class  = !empty($array[0]) ? $array[0] : $this->config('default_controller');
527
        $method = !empty($array[1]) ? $array[1] : $this->config('default_action');
528
        $param  = [];
529
530
        if (!empty($array[2])) {
531
            $this->parseUrlParams($array[2], $param);
532
        }
533
534
        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

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