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

RuleGroup::check()   B

Complexity

Conditions 11
Paths 61

Size

Total Lines 50
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 14.267

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 14
cts 20
cp 0.7
crap 14.267
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
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