CoreMiddleware.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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;
  12. use Closure;
  13. use FastRoute\Dispatcher;
  14. use Hyperf\Codec\Json;
  15. use Hyperf\Context\Context;
  16. use Hyperf\Context\RequestContext;
  17. use Hyperf\Context\ResponseContext;
  18. use Hyperf\Contract\Arrayable;
  19. use Hyperf\Contract\Jsonable;
  20. use Hyperf\Contract\NormalizerInterface;
  21. use Hyperf\Di\ClosureDefinitionCollectorInterface;
  22. use Hyperf\Di\MethodDefinitionCollectorInterface;
  23. use Hyperf\Di\ReflectionType;
  24. use Hyperf\HttpMessage\Exception\MethodNotAllowedHttpException;
  25. use Hyperf\HttpMessage\Exception\NotFoundHttpException;
  26. use Hyperf\HttpMessage\Exception\ServerErrorHttpException;
  27. use Hyperf\HttpMessage\Server\ResponsePlusProxy;
  28. use Hyperf\HttpMessage\Stream\SwooleStream;
  29. use Hyperf\HttpServer\Contract\CoreMiddlewareInterface;
  30. use Hyperf\HttpServer\Router\Dispatched;
  31. use Hyperf\HttpServer\Router\DispatcherFactory;
  32. use Hyperf\Server\Exception\ServerException;
  33. use InvalidArgumentException;
  34. use Psr\Container\ContainerInterface;
  35. use Psr\Http\Message\ResponseInterface;
  36. use Psr\Http\Message\ServerRequestInterface;
  37. use Psr\Http\Server\RequestHandlerInterface;
  38. use RuntimeException;
  39. use Swow\Psr7\Message\ResponsePlusInterface;
  40. /**
  41. * Core middleware of Hyperf, main responsibility is used to handle route info
  42. * and then delegate to the specified handler (which is Controller) to handle the request,
  43. * generate a response object and delegate to next middleware (Because this middleware is the
  44. * core middleware, then the next middleware also means it's the previous middlewares object) .
  45. */
  46. class CoreMiddleware implements CoreMiddlewareInterface
  47. {
  48. protected Dispatcher $dispatcher;
  49. private MethodDefinitionCollectorInterface $methodDefinitionCollector;
  50. private ?ClosureDefinitionCollectorInterface $closureDefinitionCollector = null;
  51. private NormalizerInterface $normalizer;
  52. public function __construct(protected ContainerInterface $container, private string $serverName)
  53. {
  54. $this->dispatcher = $this->createDispatcher($serverName);
  55. $this->normalizer = $this->container->get(NormalizerInterface::class);
  56. $this->methodDefinitionCollector = $this->container->get(MethodDefinitionCollectorInterface::class);
  57. if ($this->container->has(ClosureDefinitionCollectorInterface::class)) {
  58. $this->closureDefinitionCollector = $this->container->get(ClosureDefinitionCollectorInterface::class);
  59. }
  60. }
  61. public function dispatch(ServerRequestInterface $request): ServerRequestInterface
  62. {
  63. $routes = $this->dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());
  64. $dispatched = new Dispatched($routes, $this->serverName);
  65. return RequestContext::set($request)->setAttribute(Dispatched::class, $dispatched);
  66. }
  67. /**
  68. * Process an incoming server request and return a response, optionally delegating
  69. * response creation to a handler.
  70. */
  71. public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
  72. {
  73. $request = RequestContext::set($request);
  74. /** @var Dispatched $dispatched */
  75. $dispatched = $request->getAttribute(Dispatched::class);
  76. if (! $dispatched instanceof Dispatched) {
  77. throw new ServerException(sprintf('The dispatched object is not a %s object.', Dispatched::class));
  78. }
  79. $response = match ($dispatched->status) {
  80. Dispatcher::NOT_FOUND => $this->handleNotFound($request),
  81. Dispatcher::METHOD_NOT_ALLOWED => $this->handleMethodNotAllowed($dispatched->params, $request),
  82. Dispatcher::FOUND => $this->handleFound($dispatched, $request),
  83. default => null,
  84. };
  85. if (! $response instanceof ResponsePlusInterface) {
  86. $response = $this->transferToResponse($response, $request);
  87. }
  88. return $response->addHeader('Server', 'Hyperf');
  89. }
  90. public function getMethodDefinitionCollector(): MethodDefinitionCollectorInterface
  91. {
  92. return $this->methodDefinitionCollector;
  93. }
  94. public function getClosureDefinitionCollector(): ClosureDefinitionCollectorInterface
  95. {
  96. return $this->closureDefinitionCollector;
  97. }
  98. public function getNormalizer(): NormalizerInterface
  99. {
  100. return $this->normalizer;
  101. }
  102. protected function createDispatcher(string $serverName): Dispatcher
  103. {
  104. $factory = $this->container->get(DispatcherFactory::class);
  105. return $factory->getDispatcher($serverName);
  106. }
  107. /**
  108. * Handle the response when found.
  109. *
  110. * @return array|Arrayable|mixed|ResponseInterface|string
  111. */
  112. protected function handleFound(Dispatched $dispatched, ServerRequestInterface $request): mixed
  113. {
  114. if ($dispatched->handler->callback instanceof Closure) {
  115. $parameters = $this->parseClosureParameters($dispatched->handler->callback, $dispatched->params);
  116. $callback = $dispatched->handler->callback;
  117. $response = $callback(...$parameters);
  118. } else {
  119. [$controller, $action] = $this->prepareHandler($dispatched->handler->callback);
  120. $controllerInstance = $this->container->get($controller);
  121. if (! method_exists($controllerInstance, $action)) {
  122. // Route found, but the handler does not exist.
  123. throw new ServerErrorHttpException('Method of class does not exist.');
  124. }
  125. $parameters = $this->parseMethodParameters($controller, $action, $dispatched->params);
  126. $response = $controllerInstance->{$action}(...$parameters);
  127. }
  128. return $response;
  129. }
  130. /**
  131. * Handle the response when cannot found any routes.
  132. */
  133. protected function handleNotFound(ServerRequestInterface $request): mixed
  134. {
  135. throw new NotFoundHttpException();
  136. }
  137. /**
  138. * Handle the response when the routes found but doesn't match any available methods.
  139. */
  140. protected function handleMethodNotAllowed(array $methods, ServerRequestInterface $request): mixed
  141. {
  142. throw new MethodNotAllowedHttpException('Allow: ' . implode(', ', $methods));
  143. }
  144. protected function prepareHandler(array|string $handler): array
  145. {
  146. if (is_string($handler)) {
  147. if (str_contains($handler, '@')) {
  148. return explode('@', $handler);
  149. }
  150. if (str_contains($handler, '::')) {
  151. return explode('::', $handler);
  152. }
  153. return [$handler, '__invoke'];
  154. }
  155. if (is_array($handler) && isset($handler[0], $handler[1])) {
  156. return $handler;
  157. }
  158. throw new RuntimeException('Handler not exist.');
  159. }
  160. /**
  161. * Transfer the non-standard response content to a standard response object.
  162. *
  163. * @param null|array|Arrayable|Jsonable|ResponseInterface|string $response
  164. */
  165. protected function transferToResponse($response, ServerRequestInterface $request): ResponsePlusInterface
  166. {
  167. if (is_string($response)) {
  168. return $this->response()->addHeader('content-type', 'text/plain')->setBody(new SwooleStream($response));
  169. }
  170. if ($response instanceof ResponseInterface) {
  171. return new ResponsePlusProxy($response);
  172. }
  173. if (is_array($response) || $response instanceof Arrayable) {
  174. return $this->response()
  175. ->addHeader('content-type', 'application/json')
  176. ->setBody(new SwooleStream(Json::encode($response)));
  177. }
  178. if ($response instanceof Jsonable) {
  179. return $this->response()
  180. ->addHeader('content-type', 'application/json')
  181. ->setBody(new SwooleStream((string) $response));
  182. }
  183. if ($this->response()->hasHeader('content-type')) {
  184. return $this->response()->setBody(new SwooleStream((string) $response));
  185. }
  186. return $this->response()->addHeader('content-type', 'text/plain')->setBody(new SwooleStream((string) $response));
  187. }
  188. /**
  189. * Get response instance from context.
  190. */
  191. protected function response(): ResponsePlusInterface
  192. {
  193. return ResponseContext::get();
  194. }
  195. /**
  196. * Parse the parameters of method definitions, and then bind the specified arguments or
  197. * get the value from DI container, combine to an argument array that should be injected
  198. * and return the array.
  199. */
  200. protected function parseMethodParameters(string $controller, string $action, array $arguments): array
  201. {
  202. $definitions = $this->getMethodDefinitionCollector()->getParameters($controller, $action);
  203. return $this->getInjections($definitions, "{$controller}::{$action}", $arguments);
  204. }
  205. /**
  206. * Parse the parameters of closure definitions, and then bind the specified arguments or
  207. * get the value from DI container, combine to an argument array that should be injected
  208. * and return the array.
  209. */
  210. protected function parseClosureParameters(Closure $closure, array $arguments): array
  211. {
  212. if (! $this->container->has(ClosureDefinitionCollectorInterface::class)) {
  213. return [];
  214. }
  215. $definitions = $this->getClosureDefinitionCollector()->getParameters($closure);
  216. return $this->getInjections($definitions, 'Closure', $arguments);
  217. }
  218. /**
  219. * @param ReflectionType[] $definitions
  220. */
  221. private function getInjections(array $definitions, string $callableName, array $arguments): array
  222. {
  223. $injections = [];
  224. foreach ($definitions as $pos => $definition) {
  225. $value = $arguments[$pos] ?? $arguments[$definition->getMeta('name')] ?? null;
  226. if ($value === null) {
  227. if ($definition->getMeta('defaultValueAvailable')) {
  228. $injections[] = $definition->getMeta('defaultValue');
  229. } elseif ($this->container->has($definition->getName())) {
  230. $injections[] = $this->container->get($definition->getName());
  231. } elseif ($definition->allowsNull()) {
  232. $injections[] = null;
  233. } else {
  234. throw new InvalidArgumentException("Parameter '{$definition->getMeta('name')}' "
  235. . "of {$callableName} should not be null");
  236. }
  237. } else {
  238. $injections[] = $this->getNormalizer()->denormalize($value, $definition->getName());
  239. }
  240. }
  241. return $injections;
  242. }
  243. }