/src/websocket-server/src/Router/Router.php

https://github.com/swoft-cloud/swoft-component · PHP · 365 lines · 155 code · 47 blank · 163 comment · 10 complexity · 5a084287a4f1115449f3821cd217dc46 MD5 · raw file

  1. <?php declare(strict_types=1);
  2. /**
  3. * This file is part of Swoft.
  4. *
  5. * @link https://swoft.org
  6. * @document https://swoft.org/docs
  7. * @contact group@swoft.org
  8. * @license https://github.com/swoft-cloud/swoft/blob/master/LICENSE
  9. */
  10. namespace Swoft\WebSocket\Server\Router;
  11. use Swoft\Bean\Annotation\Mapping\Bean;
  12. use Swoft\Contract\RouterInterface;
  13. use Swoft\Stdlib\Helper\Str;
  14. use function array_shift;
  15. use function count;
  16. use function preg_match;
  17. use function preg_match_all;
  18. use function strpos;
  19. use function strtr;
  20. use function trim;
  21. /**
  22. * Class Router
  23. *
  24. * @since 2.0
  25. * @Bean("wsRouter")
  26. */
  27. class Router implements RouterInterface
  28. {
  29. // Default var regex
  30. public const DEFAULT_REGEX = '[^/]+';
  31. /**
  32. * Command counter
  33. *
  34. * @var int
  35. */
  36. private $counter = 0;
  37. /**
  38. * WebSocket modules
  39. *
  40. * [
  41. * '/echo' => [
  42. * 'path' => route path,
  43. * 'class' => moduleClass,
  44. * 'name' => module name,
  45. * 'params' => ['id' => '\d+'],
  46. * 'messageParser' => message parser class,
  47. * 'defaultOpcode' => 1,
  48. * 'defaultCommand' => default command,
  49. * 'eventMethods' => [
  50. * 'handshake' => method1, (on the moduleClass)
  51. * 'open' => method2,
  52. * 'close' => method3,
  53. * ],
  54. * ],
  55. * ... ...
  56. * ]
  57. *
  58. * @var array
  59. */
  60. private $modules = [];
  61. /**
  62. * Message commands for each module
  63. *
  64. * [
  65. * '/echo' => [
  66. * 'prefix1.cmd1' => [
  67. * 'opcode' => 0,
  68. * 'handler' => [controllerClass1, method1],
  69. * ],
  70. * ],
  71. * ]
  72. *
  73. * @var array
  74. */
  75. private $commands = [];
  76. /**
  77. * [
  78. * '/ws-test:chat.send' => [middle1, middle2],
  79. * ]
  80. *
  81. * @var array
  82. */
  83. private $middlewares = [];
  84. /**
  85. * Want disabled modules
  86. *
  87. * [
  88. * // path => 1,
  89. * '/echo' => 1,
  90. * ]
  91. *
  92. * @var array
  93. */
  94. private $disabledModules = [];
  95. /**
  96. * @param string $path
  97. * @param array $info module Info
  98. */
  99. public function addModule(string $path, array $info = []): void
  100. {
  101. $path = Str::formatPath($path);
  102. // It's an disabled module
  103. if (isset($this->disabledModules[$path])) {
  104. return;
  105. }
  106. // Re-set path
  107. $info['path'] = $path;
  108. $info['regex'] = '';
  109. // Not exist path var. eg: "/users/{id}"
  110. if (strpos($path, '{') === false) {
  111. $this->modules[$path] = $info;
  112. return;
  113. }
  114. $matches = [];
  115. $params = $info['params'] ?? [];
  116. // Parse the parameters and replace them with the corresponding regular
  117. if (preg_match_all('#\{([a-zA-Z_][\w-]*)\}#', $path, $matches)) {
  118. /** @var array[] $m */
  119. $pairs = [];
  120. foreach ($matches[1] as $name) {
  121. $regex = $params[$name] ?? self::DEFAULT_REGEX;
  122. // Build pairs
  123. $pairs['{' . $name . '}'] = '(' . $regex . ')';
  124. }
  125. $info['vars'] = $matches[1];
  126. $info['regex'] = '#^' . strtr($path, $pairs) . '$#';
  127. }
  128. $this->modules[$path] = $info;
  129. }
  130. /**
  131. * @param string $modPath
  132. * @param string $cmdId
  133. * @param callable $handler
  134. * @param array $info
  135. */
  136. public function addCommand(string $modPath, string $cmdId, $handler, array $info = []): void
  137. {
  138. $modPath = Str::formatPath($modPath);
  139. // It's an disabled module
  140. if (isset($this->disabledModules[$modPath])) {
  141. return;
  142. }
  143. // Set handler
  144. $info['cmdId'] = $cmdId;
  145. $info['handler'] = $handler;
  146. $info['modPath'] = $modPath;
  147. // Has middleware
  148. if (!empty($info['middles'])) {
  149. $fullId = $this->getFullCmdID($modPath, $cmdId);
  150. $this->addMiddlewares($fullId, $info['middles']);
  151. }
  152. unset($info['middles']);
  153. $this->counter++;
  154. $this->commands[$modPath][$cmdId] = $info;
  155. }
  156. /**
  157. * Match route path for find module info
  158. *
  159. * @param string $path e.g '/echo'
  160. *
  161. * @return array
  162. */
  163. public function match(string $path): array
  164. {
  165. $path = Str::formatPath($path);
  166. if (isset($this->modules[$path])) {
  167. return $this->modules[$path];
  168. }
  169. // If is dynamic route
  170. foreach ($this->modules as $module) {
  171. if (!$pathRegex = $module['regex']) {
  172. continue;
  173. }
  174. // Regex match
  175. $matches = [];
  176. if (preg_match($pathRegex, $path, $matches)) {
  177. $params = [];
  178. $pathVars = $module['vars'];
  179. // First is full match.
  180. array_shift($matches);
  181. foreach ($matches as $index => $value) {
  182. $params[$pathVars[$index]] = $value;
  183. }
  184. $module['routeParams'] = $params;
  185. return $module;
  186. }
  187. }
  188. return [];
  189. }
  190. /**
  191. * @param string $modPath
  192. * @param string $cmdId The message route command ID. like 'home.index'
  193. *
  194. * @return array Return match result
  195. * [
  196. * status,
  197. * route info
  198. * ]
  199. */
  200. public function matchCommand(string $modPath, string $cmdId): array
  201. {
  202. $command = trim($cmdId);
  203. $modPath = Str::formatPath($modPath);
  204. $baseInfo = ['cmdId' => $command, 'modPath' => $modPath];
  205. if (!isset($this->commands[$modPath])) {
  206. return [self::NOT_FOUND, $baseInfo];
  207. }
  208. if (isset($this->commands[$modPath][$command])) {
  209. return [self::FOUND, $this->commands[$modPath][$command]];
  210. }
  211. return [self::NOT_FOUND, $baseInfo];
  212. }
  213. /**
  214. * @param string $modPath
  215. * @param string $cmdId
  216. *
  217. * @return string
  218. */
  219. public function getFullCmdID(string $modPath, string $cmdId): string
  220. {
  221. return $modPath . ':' . $cmdId;
  222. }
  223. /**
  224. * @param string $fullCmdID Full command ID: modPath + ':' + cmdId eg: '/ws-test:chat.send'
  225. * @param array $middlewares
  226. */
  227. public function addMiddlewares(string $fullCmdID, array $middlewares): void
  228. {
  229. $this->middlewares[$fullCmdID] = $middlewares;
  230. }
  231. /**
  232. * @param string $fullCmdID
  233. *
  234. * @return array
  235. */
  236. public function getMiddlewaresByID(string $fullCmdID): array
  237. {
  238. return $this->middlewares[$fullCmdID] ?? [];
  239. }
  240. /**
  241. * @param string $modPath
  242. * @param string $cmdId
  243. *
  244. * @return array
  245. */
  246. public function getCmdMiddlewares(string $modPath, string $cmdId): array
  247. {
  248. $fullCmdID = $this->getFullCmdID($modPath, $cmdId);
  249. return $this->middlewares[$fullCmdID] ?? [];
  250. }
  251. /**
  252. * @param string $path
  253. *
  254. * @return bool
  255. */
  256. public function hasModule(string $path): bool
  257. {
  258. return isset($this->modules[$path]);
  259. }
  260. /**
  261. * @return array
  262. */
  263. public function getModules(): array
  264. {
  265. return $this->modules;
  266. }
  267. /**
  268. * @return array
  269. */
  270. public function getCommands(): array
  271. {
  272. return $this->commands;
  273. }
  274. /**
  275. * @return int
  276. */
  277. public function getModuleCount(): int
  278. {
  279. return count($this->modules);
  280. }
  281. /**
  282. * @return int
  283. */
  284. public function getCounter(): int
  285. {
  286. return $this->counter;
  287. }
  288. /**
  289. * @return array
  290. */
  291. public function getDisabledModules(): array
  292. {
  293. return $this->disabledModules;
  294. }
  295. /**
  296. * @param array $paths
  297. */
  298. public function setDisabledModules(array $paths): void
  299. {
  300. foreach ($paths as $path) {
  301. $this->disabledModules[$path] = 1;
  302. }
  303. }
  304. /**
  305. * @return array
  306. */
  307. public function getMiddlewares(): array
  308. {
  309. return $this->middlewares;
  310. }
  311. /**
  312. * @param array $middlewares
  313. */
  314. public function setMiddlewares(array $middlewares): void
  315. {
  316. $this->middlewares = $middlewares;
  317. }
  318. }