RuleGroup.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: liu21st <liu21st@gmail.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types = 1);
  12. namespace think\route;
  13. use Closure;
  14. use think\Container;
  15. use think\Exception;
  16. use think\Request;
  17. use think\Response;
  18. use think\Route;
  19. use think\route\dispatch\Response as ResponseDispatch;
  20. /**
  21. * 路由分组类
  22. */
  23. class RuleGroup extends Rule
  24. {
  25. /**
  26. * 分组路由(包括子分组)
  27. * @var array
  28. */
  29. protected $rules = [
  30. '*' => [],
  31. 'get' => [],
  32. 'post' => [],
  33. 'put' => [],
  34. 'patch' => [],
  35. 'delete' => [],
  36. 'head' => [],
  37. 'options' => [],
  38. ];
  39. /**
  40. * 分组路由规则
  41. * @var mixed
  42. */
  43. protected $rule;
  44. /**
  45. * MISS路由
  46. * @var RuleItem
  47. */
  48. protected $miss;
  49. /**
  50. * 完整名称
  51. * @var string
  52. */
  53. protected $fullName;
  54. /**
  55. * 分组别名
  56. * @var string
  57. */
  58. protected $alias;
  59. /**
  60. * 架构函数
  61. * @access public
  62. * @param Route $router 路由对象
  63. * @param RuleGroup $parent 上级对象
  64. * @param string $name 分组名称
  65. * @param mixed $rule 分组路由
  66. */
  67. public function __construct(Route $router, RuleGroup $parent = null, string $name = '', $rule = null)
  68. {
  69. $this->router = $router;
  70. $this->parent = $parent;
  71. $this->rule = $rule;
  72. $this->name = trim($name, '/');
  73. $this->setFullName();
  74. if ($this->parent) {
  75. $this->domain = $this->parent->getDomain();
  76. $this->parent->addRuleItem($this);
  77. }
  78. if ($router->isTest()) {
  79. $this->lazy(false);
  80. }
  81. }
  82. /**
  83. * 设置分组的路由规则
  84. * @access public
  85. * @return void
  86. */
  87. protected function setFullName(): void
  88. {
  89. if (false !== strpos($this->name, ':')) {
  90. $this->name = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $this->name);
  91. }
  92. if ($this->parent && $this->parent->getFullName()) {
  93. $this->fullName = $this->parent->getFullName() . ($this->name ? '/' . $this->name : '');
  94. } else {
  95. $this->fullName = $this->name;
  96. }
  97. if ($this->name) {
  98. $this->router->getRuleName()->setGroup($this->name, $this);
  99. }
  100. }
  101. /**
  102. * 获取所属域名
  103. * @access public
  104. * @return string
  105. */
  106. public function getDomain(): string
  107. {
  108. return $this->domain ?: '-';
  109. }
  110. /**
  111. * 获取分组别名
  112. * @access public
  113. * @return string
  114. */
  115. public function getAlias(): string
  116. {
  117. return $this->alias ?: '';
  118. }
  119. /**
  120. * 检测分组路由
  121. * @access public
  122. * @param Request $request 请求对象
  123. * @param string $url 访问地址
  124. * @param bool $completeMatch 路由是否完全匹配
  125. * @return Dispatch|false
  126. */
  127. public function check(Request $request, string $url, bool $completeMatch = false)
  128. {
  129. // 检查分组有效性
  130. if (!$this->checkOption($this->option, $request) || !$this->checkUrl($url)) {
  131. return false;
  132. }
  133. // 解析分组路由
  134. if ($this instanceof Resource) {
  135. $this->buildResourceRule();
  136. } elseif ($this->rule instanceof Response) {
  137. return new ResponseDispatch($request, $this, $this->rule);
  138. } else {
  139. $this->parseGroupRule($this->rule);
  140. }
  141. // 获取当前路由规则
  142. $method = strtolower($request->method());
  143. $rules = $this->getMethodRules($method);
  144. if ($this->parent) {
  145. // 合并分组参数
  146. $this->mergeGroupOptions();
  147. // 合并分组变量规则
  148. $this->pattern = array_merge($this->parent->getPattern(), $this->pattern);
  149. }
  150. if (isset($this->option['complete_match'])) {
  151. $completeMatch = $this->option['complete_match'];
  152. }
  153. if (!empty($this->option['merge_rule_regex'])) {
  154. // 合并路由正则规则进行路由匹配检查
  155. $result = $this->checkMergeRuleRegex($request, $rules, $url, $completeMatch);
  156. if (false !== $result) {
  157. return $result;
  158. }
  159. }
  160. // 检查分组路由
  161. foreach ($rules as $key => $item) {
  162. $result = $item->check($request, $url, $completeMatch);
  163. if (false !== $result) {
  164. return $result;
  165. }
  166. }
  167. if ($this->miss && in_array($this->miss->getMethod(), ['*', $method])) {
  168. // 未匹配所有路由的路由规则处理
  169. $result = $this->parseRule($request, '', $this->miss->getRoute(), $url, $this->miss->mergeGroupOptions());
  170. } else {
  171. $result = false;
  172. }
  173. return $result;
  174. }
  175. /**
  176. * 获取当前请求的路由规则(包括子分组、资源路由)
  177. * @access protected
  178. * @param string $method 请求类型
  179. * @return array
  180. */
  181. protected function getMethodRules(string $method): array
  182. {
  183. return array_merge($this->rules[$method], $this->rules['*']);
  184. }
  185. /**
  186. * 分组URL匹配检查
  187. * @access protected
  188. * @param string $url URL
  189. * @return bool
  190. */
  191. protected function checkUrl(string $url): bool
  192. {
  193. if ($this->fullName) {
  194. $pos = strpos($this->fullName, '<');
  195. if (false !== $pos) {
  196. $str = substr($this->fullName, 0, $pos);
  197. } else {
  198. $str = $this->fullName;
  199. }
  200. if ($str && 0 !== stripos(str_replace('|', '/', $url), $str)) {
  201. return false;
  202. }
  203. }
  204. return true;
  205. }
  206. /**
  207. * 设置路由分组别名
  208. * @access public
  209. * @param string $alias 路由分组别名
  210. * @return $this
  211. */
  212. public function alias(string $alias)
  213. {
  214. $this->alias = $alias;
  215. $this->router->getRuleName()->setGroup($alias, $this);
  216. return $this;
  217. }
  218. /**
  219. * 延迟解析分组的路由规则
  220. * @access public
  221. * @param bool $lazy 路由是否延迟解析
  222. * @return $this
  223. */
  224. public function lazy(bool $lazy = true)
  225. {
  226. if (!$lazy) {
  227. $this->parseGroupRule($this->rule);
  228. $this->rule = null;
  229. }
  230. return $this;
  231. }
  232. /**
  233. * 解析分组和域名的路由规则及绑定
  234. * @access public
  235. * @param mixed $rule 路由规则
  236. * @return void
  237. */
  238. public function parseGroupRule($rule): void
  239. {
  240. $origin = $this->router->getGroup();
  241. $this->router->setGroup($this);
  242. if ($rule instanceof \Closure) {
  243. Container::getInstance()->invokeFunction($rule);
  244. } elseif (is_string($rule) && $rule) {
  245. $this->router->bind($rule, $this->domain);
  246. }
  247. $this->router->setGroup($origin);
  248. }
  249. /**
  250. * 检测分组路由
  251. * @access public
  252. * @param Request $request 请求对象
  253. * @param array $rules 路由规则
  254. * @param string $url 访问地址
  255. * @param bool $completeMatch 路由是否完全匹配
  256. * @return Dispatch|false
  257. */
  258. protected function checkMergeRuleRegex(Request $request, array &$rules, string $url, bool $completeMatch)
  259. {
  260. $depr = $this->router->config('pathinfo_depr');
  261. $url = $depr . str_replace('|', $depr, $url);
  262. $regex = [];
  263. $items = [];
  264. foreach ($rules as $key => $item) {
  265. if ($item instanceof RuleItem) {
  266. $rule = $depr . str_replace('/', $depr, $item->getRule());
  267. if ($depr == $rule && $depr != $url) {
  268. unset($rules[$key]);
  269. continue;
  270. }
  271. $complete = $item->getOption('complete_match', $completeMatch);
  272. if (false === strpos($rule, '<')) {
  273. if (0 === strcasecmp($rule, $url) || (!$complete && 0 === strncasecmp($rule, $url, strlen($rule)))) {
  274. return $item->checkRule($request, $url, []);
  275. }
  276. unset($rules[$key]);
  277. continue;
  278. }
  279. $slash = preg_quote('/-' . $depr, '/');
  280. if ($matchRule = preg_split('/[' . $slash . ']<\w+\??>/', $rule, 2)) {
  281. if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) {
  282. unset($rules[$key]);
  283. continue;
  284. }
  285. }
  286. if (preg_match_all('/[' . $slash . ']?<?\w+\??>?/', $rule, $matches)) {
  287. unset($rules[$key]);
  288. $pattern = array_merge($this->getPattern(), $item->getPattern());
  289. $option = array_merge($this->getOption(), $item->getOption());
  290. $regex[$key] = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $complete, '_THINK_' . $key);
  291. $items[$key] = $item;
  292. }
  293. }
  294. }
  295. if (empty($regex)) {
  296. return false;
  297. }
  298. try {
  299. $result = preg_match('/^(?:' . implode('|', $regex) . ')/u', $url, $match);
  300. } catch (\Exception $e) {
  301. throw new Exception('route pattern error');
  302. }
  303. if ($result) {
  304. $var = [];
  305. foreach ($match as $key => $val) {
  306. if (is_string($key) && '' !== $val) {
  307. list($name, $pos) = explode('_THINK_', $key);
  308. $var[$name] = $val;
  309. }
  310. }
  311. if (!isset($pos)) {
  312. foreach ($regex as $key => $item) {
  313. if (0 === strpos(str_replace(['\/', '\-', '\\' . $depr], ['/', '-', $depr], $item), $match[0])) {
  314. $pos = $key;
  315. break;
  316. }
  317. }
  318. }
  319. $rule = $items[$pos]->getRule();
  320. $array = $this->router->getRule($rule);
  321. foreach ($array as $item) {
  322. if (in_array($item->getMethod(), ['*', strtolower($request->method())])) {
  323. $result = $item->checkRule($request, $url, $var);
  324. if (false !== $result) {
  325. return $result;
  326. }
  327. }
  328. }
  329. }
  330. return false;
  331. }
  332. /**
  333. * 获取分组的MISS路由
  334. * @access public
  335. * @return RuleItem|null
  336. */
  337. public function getMissRule(): ? RuleItem
  338. {
  339. return $this->miss;
  340. }
  341. /**
  342. * 注册MISS路由
  343. * @access public
  344. * @param string|Closure $route 路由地址
  345. * @param string $method 请求类型
  346. * @return RuleItem
  347. */
  348. public function miss($route, string $method = '*') : RuleItem
  349. {
  350. // 创建路由规则实例
  351. $ruleItem = new RuleItem($this->router, $this, null, '', $route, strtolower($method));
  352. $ruleItem->setMiss();
  353. $this->miss = $ruleItem;
  354. return $ruleItem;
  355. }
  356. /**
  357. * 添加分组下的路由规则
  358. * @access public
  359. * @param string $rule 路由规则
  360. * @param mixed $route 路由地址
  361. * @param string $method 请求类型
  362. * @return RuleItem
  363. */
  364. public function addRule(string $rule, $route = null, string $method = '*'): RuleItem
  365. {
  366. // 读取路由标识
  367. if (is_string($route)) {
  368. $name = $route;
  369. } else {
  370. $name = null;
  371. }
  372. $method = strtolower($method);
  373. if ('' === $rule || '/' === $rule) {
  374. $rule .= '$';
  375. }
  376. // 创建路由规则实例
  377. $ruleItem = new RuleItem($this->router, $this, $name, $rule, $route, $method);
  378. $this->addRuleItem($ruleItem, $method);
  379. return $ruleItem;
  380. }
  381. /**
  382. * 注册分组下的路由规则
  383. * @access public
  384. * @param Rule $rule 路由规则
  385. * @param string $method 请求类型
  386. * @return $this
  387. */
  388. public function addRuleItem(Rule $rule, string $method = '*')
  389. {
  390. if (strpos($method, '|')) {
  391. $rule->method($method);
  392. $method = '*';
  393. }
  394. $this->rules[$method][] = $rule;
  395. if ($rule instanceof RuleItem && 'options' != $method) {
  396. $this->rules['options'][] = $rule->setAutoOptions();
  397. }
  398. return $this;
  399. }
  400. /**
  401. * 设置分组的路由前缀
  402. * @access public
  403. * @param string $prefix 路由前缀
  404. * @return $this
  405. */
  406. public function prefix(string $prefix)
  407. {
  408. if ($this->parent && $this->parent->getOption('prefix')) {
  409. $prefix = $this->parent->getOption('prefix') . $prefix;
  410. }
  411. return $this->setOption('prefix', $prefix);
  412. }
  413. /**
  414. * 合并分组的路由规则正则
  415. * @access public
  416. * @param bool $merge 是否合并
  417. * @return $this
  418. */
  419. public function mergeRuleRegex(bool $merge = true)
  420. {
  421. return $this->setOption('merge_rule_regex', $merge);
  422. }
  423. /**
  424. * 获取完整分组Name
  425. * @access public
  426. * @return string
  427. */
  428. public function getFullName(): string
  429. {
  430. return $this->fullName ?: '';
  431. }
  432. /**
  433. * 获取分组的路由规则
  434. * @access public
  435. * @param string $method 请求类型
  436. * @return array
  437. */
  438. public function getRules(string $method = ''): array
  439. {
  440. if ('' === $method) {
  441. return $this->rules;
  442. }
  443. return $this->rules[strtolower($method)] ?? [];
  444. }
  445. /**
  446. * 清空分组下的路由规则
  447. * @access public
  448. * @return void
  449. */
  450. public function clear(): void
  451. {
  452. $this->rules = [
  453. '*' => [],
  454. 'get' => [],
  455. 'post' => [],
  456. 'put' => [],
  457. 'patch' => [],
  458. 'delete' => [],
  459. 'head' => [],
  460. 'options' => [],
  461. ];
  462. }
  463. }