*/ private array $methods = []; /** * @var array */ protected array $httpHandler = []; /** * @var Handler */ protected Handler $found; /** * @throws */ public function __construct() { $this->found = new Handler([NotFoundController::class, 'fail'], [], ResponseFormat::class); } /** * @return array */ public function getMethods(): array { return $this->methods; } public function clear(): void { $this->_item = []; $this->dump = []; $this->groupTack = []; $this->methods = []; $this->httpHandler = []; } /** * @param string $method * @param HttpRequestHandler $handler * @return void */ public function setHttpHandler(string $method, HttpRequestHandler $handler): void { $this->httpHandler[$method] = $handler; } /** * @return array */ public function getDump(): array { return $this->dump; } /** * @return Traversable */ public function getIterator(): Traversable { return new \ArrayIterator($this->_item); } /** * @param array $method * @param string $route * @param string|array|Closure $closure */ public function addRoute(array $method, string $route, string|array|Closure $closure): void { try { $route = $this->_splicing_routing($route); if ($closure instanceof Closure) { $handler = di(ControllerInterpreter::class)->addRouteByClosure($closure); } else { $handler = $this->resolve($closure, di(ControllerInterpreter::class)); } foreach ($method as $value) { if ($value instanceof RequestMethod) { $value = $value->getString(); } $handler->setRequestMethod($value); if (is_array($closure)) { $closure[0] = is_object($closure[0]) ? get_class($closure[0]) : $closure; } else if (is_string($closure)) { $closure = explode('@', $closure); } $this->register($route, $value, $handler); } } catch (Throwable $throwable) { \Kiri::getLogger()->json_log($throwable); } } /** * @return array */ public function dump(): array { $array = []; foreach ($this->methods as $methodPath => $handler) { [$path, $method] = explode('_', $methodPath); $controller = $handler instanceof Closure ? '\Closure' : $handler->getClass() . '::' . $handler->getMethod(); $array[] = [ 'path' => $path, 'method' => $method, 'handler' => $controller ]; } return $array; } /** * @param string|array $closure * @param ControllerInterpreter $interpreter * @return Handler * @throws */ private function resolve(string|array $closure, ControllerInterpreter $interpreter): Handler { $container = \Kiri::getDi(); if (is_array($closure)) { if (is_string($closure[0])) { $closure[0] = $container->get($closure[0]); } $handler = $interpreter->addRouteByString(... $closure); $sourceFile = Router::getCurrentSourceFile(); if ($sourceFile !== null) { $handler->setSourceFile($sourceFile); $handler->setSourceKind('route_file'); } return $handler; } if (!str_contains($closure, '@')) { $closure .= '@'; } [$className, $method] = explode('@', $closure); $class = $container->get($this->resetName($className)); $handler = $interpreter->addRouteByString($class, $method); $sourceFile = Router::getCurrentSourceFile(); if ($sourceFile !== null) { $handler->setSourceFile($sourceFile); $handler->setSourceKind('route_file'); } return $handler; } /** * @param string $className * @return string */ private function resetName(string $className): string { $namespace = array_filter(array_column($this->groupTack, 'namespace')); if (count($namespace) < 1) { return $className; } return implode('\\', $namespace) . '\\' . $className; } /** * @param string $path * @param string $method * @param Handler $handler * @return void * @throws */ public function register(string $path, string $method, Handler $handler): void { if ($handler->getSourceFile() === null && $handler->getClass() !== null) { $reflect = \Kiri::getDi()->getReflectionClass($handler->getClass()); $handler->setSourceFile($this->normalizePath((string)$reflect->getFileName())); $handler->setSourceKind('attribute'); } $this->methods[$method . '_' . $path] = $handler; $handler->setMiddlewares($this->registerMiddleware($handler->getClass(), $handler->getMethod())); $handler->setDeferred(DeferRegistry::get($handler->getClass(), $handler->getMethod())); } public function exportArtifact(): array { $entries = []; $hasClosureRoutes = false; foreach ($this->methods as $methodPath => $handler) { if ($handler instanceof Handler && $handler->isClosure()) { $hasClosureRoutes = true; continue; } [$requestMethod, $path] = explode('_', $methodPath, 2); $class = $handler instanceof Handler ? $handler->getClass() : $handler->class; $method = $handler instanceof Handler ? $handler->getMethod() : $handler->method; $middlewares = $handler instanceof Handler ? $handler->getMiddlewares() : $handler->middlewares; $sourceFile = $handler instanceof Handler ? $handler->getSourceFile() : $handler->sourceFile; $sourceKind = $handler instanceof Handler ? $handler->getSourceKind() : $handler->sourceKind; $deferred = $handler instanceof Handler ? $this->serializeDeferred($handler->getDeferred()) : ($handler->deferred ?? []); $entries[] = [ 'request_method' => $requestMethod, 'path' => $path, 'class' => $class, 'method' => $method, 'middlewares' => $middlewares, 'source_file' => $sourceFile, 'source_kind' => $sourceKind, 'deferred' => $deferred, ]; } return [ 'has_closure_routes' => $hasClosureRoutes, 'entries' => $entries, ]; } public function importArtifact(array $artifact, array $excludeSourceFiles = []): bool { if (($artifact['has_closure_routes'] ?? false) === true) { return false; } $entries = $artifact['entries'] ?? null; if (!is_array($entries)) { return false; } $exclude = array_fill_keys(array_map([$this, 'normalizePath'], $excludeSourceFiles), true); foreach ($entries as $entry) { if (!is_array($entry)) { continue; } $sourceFile = $entry['source_file'] ?? null; if (is_string($sourceFile) && isset($exclude[$this->normalizePath($sourceFile)])) { continue; } $class = $entry['class'] ?? null; $method = $entry['method'] ?? null; $requestMethod = $entry['request_method'] ?? null; $path = $entry['path'] ?? null; if (!is_string($class) || !is_string($method) || !is_string($requestMethod) || !is_string($path)) { continue; } $this->methods[$requestMethod . '_' . $path] = new RouteEntry( requestMethod: $requestMethod, path: $path, class: $class, method: $method, middlewares: is_array($entry['middlewares'] ?? null) ? $entry['middlewares'] : [], sourceFile: is_string($sourceFile) ? $this->normalizePath($sourceFile) : null, sourceKind: is_string($entry['source_kind'] ?? null) ? $entry['source_kind'] : 'attribute', deferred: is_array($entry['deferred'] ?? null) ? $entry['deferred'] : [], ); } return true; } /** * @param string $class * @param string $method * @return array * @throws ReflectionException */ public function registerMiddleware(string $class, string $method): array { $response = []; $middlewares = \config('servers.request.middlewares', []); if (is_array($middlewares) && count($middlewares) > 0) { $response = $this->appendMiddleware($response, $middlewares); } $middlewares = array_column($this->groupTack, 'middleware'); $response = $this->appendMiddleware($response, $middlewares); return $this->read_method_middleware($response, $class, $method); } /** * @param array $response * @param string $class * @param string $method * @return array * @throws ReflectionException */ private function read_method_middleware(array $response, string $class, string $method): array { $reflect = \Kiri::getDi()->getReflectionClass($class); $attributes = $reflect->getMethod($method)->getAttributes(Annotate\Middleware::class); foreach ($attributes as $attribute) { /** @var Annotate\Middleware $instance */ $instance = $attribute->newInstance(); $data = $instance->middleware; if (is_string($data)) { $data = [$data]; } foreach ($data as $middleware) { if (!in_array($middleware, $response)) { $response[] = $middleware; } } } return $response; } /** * @param string $class * @param string $method * @return Defer[] */ private function appendMiddleware(array $response, array $middlewares): array { foreach ($middlewares as $middleware) { if (is_string($middleware)) { $middleware = [$middleware]; } foreach ($middleware as $value) { if (!in_array($value, $response)) { $response[] = $value; } } } return $response; } /** * @param string $path * @param string $method * @return HttpRequestHandler * @throws */ public function query(string $path, string $method): HttpRequestHandler { $key = $method . '_' . $path; if (!isset($this->httpHandler[$key]) && isset($this->methods[$key])) { $this->httpHandler[$key] = $this->compileHandler($this->methods[$key]); } return $this->httpHandler[$key] ?? $this->not_found_handler(); } /** * @return HttpRequestHandler */ protected function not_found_handler(): HttpRequestHandler { $middlewares = \config('servers.request.middlewares', []); if (!is_array($middlewares) || count($middlewares) < 1) { return new HttpRequestHandler($middlewares, $this->found); } for ($index = 0; $index < count($middlewares); $index++) { $middlewares[$index] = \Kiri::getDi()->get($middlewares[$index]); } return new HttpRequestHandler($middlewares, $this->found); } public function warmHttpHandlers(): void { foreach ($this->methods as $name => $method) { $this->httpHandler[$name] = $this->compileHandler($method); } } /** * @param string $route * @return string */ protected function _splicing_routing(string $route): string { $route = ltrim($route, '/'); $prefix = array_column($this->groupTack, 'prefix'); if (empty($prefix = array_filter($prefix))) { return '/' . $route; } return '/' . implode('/', $prefix) . '/' . $route; } /** * @param Defer[] $deferred * @return array */ private function serializeDeferred(array $deferred): array { $result = []; foreach ($deferred as $defer) { $result[] = [ 'callback' => $defer->callback, 'params' => $defer->params, ]; } return $result; } /** * @param array $data * @return Defer[] */ private function deserializeDeferred(array $data): array { $result = []; foreach ($data as $item) { if (isset($item['callback'])) { $result[] = new Defer($item['callback'], $item['params'] ?? []); } } return $result; } private function normalizePath(string $path): string { $resolved = realpath($path) ?: $path; return str_replace('\\', '/', $resolved); } private function compileHandler(Handler|RouteEntry $method): HttpRequestHandler { if ($method instanceof RouteEntry) { $controller = \Kiri::getDi()->get($method->class); $handler = di(ControllerInterpreter::class)->addRouteByString($controller, $method->method); $handler->setRequestMethod($method->requestMethod); $handler->setMiddlewares($method->middlewares); $handler->setSourceFile($method->sourceFile); $handler->setSourceKind($method->sourceKind); $handler->setDeferred($this->deserializeDeferred($method->deferred)); $method = $handler; } $middlewares = $method->getMiddlewares(); foreach ($middlewares as $key => $middleware) { $middlewares[$key] = di($middleware); } $requestHandler = new HttpRequestHandler($middlewares, $method); $validator = Middleware::getValidator($method->getClass(), $method->getMethod()); if ($validator !== null) { $requestHandler->withValidatorMiddleware(new ValidatorMiddleware(di(\Psr\Http\Message\ResponseInterface::class), $method->getClass(), $method->getMethod())); } return $requestHandler; } /** * @param mixed $offset * @return bool */ public function offsetExists(mixed $offset): bool { // TODO: Implement offsetExists() method. return isset($this->_item[$offset]); } /** * @param mixed $offset * @return Router|null */ public function offsetGet(mixed $offset): ?Router { if ($this->offsetExists($offset)) { return $this->_item[$offset]; } return null; } /** * @param mixed $offset * @param mixed $value * @return void */ public function offsetSet(mixed $offset, mixed $value): void { // TODO: Implement offsetSet() method. $this->_item[$offset] = $value; } /** * @param mixed $offset * @return void */ public function offsetUnset(mixed $offset): void { unset($this->_item[$offset]); } }