DispatcherFactory.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\HttpServer\Router;
  12. use FastRoute\DataGenerator\GroupCountBased as DataGenerator;
  13. use FastRoute\Dispatcher;
  14. use FastRoute\Dispatcher\GroupCountBased;
  15. use FastRoute\RouteParser\Std;
  16. use Hyperf\Collection\Arr;
  17. use Hyperf\Di\Annotation\AnnotationCollector;
  18. use Hyperf\Di\Annotation\MultipleAnnotationInterface;
  19. use Hyperf\Di\Exception\ConflictAnnotationException;
  20. use Hyperf\Di\ReflectionManager;
  21. use Hyperf\HttpServer\Annotation\AutoController;
  22. use Hyperf\HttpServer\Annotation\Controller;
  23. use Hyperf\HttpServer\Annotation\DeleteMapping;
  24. use Hyperf\HttpServer\Annotation\GetMapping;
  25. use Hyperf\HttpServer\Annotation\Mapping;
  26. use Hyperf\HttpServer\Annotation\Middleware;
  27. use Hyperf\HttpServer\Annotation\Middlewares;
  28. use Hyperf\HttpServer\Annotation\PatchMapping;
  29. use Hyperf\HttpServer\Annotation\PostMapping;
  30. use Hyperf\HttpServer\Annotation\PutMapping;
  31. use Hyperf\HttpServer\Annotation\RequestMapping;
  32. use Hyperf\HttpServer\PriorityMiddleware;
  33. use Hyperf\Stringable\Str;
  34. use ReflectionMethod;
  35. class DispatcherFactory
  36. {
  37. protected array $routes = [BASE_PATH . '/config/routes.php'];
  38. /**
  39. * @var RouteCollector[]
  40. */
  41. protected array $routers = [];
  42. /**
  43. * @var Dispatcher[]
  44. */
  45. protected array $dispatchers = [];
  46. public function __construct()
  47. {
  48. $this->initAnnotationRoute(AnnotationCollector::list());
  49. $this->initConfigRoute();
  50. }
  51. public function getDispatcher(string $serverName): Dispatcher
  52. {
  53. if (isset($this->dispatchers[$serverName])) {
  54. return $this->dispatchers[$serverName];
  55. }
  56. $router = $this->getRouter($serverName);
  57. return $this->dispatchers[$serverName] = new GroupCountBased($router->getData());
  58. }
  59. public function initConfigRoute()
  60. {
  61. Router::init($this);
  62. foreach ($this->routes as $route) {
  63. if (file_exists($route)) {
  64. require $route;
  65. }
  66. }
  67. }
  68. public function getRouter(string $serverName): RouteCollector
  69. {
  70. if (isset($this->routers[$serverName])) {
  71. return $this->routers[$serverName];
  72. }
  73. $parser = new Std();
  74. $generator = new DataGenerator();
  75. return $this->routers[$serverName] = new RouteCollector($parser, $generator, $serverName);
  76. }
  77. protected function initAnnotationRoute(array $collector): void
  78. {
  79. foreach ($collector as $className => $metadata) {
  80. if (isset($metadata['_c'][AutoController::class])) {
  81. if ($this->hasControllerAnnotation($metadata['_c'])) {
  82. $message = sprintf('AutoController annotation can\'t use with Controller annotation at the same time in %s.', $className);
  83. throw new ConflictAnnotationException($message);
  84. }
  85. $middlewares = $this->handleMiddleware($metadata['_c']);
  86. $this->handleAutoController($className, $metadata['_c'][AutoController::class], $middlewares, $metadata['_m'] ?? []);
  87. }
  88. if (isset($metadata['_c'][Controller::class])) {
  89. $middlewares = $this->handleMiddleware($metadata['_c']);
  90. $this->handleController($className, $metadata['_c'][Controller::class], $metadata['_m'] ?? [], $middlewares);
  91. }
  92. }
  93. }
  94. /**
  95. * Register route according to AutoController annotation.
  96. * @param PriorityMiddleware[] $middlewares
  97. * @throws ConflictAnnotationException
  98. */
  99. protected function handleAutoController(string $className, AutoController $annotation, array $middlewares = [], array $methodMetadata = []): void
  100. {
  101. $class = ReflectionManager::reflectClass($className);
  102. $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
  103. $prefix = $this->getPrefix($className, $annotation->prefix);
  104. $router = $this->getRouter($annotation->server);
  105. $autoMethods = ['GET', 'POST', 'HEAD'];
  106. $defaultAction = '/index';
  107. foreach ($methods as $method) {
  108. $options = $annotation->options;
  109. $path = $this->parsePath($prefix, $method);
  110. $methodName = $method->getName();
  111. if (str_starts_with($methodName, '__')) {
  112. continue;
  113. }
  114. $methodMiddlewares = $middlewares;
  115. // Handle method level middlewares.
  116. if (isset($methodMetadata[$methodName])) {
  117. $methodMiddlewares = array_merge($methodMiddlewares, $this->handleMiddleware($methodMetadata[$methodName]));
  118. }
  119. // Rewrite by annotation @Middleware for Controller.
  120. $options['middleware'] = $methodMiddlewares;
  121. $router->addRoute($autoMethods, $path, [$className, $methodName], $options);
  122. if (Str::endsWith($path, $defaultAction)) {
  123. $path = Str::replaceLast($defaultAction, '', $path);
  124. $router->addRoute($autoMethods, $path, [$className, $methodName], $options);
  125. }
  126. }
  127. }
  128. /**
  129. * Register route according to Controller and XxxMapping annotations.
  130. * Including RequestMapping, GetMapping, PostMapping, PutMapping, PatchMapping, DeleteMapping.
  131. *
  132. * @param PriorityMiddleware[] $middlewares
  133. * @throws ConflictAnnotationException
  134. */
  135. protected function handleController(string $className, Controller $annotation, array $methodMetadata, array $middlewares = []): void
  136. {
  137. if (! $methodMetadata) {
  138. return;
  139. }
  140. $prefix = $this->getPrefix($className, $annotation->prefix);
  141. $router = $this->getRouter($annotation->server);
  142. $mappingAnnotations = [
  143. RequestMapping::class,
  144. GetMapping::class,
  145. PostMapping::class,
  146. PutMapping::class,
  147. PatchMapping::class,
  148. DeleteMapping::class,
  149. ];
  150. foreach ($methodMetadata as $methodName => $values) {
  151. $options = $annotation->options;
  152. $methodMiddlewares = $middlewares;
  153. // Handle method level middlewares.
  154. if (isset($values)) {
  155. $methodMiddlewares = array_merge($methodMiddlewares, $this->handleMiddleware($values));
  156. }
  157. // Rewrite by annotation @Middleware for Controller.
  158. $options['middleware'] = $methodMiddlewares;
  159. foreach ($mappingAnnotations as $mappingAnnotation) {
  160. /** @var Mapping $mapping */
  161. if ($mapping = $values[$mappingAnnotation] ?? null) {
  162. if (! isset($mapping->methods) || ! isset($mapping->options)) {
  163. continue;
  164. }
  165. $methodOptions = Arr::merge($options, $mapping->options);
  166. // Rewrite by annotation @Middleware for method.
  167. $methodOptions['middleware'] = $options['middleware'];
  168. if (! isset($mapping->path)) {
  169. $path = $prefix . '/' . Str::snake($methodName);
  170. } elseif ($mapping->path === '') {
  171. $path = $prefix;
  172. } elseif ($mapping->path[0] !== '/') {
  173. $path = rtrim($prefix, '/') . '/' . $mapping->path;
  174. } else {
  175. $path = $mapping->path;
  176. }
  177. // $methodOptions['middleware'] : MiddlewareData[]
  178. $router->addRoute($mapping->methods, $path, [$className, $methodName], $methodOptions);
  179. }
  180. }
  181. }
  182. }
  183. protected function getPrefix(string $className, string $prefix): string
  184. {
  185. if (! $prefix) {
  186. $handledNamespace = Str::replaceFirst('Controller', '', Str::after($className, '\\Controller\\'));
  187. $handledNamespace = str_replace('\\', '/', $handledNamespace);
  188. $prefix = Str::snake($handledNamespace);
  189. $prefix = str_replace('/_', '/', $prefix);
  190. }
  191. if ($prefix[0] !== '/') {
  192. $prefix = '/' . $prefix;
  193. }
  194. return $prefix;
  195. }
  196. protected function parsePath(string $prefix, ReflectionMethod $method): string
  197. {
  198. return $prefix . '/' . $method->getName();
  199. }
  200. protected function hasControllerAnnotation(array $item): bool
  201. {
  202. return isset($item[Controller::class]);
  203. }
  204. /**
  205. * @return PriorityMiddleware[]
  206. * @throws ConflictAnnotationException
  207. */
  208. protected function handleMiddleware(array $metadata): array
  209. {
  210. /** @var null|Middlewares $middlewares */
  211. $middlewares = $metadata[Middlewares::class] ?? null;
  212. /** @var null|MultipleAnnotationInterface $middleware */
  213. $middleware = $metadata[Middleware::class] ?? null;
  214. if ($middleware instanceof MultipleAnnotationInterface) {
  215. $middleware = $middleware->toAnnotations();
  216. }
  217. if (! $middlewares && ! $middleware) {
  218. return [];
  219. }
  220. if ($middlewares && $middleware) {
  221. throw new ConflictAnnotationException('Could not use @Middlewares and @Middleware annotation at the same times at same level.');
  222. }
  223. $result = [];
  224. $middlewares = $middlewares ? $middlewares->middlewares : $middleware;
  225. /** @var Middleware $middleware */
  226. foreach ($middlewares as $middleware) {
  227. $result[] = $middleware->priorityMiddleware;
  228. }
  229. return $result;
  230. }
  231. }