[
  {
    "path": ".gitignore",
    "content": "composer.lock\nvendor\nvendor/\n.idea\n.idea/"
  },
  {
    "path": "README.md",
    "content": "# webman-framework\nNote: This repository is the core code of the webman framework. If you want to build an application using webman, visit the main [webman](https://github.com/walkor/webman) repository.\n\n## LICENSE\nMIT\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"workerman/webman-framework\",\n  \"type\": \"library\",\n  \"keywords\": [\n    \"high performance\",\n    \"http service\"\n  ],\n  \"homepage\": \"https://www.workerman.net\",\n  \"license\": \"MIT\",\n  \"description\": \"High performance HTTP Service Framework.\",\n  \"authors\": [\n    {\n      \"name\": \"walkor\",\n      \"email\": \"walkor@workerman.net\",\n      \"homepage\": \"https://www.workerman.net\",\n      \"role\": \"Developer\"\n    }\n  ],\n  \"support\": {\n    \"email\": \"walkor@workerman.net\",\n    \"issues\": \"https://github.com/walkor/webman/issues\",\n    \"forum\": \"https://wenda.workerman.net/\",\n    \"wiki\": \"https://doc.workerman.net/\",\n    \"source\": \"https://github.com/walkor/webman-framework\"\n  },\n  \"require\": {\n    \"php\": \">=8.1\",\n    \"ext-json\": \"*\",\n    \"workerman/workerman\": \"^5.1 || dev-master\",\n    \"nikic/fast-route\": \"^1.3\",\n    \"psr/container\": \">=1.0\",\n    \"psr/log\": \"^2.0 || ^3.0\"\n  },\n  \"suggest\": {\n    \"ext-event\": \"For better performance. \"\n  },\n  \"autoload\": {\n    \"psr-4\": {\n      \"Webman\\\\\": \"./src\",\n      \"support\\\\\": \"./src/support\",\n      \"Support\\\\\": \"./src/support\",\n      \"Support\\\\Bootstrap\\\\\": \"./src/support/bootstrap\",\n      \"Support\\\\Exception\\\\\": \"./src/support/exception\",\n      \"Support\\\\View\\\\\": \"./src/support/view\"\n    },\n    \"files\": [\n      \"./src/support/helpers.php\"\n    ]\n  },\n  \"minimum-stability\": \"dev\",\n  \"prefer-stable\": true\n}\n"
  },
  {
    "path": "src/App.php",
    "content": "<?php\n\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman;\n\nuse ArrayObject;\nuse Closure;\nuse Exception;\nuse FastRoute\\Dispatcher;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Psr\\Log\\LoggerInterface;\nuse ReflectionEnum;\nuse support\\exception\\InputValueException;\nuse support\\exception\\PageNotFoundException;\nuse think\\Model as ThinkModel;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\ContainerInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse ReflectionClass;\nuse ReflectionException;\nuse ReflectionFunction;\nuse ReflectionFunctionAbstract;\nuse ReflectionMethod;\nuse support\\exception\\MissingInputException;\nuse support\\exception\\RecordNotFoundException;\nuse support\\exception\\InputTypeException;\nuse Throwable;\nuse Webman\\Exception\\ExceptionHandler;\nuse Webman\\Exception\\ExceptionHandlerInterface;\nuse Webman\\Http\\Request;\nuse Webman\\Http\\Response;\nuse Webman\\Route\\Route as RouteObject;\nuse support\\annotation\\route\\Route as RouteAttribute;\nuse Workerman\\Connection\\TcpConnection;\nuse Workerman\\Protocols\\Http;\nuse Workerman\\Worker;\nuse function array_merge;\nuse function array_pop;\nuse function array_reduce;\nuse function array_splice;\nuse function array_values;\nuse function class_exists;\nuse function clearstatcache;\nuse function count;\nuse function current;\nuse function end;\nuse function explode;\nuse function get_class_methods;\nuse function gettype;\nuse function implode;\nuse function is_a;\nuse function is_array;\nuse function is_dir;\nuse function is_file;\nuse function is_numeric;\nuse function is_object;\nuse function is_string;\nuse function key;\nuse function method_exists;\nuse function ob_get_clean;\nuse function ob_start;\nuse function pathinfo;\nuse function scandir;\nuse function str_replace;\nuse function strpos;\nuse function strtolower;\nuse function substr;\nuse function trim;\n\n/**\n * Class App\n * @package Webman\n */\nclass App\n{\n\n    /**\n     * @var callable[]\n     */\n    protected static $callbacks = [];\n\n    /**\n     * @var array<string, ReflectionFunctionAbstract>\n     */\n    protected static array $reflectorCache = [];\n\n    /**\n     * @var array<string, array<int, array<string, mixed>>>\n     */\n    protected static array $parameterMetadataCache = [];\n\n    /**\n     * @var Worker\n     */\n    protected static $worker = null;\n\n    /**\n     * @var ?LoggerInterface\n     */\n    protected static ?LoggerInterface $logger = null;\n\n    /**\n     * @var string\n     */\n    protected static $appPath = '';\n\n    /**\n     * @var string\n     */\n    protected static $publicPath = '';\n\n    /**\n     * @var string\n     */\n    protected static $requestClass = '';\n\n    /**\n     * App constructor.\n     * @param string $requestClass\n     * @param LoggerInterface $logger\n     * @param string $appPath\n     * @param string $publicPath\n     */\n    public function __construct(string $requestClass, LoggerInterface $logger, string $appPath, string $publicPath)\n    {\n        static::$requestClass = $requestClass;\n        static::$logger = $logger;\n        static::$publicPath = $publicPath;\n        static::$appPath = $appPath;\n    }\n\n    /**\n     * OnMessage.\n     * @param TcpConnection|mixed $connection\n     * @param Request|mixed $request\n     * @return null\n     * @throws Throwable\n     */\n    public function onMessage($connection, $request)\n    {\n        try {\n            Context::reset(new ArrayObject([Request::class => $request]));\n            $path = $request->path();\n            $key = $request->method() . $path;\n            if (isset(static::$callbacks[$key])) {\n                [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];\n                static::send($connection, $callback($request), $request);\n                return null;\n            }\n\n            $status = 200;\n            if (\n                static::unsafeUri($connection, $path, $request) ||\n                static::findFile($connection, $path, $key, $request) ||\n                static::findRoute($connection, $path, $key, $request, $status)\n            ) {\n                return null;\n            }\n\n            $controllerAndAction = static::parseControllerAction($path);\n            $plugin = $controllerAndAction['plugin'] ?? static::getPluginByPath($path);\n            if (!$controllerAndAction || Route::isDefaultRouteDisabled($plugin, $controllerAndAction['app'] ?: '*') ||\n                Route::isDefaultRouteDisabled($controllerAndAction['controller']) ||\n                Route::isDefaultRouteDisabled([$controllerAndAction['controller'], $controllerAndAction['action']])) {\n                $request->plugin = $plugin;\n                $callback = static::getFallback($plugin, $status);\n                $request->app = $request->controller = $request->action = '';\n                static::send($connection, $callback($request), $request);\n                return null;\n            }\n            $app = $controllerAndAction['app'];\n            $controller = $controllerAndAction['controller'];\n            $action = $controllerAndAction['action'];\n\n            if ($methodNotAllowed = static::defaultRouteMethodNotAllowedResponse($controller, $action, $request->method())) {\n                $callback = $methodNotAllowed;\n                static::collectCallbacks($key, [$callback, $plugin, $app, $controller, $action, null]);\n                [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];\n                static::send($connection, $callback($request), $request);\n                return null;\n            }\n\n            $callback = static::getCallback($plugin, $app, [$controller, $action]);\n            static::collectCallbacks($key, [$callback, $plugin, $app, $controller, $action, null]);\n            [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];\n            static::send($connection, $callback($request), $request);\n        } catch (Throwable $e) {\n            static::send($connection, static::exceptionResponse($e, $request), $request);\n        }\n        return null;\n    }\n\n    /**\n     * Method allowlist for default route (no explicit route path).\n     * If action method has Route attributes with empty path, they are treated as allowed methods.\n     * Returns a cached callback producing 405 response when current method is not allowed, otherwise null.\n     * @param string $controllerClass\n     * @param string $action\n     * @param string $httpMethod\n     * @return callable|null\n     */\n    protected static function defaultRouteMethodNotAllowedResponse(string $controllerClass, string $action, string $httpMethod): ?callable\n    {\n        $httpMethod = strtoupper($httpMethod);\n        static $allowedCache = [];\n        $cacheKey = $controllerClass . '::' . $action;\n\n        if (!isset($allowedCache[$cacheKey])) {\n            $allowed = [];\n            try {\n                if (method_exists($controllerClass, $action)) {\n                    $ref = new ReflectionMethod($controllerClass, $action);\n                    $attrs = $ref->getAttributes(RouteAttribute::class, \\ReflectionAttribute::IS_INSTANCEOF);\n                    foreach ($attrs as $attr) {\n                        /** @var RouteAttribute $route */\n                        $route = $attr->newInstance();\n                        if ($route->path !== null) {\n                            continue;\n                        }\n                        foreach ($route->methods as $m) {\n                            $m = strtoupper((string)$m);\n                            $allowed[$m] = $m;\n                        }\n                    }\n                }\n            } catch (Throwable $e) {\n            }\n            $allowedCache[$cacheKey] = $allowed;\n            if (count($allowedCache) > 1024) {\n                unset($allowedCache[key($allowedCache)]);\n            }\n        } else {\n            $allowed = $allowedCache[$cacheKey];\n        }\n\n        if (!$allowed || isset($allowed[$httpMethod])) {\n            return null;\n        }\n\n        $allowHeader = implode(', ', array_values($allowed));\n        return static function () use ($allowHeader) {\n            return new Response(405, ['Allow' => $allowHeader], '405 Method Not Allowed');\n        };\n    }\n\n    /**\n     * OnWorkerStart.\n     * @param $worker\n     * @return void\n     */\n    public function onWorkerStart($worker)\n    {\n        static::$worker = $worker;\n        Http::requestClass(static::$requestClass);\n    }\n\n    /**\n     * CollectCallbacks.\n     * @param string $key\n     * @param array $data\n     * @return void\n     */\n    protected static function collectCallbacks(string $key, array $data)\n    {\n        static::$callbacks[$key] = $data;\n        if (count(static::$callbacks) > 1024) {\n            unset(static::$callbacks[key(static::$callbacks)]);\n        }\n    }\n\n    /**\n     * UnsafeUri.\n     * @param TcpConnection $connection\n     * @param string $path\n     * @param $request\n     * @return bool\n     */\n    protected static function unsafeUri(TcpConnection $connection, string $path, $request): bool\n    {\n        if (!$path || $path[0] !== '/' || static::containsPathTraversal($path)) {\n            $callback = static::getFallback('', 400);\n            $request->plugin = $request->app = $request->controller = $request->action = '';\n            static::send($connection, $callback($request, 400), $request);\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Check if a path contains directory traversal or dangerous sequences.\n     * @param string $path\n     * @return bool\n     */\n    protected static function containsPathTraversal(string $path): bool\n    {\n        return strpos($path, '/../') !== false\n            || substr($path, -3) === '/..'\n            || strpos($path, \"\\\\\") !== false\n            || strpos($path, \"\\0\") !== false;\n    }\n\n    /**\n     * GetFallback.\n     * @param string $plugin\n     * @param int $status\n     * @return Closure\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     * @throws ReflectionException\n     */\n    protected static function getFallback(string $plugin = '', int $status = 404): Closure\n    {\n        // When route, controller and action not found, try to use Route::fallback\n        return Route::getFallback($plugin, $status) ?: function () {\n            throw new PageNotFoundException();\n        };\n    }\n\n    /**\n     * ExceptionResponse.\n     * @param Throwable $e\n     * @param $request\n     * @return Response\n     */\n    protected static function exceptionResponse(Throwable $e, $request): Response\n    {\n        try {\n            $app = $request->app ?: '';\n            $plugin = $request->plugin ?: '';\n            $exceptionConfig = static::config($plugin, 'exception');\n            $appExceptionConfig = static::config(\"\", 'exception');\n            if (!isset($exceptionConfig['']) && isset($appExceptionConfig['@'])) {\n                //如果插件没有配置自己的异常处理器并且配置了全局@异常处理器 则使用全局异常处理器\n                $defaultException = $appExceptionConfig['@'] ?? ExceptionHandler::class;\n            } else {\n                $defaultException = $exceptionConfig[''] ?? ExceptionHandler::class;\n            }\n            $exceptionHandlerClass = $exceptionConfig[$app] ?? $defaultException;\n\n            /** @var ExceptionHandlerInterface $exceptionHandler */\n            $exceptionHandler = (static::container($plugin) ?? static::container(''))->make($exceptionHandlerClass, [\n                'logger' => static::$logger,\n                'debug' => static::config($plugin, 'app.debug')\n            ]);\n            $exceptionHandler->report($e);\n            $response = $exceptionHandler->render($request, $e);\n            $response->exception($e);\n            return $response;\n        } catch (Throwable $e) {\n            $response = new Response(500, [], static::config($plugin ?? '', 'app.debug') ? (string)$e : $e->getMessage());\n            $response->exception($e);\n            return $response;\n        }\n    }\n\n    /**\n     * GetCallback.\n     * @param string $plugin\n     * @param string $app\n     * @param $call\n     * @param array $args\n     * @param bool $withGlobalMiddleware\n     * @param RouteObject|null $route\n     * @return callable\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     * @throws ReflectionException\n     */\n    public static function getCallback(string $plugin, string $app, $call, array $args = [], bool $withGlobalMiddleware = true, ?RouteObject $route = null)\n    {\n        $isController = is_array($call) && is_string($call[0]);\n        $middlewares = Middleware::getMiddleware($plugin, $app, $call, $route, $withGlobalMiddleware);\n\n        $container = static::container($plugin) ?? static::container('');\n        foreach ($middlewares as $key => $item) {\n            $middleware = $item[0];\n            if (is_string($middleware)) {\n                $middleware = $container->get($middleware);\n            } elseif ($middleware instanceof Closure) {\n                $middleware = call_user_func($middleware, $container);\n            }\n            $middlewares[$key][0] = $middleware;\n        }\n\n        $needInject = static::isNeedInject($call, $args);\n        $anonymousArgs = array_values($args);\n        // Pre-compute return type for Response check optimization in middleware chain\n        $alwaysReturnsResponse = false;\n        if ($middlewares) {\n            try {\n                $returnType = static::getReflector($call)->getReturnType();\n                $alwaysReturnsResponse = $returnType instanceof \\ReflectionNamedType\n                    && !$returnType->allowsNull()\n                    && is_a($returnType->getName(), Response::class, true);\n            } catch (Throwable $e) {\n            }\n        }\n        if ($isController) {\n            $controllerReuse = static::config($plugin, 'app.controller_reuse', true);\n            if (!$controllerReuse) {\n                if ($needInject) {\n                    // Pre-compute metadata at closure creation time\n                    $reflector = static::getReflector($call);\n                    $metadataList = static::getMethodParameterMetadata($reflector);\n                    $debug = static::config($plugin, 'app.debug');\n                    $call = function ($request) use ($call, $plugin, $args, $container, $metadataList, $debug) {\n                        $call[0] = $container->make($call[0]);\n                        $inputs = $args ? array_merge($request->all(), $args) : $request->all();\n                        $resolvedArgs = array_values(static::resolveMethodDependenciesFromMetadata($container, $request, $inputs, $metadataList, $debug));\n                        return $call(...$resolvedArgs);\n                    };\n                    $needInject = false;\n                } else {\n                    $call = function ($request, ...$anonymousArgs) use ($call, $plugin, $container) {\n                        $call[0] = $container->make($call[0]);\n                        return $call($request, ...$anonymousArgs);\n                    };\n                }\n            } else {\n                $call[0] = $container->get($call[0]);\n            }\n        }\n\n        if ($needInject) {\n            $call = static::resolveInject($container, $call, $args, static::config($plugin, 'app.debug'));\n        }\n\n        if ($middlewares) {\n            if ($alwaysReturnsResponse) {\n                $innermost = function ($request) use ($call, $anonymousArgs) {\n                    try {\n                        return $call($request, ...$anonymousArgs);\n                    } catch (Throwable $e) {\n                        return static::exceptionResponse($e, $request);\n                    }\n                };\n            } else {\n                $innermost = function ($request) use ($call, $anonymousArgs) {\n                    try {\n                        $response = $call($request, ...$anonymousArgs);\n                    } catch (Throwable $e) {\n                        return static::exceptionResponse($e, $request);\n                    }\n                    if (!$response instanceof Response) {\n                        if (!is_string($response)) {\n                            $response = static::stringify($response);\n                        }\n                        $response = new Response(200, [], $response);\n                    }\n                    return $response;\n                };\n            }\n            $callback = array_reduce($middlewares, function ($carry, $pipe) {\n                return function ($request) use ($carry, $pipe) {\n                    try {\n                        return $pipe($request, $carry);\n                    } catch (Throwable $e) {\n                        return static::exceptionResponse($e, $request);\n                    }\n                };\n            }, $innermost);\n        } else {\n            if (!$anonymousArgs) {\n                $callback = $call;\n            } else {\n                $callback = function ($request) use ($call, $anonymousArgs) {\n                    return $call($request, ...$anonymousArgs);\n                };\n            }\n        }\n        return $callback;\n    }\n\n    /**\n     * ResolveInject.\n     * @param ContainerInterface $container\n     * @param array|Closure $call\n     * @param array $args\n     * @param bool $debug\n     * @return Closure\n     * @see Dependency injection through reflection information\n     */\n    protected static function resolveInject(ContainerInterface $container, $call, array $args, bool $debug): Closure\n    {\n        // Pre-compute metadata at closure creation time (once), not at execution time (every request)\n        $metadataList = static::getMethodParameterMetadata(static::getReflector($call));\n\n        return function (Request $request) use ($container, $call, $args, $metadataList, $debug) {\n            $inputs = $args ? array_merge($request->all(), $args) : $request->all();\n            $resolvedArgs = array_values(static::resolveMethodDependenciesFromMetadata(\n                $container, $request, $inputs, $metadataList, $debug\n            ));\n            return $call(...$resolvedArgs);\n        };\n    }\n\n    /**\n     * Check whether inject is required.\n     * @param $call\n     * @param array $args\n     * @return bool\n     * @throws ReflectionException\n     */\n    protected static function isNeedInject($call, array &$args): bool\n    {\n        if (is_array($call) && !method_exists($call[0], $call[1])) {\n            return false;\n        }\n        $reflector = static::getReflector($call);\n        $reflectionParameters = $reflector->getParameters();\n        if (!$reflectionParameters) {\n            return false;\n        }\n        $firstParameter = current($reflectionParameters);\n        unset($reflectionParameters[key($reflectionParameters)]);\n        $adaptersList = ['int', 'string', 'bool', 'array', 'object', 'float', 'mixed', 'resource'];\n        $keys = [];\n        $needInject = false;\n        foreach ($reflectionParameters as $parameter) {\n            $parameterName = $parameter->name;\n            $keys[] = $parameterName;\n            if ($parameter->hasType()) {\n                $type = $parameter->getType();\n                if (!$type instanceof \\ReflectionNamedType) {\n                    throw new \\RuntimeException(\n                        sprintf('Union/intersection types are not supported for controller parameter $%s. Use a single type instead.', $parameter->name)\n                    );\n                }\n                $typeName = $type->getName();\n                if (!in_array($typeName, $adaptersList)) {\n                    $needInject = true;\n                    continue;\n                }\n                if (!array_key_exists($parameterName, $args)) {\n                    $needInject = true;\n                    continue;\n                }\n                switch ($typeName) {\n                    case 'int':\n                    case 'float':\n                        if (!is_numeric($args[$parameterName])) {\n                            return true;\n                        }\n                        $args[$parameterName] = $typeName === 'int' ? (int)$args[$parameterName]: (float)$args[$parameterName];\n                        break;\n                    case 'bool':\n                        $args[$parameterName] = (bool)$args[$parameterName];\n                        break;\n                    case 'array':\n                    case 'object':\n                        if (!is_array($args[$parameterName])) {\n                            return true;\n                        }\n                        $args[$parameterName] = $typeName === 'array' ? $args[$parameterName] : (object)$args[$parameterName];\n                        break;\n                    case 'string':\n                    case 'mixed':\n                    case 'resource':\n                        break;\n                }\n            }\n        }\n        if (array_keys($args) !== $keys) {\n            return true;\n        }\n        if (!$firstParameter->hasType()) {\n            return $firstParameter->getName() !== 'request';\n        }\n        $firstType = $firstParameter->getType();\n        if (!$firstType instanceof \\ReflectionNamedType) {\n            return true;\n        }\n        if (!is_a(static::$requestClass, $firstType->getName(), true)) {\n            return true;\n        }\n\n        return $needInject;\n    }\n\n    /**\n     * Get reflector.\n     * @param $call\n     * @return ReflectionFunction|ReflectionMethod\n     * @throws ReflectionException\n     */\n    protected static function getReflector($call)\n    {\n        $cacheKey = static::getReflectorCacheKey($call);\n        if ($cacheKey !== null && isset(static::$reflectorCache[$cacheKey])) {\n            return static::$reflectorCache[$cacheKey];\n        }\n\n        if ($call instanceof Closure || is_string($call)) {\n            $reflector = new ReflectionFunction($call);\n        } else {\n            $reflector = new ReflectionMethod($call[0], $call[1]);\n        }\n\n        if ($cacheKey !== null) {\n            static::$reflectorCache[$cacheKey] = $reflector;\n            if (count(static::$reflectorCache) > 1024) {\n                unset(static::$reflectorCache[key(static::$reflectorCache)]);\n            }\n        }\n\n        return $reflector;\n    }\n\n    /**\n     * Get reflector cache key.\n     * @param mixed $call\n     * @return string|null\n     */\n    protected static function getReflectorCacheKey($call): ?string\n    {\n        if (is_string($call)) {\n            return 'func:' . $call;\n        }\n        if (is_array($call) && isset($call[0], $call[1])) {\n            $class = is_object($call[0]) ? get_class($call[0]) : $call[0];\n            return 'method:' . $class . '::' . $call[1];\n        }\n        // Closures may be short-lived; avoid caching to prevent key reuse risks.\n        return null;\n    }\n\n    /**\n     * Return dependent parameters\n     * @param ContainerInterface $container\n     * @param Request $request\n     * @param array $inputs\n     * @param ReflectionFunctionAbstract $reflector\n     * @param bool $debug\n     * @return array\n     * @throws ReflectionException\n     */\n    protected static function resolveMethodDependencies(ContainerInterface $container, Request $request, array $inputs, ReflectionFunctionAbstract $reflector, bool $debug): array\n    {\n        $metadataList = static::getMethodParameterMetadata($reflector);\n        return static::resolveMethodDependenciesFromMetadata($container, $request, $inputs, $metadataList, $debug);\n    }\n\n    /**\n     * Return dependent parameters from pre-computed metadata.\n     * @param ContainerInterface $container\n     * @param Request $request\n     * @param array $inputs\n     * @param array $metadataList\n     * @param bool $debug\n     * @return array\n     * @throws ReflectionException\n     */\n    protected static function resolveMethodDependenciesFromMetadata(ContainerInterface $container, Request $request, array $inputs, array $metadataList, bool $debug): array\n    {\n        $parameters = [];\n        foreach ($metadataList as $metadata) {\n            $parameterName = $metadata['name'];\n            $typeName = $metadata['type'];\n\n            if (!empty($metadata['isRequest'])) {\n                $parameters[$parameterName] = $request;\n                continue;\n            }\n\n            if (!array_key_exists($parameterName, $inputs)) {\n                if (!$metadata['hasDefault']) {\n                    if (!$typeName || (!$metadata['isClass'] && !$metadata['isEnum']) || $metadata['isEnum']) {\n                        throw (new MissingInputException())->data([\n                            'parameter' => $parameterName,\n                        ])->debug($debug);\n                    }\n                } else {\n                    $parameters[$parameterName] = $metadata['default'];\n                    continue;\n                }\n            }\n\n            $parameterValue = $inputs[$parameterName] ?? null;\n\n            switch ($typeName) {\n                case 'int':\n                case 'float':\n                    if (!is_numeric($parameterValue)) {\n                        throw (new InputTypeException())->data([\n                            'parameter' => $parameterName,\n                            'exceptType' => $typeName,\n                            'actualType' => gettype($parameterValue),\n                        ])->debug($debug);\n                    }\n                    $parameters[$parameterName] = $typeName === 'float' ? (float)$parameterValue :  (int)$parameterValue;\n                    break;\n                case 'bool':\n                    $parameters[$parameterName] = (bool)$parameterValue;\n                    break;\n                case 'array':\n                case 'object':\n                    if (!is_array($parameterValue)) {\n                        throw (new InputTypeException())->data([\n                            'parameter' => $parameterName,\n                            'exceptType' => $typeName,\n                            'actualType' => gettype($parameterValue),\n                        ])->debug($debug);\n                    }\n                    $parameters[$parameterName] = $typeName === 'object' ? (object)$parameterValue : $parameterValue;\n                    break;\n                case 'string':\n                case 'mixed':\n                case 'resource':\n                case null:\n                    $parameters[$parameterName] = $parameterValue;\n                    break;\n                default:\n                    $subInputs = is_array($parameterValue) ? $parameterValue : [];\n                    if (!empty($metadata['isModel'])) {\n                        $parameters[$parameterName] = $container->make($typeName, [\n                            'attributes' => $subInputs\n                        ]);\n                        break;\n                    }\n                    if (!empty($metadata['isThinkModel'])) {\n                        $parameters[$parameterName] = $container->make($typeName, [\n                            'data' => $subInputs\n                        ]);\n                        break;\n                    }\n                    if (!empty($metadata['isEnum'])) {\n                        // Use pre-computed enum case mappings (avoids per-request ReflectionEnum)\n                        if (isset($metadata['enumCases'][$parameterValue])) {\n                            $parameters[$parameterName] = $metadata['enumCases'][$parameterValue];\n                            break;\n                        }\n                        if (!empty($metadata['enumIsBacked']) && isset($metadata['enumBackedValues'][$parameterValue])) {\n                            $parameters[$parameterName] = $metadata['enumBackedValues'][$parameterValue];\n                            break;\n                        }\n                        throw (new InputValueException())->data([\n                            'parameter' => $parameterName,\n                            'enum' => $typeName\n                        ])->debug($debug);\n                    }\n                    if (is_array($subInputs) && !empty($metadata['hasConstructor'])) {\n                        $constructorReflector = static::getReflector([$typeName, '__construct']);\n                        $parameters[$parameterName] = $container->make($typeName, static::resolveMethodDependencies($container, $request, $subInputs, $constructorReflector, $debug));\n                    } else {\n                        $parameters[$parameterName] = $container->make($typeName);\n                    }\n                    break;\n            }\n        }\n        return $parameters;\n    }\n\n    /**\n     * Get method parameter metadata from cache.\n     * @param ReflectionFunctionAbstract $reflector\n     * @return array<int, array<string, mixed>>\n     * @throws ReflectionException\n     */\n    protected static function getMethodParameterMetadata(ReflectionFunctionAbstract $reflector): array\n    {\n        $cacheKey = static::getParameterMetadataCacheKey($reflector);\n        if ($cacheKey !== null && isset(static::$parameterMetadataCache[$cacheKey])) {\n            return static::$parameterMetadataCache[$cacheKey];\n        }\n\n        $metadataList = [];\n        foreach ($reflector->getParameters() as $parameter) {\n            $type = $parameter->getType();\n            if ($type !== null && !$type instanceof \\ReflectionNamedType) {\n                throw new \\RuntimeException(\n                    sprintf('Union/intersection types are not supported for controller parameter $%s. Use a single type instead.', $parameter->name)\n                );\n            }\n            $typeName = $type?->getName();\n            $hasDefault = $parameter->isDefaultValueAvailable();\n            $isEnum = $typeName && enum_exists($typeName);\n            $isClass = $typeName && class_exists($typeName);\n            $isRequest = $typeName && is_a(static::$requestClass, $typeName, true);\n            $isModel = $typeName && is_a($typeName, Model::class, true);\n            $isThinkModel = $typeName && is_a($typeName, ThinkModel::class, true);\n            $metadata = [\n                'name' => $parameter->name,\n                'type' => $typeName,\n                'hasDefault' => $hasDefault,\n                'default' => $hasDefault ? $parameter->getDefaultValue() : null,\n                'isRequest' => $isRequest,\n                'isEnum' => $isEnum,\n                'isClass' => $isClass,\n                'isModel' => $isModel,\n                'isThinkModel' => $isThinkModel,\n            ];\n            // Pre-compute enum case mappings to avoid per-request ReflectionEnum\n            if ($isEnum) {\n                $enumReflection = new ReflectionEnum($typeName);\n                $enumCases = [];\n                $enumBackedValues = [];\n                $isBacked = $enumReflection->isBacked();\n                foreach ($enumReflection->getCases() as $case) {\n                    $caseValue = $case->getValue();\n                    $enumCases[$case->getName()] = $caseValue;\n                    if ($isBacked) {\n                        $enumBackedValues[$caseValue->value] = $caseValue;\n                    }\n                }\n                $metadata['enumCases'] = $enumCases;\n                $metadata['enumBackedValues'] = $enumBackedValues;\n                $metadata['enumIsBacked'] = $isBacked;\n            }\n            // Pre-compute class constructor info to avoid per-request ReflectionClass\n            if ($isClass && !$isRequest && !$isEnum && !$isModel && !$isThinkModel) {\n                $classRef = new ReflectionClass($typeName);\n                $constructor = $classRef->getConstructor();\n                $metadata['hasConstructor'] = $constructor !== null;\n                if ($constructor) {\n                    // Pre-cache constructor reflector for use by getReflector()\n                    $constructorKey = 'method:' . $typeName . '::__construct';\n                    if (!isset(static::$reflectorCache[$constructorKey])) {\n                        static::$reflectorCache[$constructorKey] = $constructor;\n                    }\n                }\n            }\n            $metadataList[] = $metadata;\n        }\n\n        if ($cacheKey !== null) {\n            static::$parameterMetadataCache[$cacheKey] = $metadataList;\n            if (count(static::$parameterMetadataCache) > 1024) {\n                unset(static::$parameterMetadataCache[key(static::$parameterMetadataCache)]);\n            }\n        }\n\n        return $metadataList;\n    }\n\n    /**\n     * Get parameter metadata cache key.\n     * @param ReflectionFunctionAbstract $reflector\n     * @return string|null\n     */\n    protected static function getParameterMetadataCacheKey(ReflectionFunctionAbstract $reflector): ?string\n    {\n        if ($reflector instanceof ReflectionMethod) {\n            return 'method:' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName();\n        }\n        if ($reflector instanceof ReflectionFunction && $reflector->isClosure()) {\n            return null;\n        }\n        if ($reflector instanceof ReflectionFunction) {\n            return 'func:' . $reflector->getName();\n        }\n        return null;\n    }\n\n    /**\n     * Container.\n     * @param string $plugin\n     * @return ContainerInterface\n     */\n    public static function container(string $plugin = '')\n    {\n        return static::config($plugin, 'container');\n    }\n\n    /**\n     * Get request.\n     * @return Request|\\support\\Request\n     */\n    public static function request()\n    {\n        return Context::get(Request::class);\n    }\n\n    /**\n     * Get worker.\n     * @return Worker\n     */\n    public static function worker(): ?Worker\n    {\n        return static::$worker;\n    }\n\n    /**\n     * Find Route.\n     * @param TcpConnection $connection\n     * @param string $path\n     * @param string $key\n     * @param $request\n     * @param $status\n     * @return bool\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     * @throws ReflectionException|Throwable\n     */\n    protected static function findRoute(TcpConnection $connection, string $path, string $key, $request, &$status): bool\n    {\n        $routeInfo = Route::dispatch($request->method(), $path);\n        if ($routeInfo[0] === Dispatcher::FOUND) {\n            $status = 200;\n            $routeInfo[0] = 'route';\n            $callback = $routeInfo[1]['callback'];\n            $route = clone $routeInfo[1]['route'];\n            $app = $controller = $action = '';\n            $args = !empty($routeInfo[2]) ? $routeInfo[2] : [];\n            if ($args) {\n                $route->setParams($args);\n            }\n            $args = array_merge($route->param(), $args);\n            if (is_array($callback)) {\n                $controller = $callback[0];\n                $plugin = static::getPluginByClass($controller);\n                $app = static::getAppByController($controller);\n                $action = static::getRealMethod($controller, $callback[1]) ?? '';\n            } else {\n                $plugin = static::getPluginByPath($path);\n            }\n            $callback = static::getCallback($plugin, $app, $callback, $args, true, $route);\n            static::collectCallbacks($key, [$callback, $plugin, $app, $controller ?: '', $action, $route]);\n            [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];\n            static::send($connection, $callback($request), $request);\n            return true;\n        }\n        $status = $routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED ? 405 : 404;\n        return false;\n    }\n\n    /**\n     * Find File.\n     * @param TcpConnection $connection\n     * @param string $path\n     * @param string $key\n     * @param $request\n     * @return bool\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     * @throws ReflectionException\n     */\n    protected static function findFile(TcpConnection $connection, string $path, string $key, $request): bool\n    {\n        if (preg_match('/%[0-9a-f]{2}/i', $path)) {\n            $path = urldecode($path);\n            if (static::unsafeUri($connection, $path, $request)) {\n                return true;\n            }\n        }\n\n        $pathExplodes = explode('/', trim($path, '/'));\n        $plugin = '';\n        if (isset($pathExplodes[1]) && $pathExplodes[0] === 'app') {\n            $plugin = $pathExplodes[1];\n            $publicDir = static::config($plugin, 'app.public_path') ?: BASE_PATH . \"/plugin/$pathExplodes[1]/public\";\n            $path = substr($path, strlen(\"/app/$pathExplodes[1]/\"));\n        } else {\n            $publicDir = static::$publicPath;\n        }\n        $file = \"$publicDir/$path\";\n        if (!is_file($file)) {\n            return false;\n        }\n\n        if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {\n            if (!static::config($plugin, 'app.support_php_files', false)) {\n                return false;\n            }\n            static::collectCallbacks($key, [function () use ($file) {\n                return static::execPhpFile($file);\n            }, $plugin, '', '', '', null]);\n            [, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];\n            static::send($connection, static::execPhpFile($file), $request);\n            return true;\n        }\n\n        if (!static::config($plugin, 'static.enable', false)) {\n            return false;\n        }\n\n        static::collectCallbacks($key, [static::getCallback($plugin, '__static__', function ($request) use ($file, $plugin) {\n            clearstatcache(true, $file);\n            if (!is_file($file)) {\n                $callback = static::getFallback($plugin);\n                return $callback($request);\n            }\n            return (new Response())->file($file);\n        }, [], false), '', '', '', '', null]);\n        [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];\n        static::send($connection, $callback($request), $request);\n        return true;\n    }\n\n    /**\n     * Send.\n     * @param TcpConnection|mixed $connection\n     * @param mixed|Response $response\n     * @param Request|mixed $request\n     * @return void\n     */\n    protected static function send($connection, $response, $request)\n    {\n        Context::destroy();\n        // Remove the reference of request to session.\n        unset($request->context['session']);\n        $keepAlive = $request->header('connection');\n        if ($keepAlive === null) {\n            if ($request->protocolVersion() === '1.1') {\n                $connection->send($response);\n                return;\n            }\n        } elseif (\\strcasecmp($keepAlive, 'keep-alive') === 0) {\n            $connection->send($response);\n            return;\n        }\n        if ($response instanceof Response && $response->getHeader('Transfer-Encoding') === 'chunked') {\n            $connection->send($response);\n            return;\n        }\n        $connection->close($response);\n    }\n\n    /**\n     * ParseControllerAction.\n     * @param string $path\n     * @return array|false\n     * @throws ReflectionException\n     */\n    protected static function parseControllerAction(string $path)\n    {\n        $path = str_replace(['-', '//'], ['', '/'], $path);\n        if (static::containsPathTraversal($path)) {\n            return false;\n        }\n        static $cache = [];\n        if (isset($cache[$path])) {\n            return $cache[$path];\n        }\n        $pathExplode = explode('/', trim($path, '/'));\n        $isPlugin = isset($pathExplode[1]) && $pathExplode[0] === 'app';\n        $configPrefix = $isPlugin ? \"plugin.$pathExplode[1].\" : '';\n        $pathPrefix = $isPlugin ? \"/app/$pathExplode[1]\" : '';\n        $classPrefix = $isPlugin ? \"plugin\\\\$pathExplode[1]\" : '';\n        $suffix = Config::get(\"{$configPrefix}app.controller_suffix\", '');\n        $relativePath = trim(substr($path, strlen($pathPrefix)), '/');\n        $pathExplode = $relativePath ? explode('/', $relativePath) : [];\n\n        $action = 'index';\n        if (!$controllerAction = static::guessControllerAction($pathExplode, $action, $suffix, $classPrefix)) {\n            if (count($pathExplode) <= 1) {\n                return false;\n            }\n            $action = end($pathExplode);\n            unset($pathExplode[count($pathExplode) - 1]);\n            $controllerAction = static::guessControllerAction($pathExplode, $action, $suffix, $classPrefix);\n        }\n        if ($controllerAction && !isset($path[256])) {\n            $cache[$path] = $controllerAction;\n            if (count($cache) > 1024) {\n                unset($cache[key($cache)]);\n            }\n        }\n        return $controllerAction;\n    }\n\n    /**\n     * GuessControllerAction.\n     * @param $pathExplode\n     * @param $action\n     * @param $suffix\n     * @param $classPrefix\n     * @return array|false\n     * @throws ReflectionException\n     */\n    protected static function guessControllerAction($pathExplode, $action, $suffix, $classPrefix)\n    {\n        $map[] = trim(\"$classPrefix\\\\app\\\\controller\\\\\" . implode('\\\\', $pathExplode), '\\\\');\n        foreach ($pathExplode as $index => $section) {\n            $tmp = $pathExplode;\n            array_splice($tmp, $index, 1, [$section, 'controller']);\n            $map[] = trim(\"$classPrefix\\\\\" . implode('\\\\', array_merge(['app'], $tmp)), '\\\\');\n        }\n        foreach ($map as $item) {\n            $map[] = $item . '\\\\index';\n        }\n        foreach ($map as $controllerClass) {\n            // Remove xx\\xx\\controller\n            if (substr($controllerClass, -11) === '\\\\controller') {\n                continue;\n            }\n            $controllerClass .= $suffix;\n            if ($controllerAction = static::getControllerAction($controllerClass, $action)) {\n                return $controllerAction;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * GetControllerAction.\n     * @param string $controllerClass\n     * @param string $action\n     * @return array|false\n     * @throws ReflectionException\n     */\n    protected static function getControllerAction(string $controllerClass, string $action)\n    {\n        // Disable calling magic methods\n        if (strpos($action, '__') === 0) {\n            return false;\n        }\n        if (($controllerClass = static::getController($controllerClass)) && ($action = static::getAction($controllerClass, $action))) {\n            return [\n                'plugin' => static::getPluginByClass($controllerClass),\n                'app' => static::getAppByController($controllerClass),\n                'controller' => $controllerClass,\n                'action' => $action\n            ];\n        }\n        return false;\n    }\n\n    /**\n     * GetController.\n     * @param string $controllerClass\n     * @return string|false\n     * @throws ReflectionException\n     */\n    protected static function getController(string $controllerClass)\n    {\n        if (class_exists($controllerClass)) {\n            return (new ReflectionClass($controllerClass))->name;\n        }\n        $explodes = explode('\\\\', strtolower(ltrim($controllerClass, '\\\\')));\n        $basePath = $explodes[0] === 'plugin' ? BASE_PATH . '/plugin' : static::$appPath;\n        unset($explodes[0]);\n        $fileName = array_pop($explodes) . '.php';\n        $found = true;\n        foreach ($explodes as $pathSection) {\n            if (!$found) {\n                break;\n            }\n            $dirs = Util::scanDir($basePath, false);\n            $found = false;\n            foreach ($dirs as $name) {\n                $path = \"$basePath/$name\";\n\n                if (is_dir($path) && strtolower($name) === $pathSection) {\n                    $basePath = $path;\n                    $found = true;\n                    break;\n                }\n            }\n        }\n        if (!$found) {\n            return false;\n        }\n        foreach (scandir($basePath) ?: [] as $name) {\n            if (strtolower($name) === $fileName) {\n                require_once \"$basePath/$name\";\n                if (class_exists($controllerClass, false)) {\n                    return (new ReflectionClass($controllerClass))->name;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * GetAction.\n     * @param string $controllerClass\n     * @param string $action\n     * @return string|false\n     */\n    protected static function getAction(string $controllerClass, string $action)\n    {\n        $methods = get_class_methods($controllerClass);\n        $lowerAction = strtolower($action);\n        $found = false;\n        foreach ($methods as $candidate) {\n            if (strtolower($candidate) === $lowerAction) {\n                $action = $candidate;\n                $found = true;\n                break;\n            }\n        }\n        if ($found) {\n            return $action;\n        }\n        // Action is not public method\n        if (method_exists($controllerClass, $action)) {\n            return false;\n        }\n        if (method_exists($controllerClass, '__call')) {\n            return $action;\n        }\n        return false;\n    }\n\n    /**\n     * GetPluginByClass.\n     * @param string $controllerClass\n     * @return mixed|string\n     */\n    public static function getPluginByClass(string $controllerClass)\n    {\n        $controllerClass = trim($controllerClass, '\\\\');\n        $tmp = explode('\\\\', $controllerClass, 3);\n        if ($tmp[0] !== 'plugin') {\n            return '';\n        }\n        return $tmp[1] ?? '';\n    }\n\n    /**\n     * GetPluginByPath.\n     * @param string $path\n     * @return mixed|string\n     */\n    public static function getPluginByPath(string $path)\n    {\n        $path = trim($path, '/');\n        $tmp = explode('/', $path, 3);\n        if ($tmp[0] !== 'app') {\n            return '';\n        }\n        $plugin = $tmp[1] ?? '';\n        if ($plugin && !static::config('', \"plugin.$plugin.app\")) {\n            return '';\n        }\n        return $plugin;\n    }\n\n    /**\n     * GetAppByController.\n     * @param string $controllerClass\n     * @return mixed|string\n     */\n    protected static function getAppByController(string $controllerClass)\n    {\n        $controllerClass = trim($controllerClass, '\\\\');\n        $tmp = explode('\\\\', $controllerClass, 5);\n        $pos = $tmp[0] === 'plugin' ? 3 : 1;\n        if (!isset($tmp[$pos])) {\n            return '';\n        }\n        return strtolower($tmp[$pos]) === 'controller' ? '' : $tmp[$pos];\n    }\n\n    /**\n     * ExecPhpFile.\n     * @param string $file\n     * @return false|string\n     */\n    public static function execPhpFile(string $file)\n    {\n        ob_start();\n        // Try to include php file.\n        try {\n            include $file;\n        } catch (Throwable $e) {\n            ob_get_clean();\n            throw $e;\n        }\n        return ob_get_clean();\n    }\n\n    /**\n     * GetRealMethod.\n     * @param string $class\n     * @param string $method\n     * @return string\n     */\n    protected static function getRealMethod(string $class, string $method): string\n    {\n        $method = strtolower($method);\n        $methods = get_class_methods($class);\n        foreach ($methods as $candidate) {\n            if (strtolower($candidate) === $method) {\n                return $candidate;\n            }\n        }\n        return $method;\n    }\n\n    /**\n     * Config.\n     * @param string $plugin\n     * @param string $key\n     * @param mixed $default\n     * @return mixed\n     */\n    protected static function config(string $plugin, string $key, mixed $default = null)\n    {\n        return Config::get($plugin ? \"plugin.$plugin.$key\" : $key, $default);\n    }\n\n\n    /**\n     * @param mixed $data\n     * @return string\n     */\n    protected static function stringify($data): string\n    {\n        $type = gettype($data);\n        switch ($type) {\n            case 'boolean':\n                return  $data ? 'true' : 'false';\n            case 'NULL':\n                return 'NULL';\n            case 'array':\n                return 'Array';\n            case 'object':\n                if (!method_exists($data, '__toString')) {\n                    return 'Object';\n                }\n            default:\n                return (string)$data;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Bootstrap.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman;\n\nuse Workerman\\Worker;\n\ninterface Bootstrap\n{\n    /**\n     * onWorkerStart\n     *\n     * @param Worker|null $worker\n     * @return mixed\n     */\n    public static function start(?Worker $worker);\n}\n"
  },
  {
    "path": "src/Config.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman;\n\nuse FilesystemIterator;\nuse RecursiveDirectoryIterator;\nuse RecursiveIteratorIterator;\nuse function array_replace_recursive;\nuse function array_reverse;\nuse function count;\nuse function explode;\nuse function in_array;\nuse function is_array;\nuse function is_dir;\nuse function is_file;\nuse function key;\nuse function str_replace;\n\nclass Config\n{\n\n    /**\n     * @var array\n     */\n    protected static $config = [];\n\n    /**\n     * @var string\n     */\n    protected static $configPath = '';\n\n    /**\n     * @var bool\n     */\n    protected static $loaded = false;\n\n    /**\n     * @var array Flat cache for repeated get() lookups.\n     */\n    protected static $flatCache = [];\n\n    /**\n     * Load.\n     * @param string $configPath\n     * @param array $excludeFile\n     * @param string|null $key\n     * @return void\n     */\n    public static function load(string $configPath, array $excludeFile = [], ?string $key = null)\n    {\n        static::$configPath = $configPath;\n        static::$flatCache = [];\n        if (!$configPath) {\n            return;\n        }\n        static::$loaded = false;\n        $config = static::loadFromDir($configPath, $excludeFile);\n        if (!$config) {\n            static::$loaded = true;\n            return;\n        }\n        if ($key !== null) {\n            foreach (array_reverse(explode('.', $key)) as $k) {\n                $config = [$k => $config];\n            }\n        }\n        static::$config = array_replace_recursive(static::$config, $config);\n        static::formatConfig();\n        static::$loaded = true;\n    }\n\n    /**\n     * This deprecated method will certainly be removed in the future.\n     * @param string $configPath\n     * @param array $excludeFile\n     * @return void\n     * @deprecated\n     */\n    public static function reload(string $configPath, array $excludeFile = [])\n    {\n        static::load($configPath, $excludeFile);\n    }\n\n    /**\n     * Clear.\n     * @return void\n     */\n    public static function clear()\n    {\n        static::$config = [];\n        static::$flatCache = [];\n    }\n\n    /**\n     * FormatConfig.\n     * @return void\n     */\n    protected static function formatConfig()\n    {\n        $config = static::$config;\n        // Merge log config\n        foreach ($config['plugin'] ?? [] as $firm => $projects) {\n            if (isset($projects['app'])) {\n                foreach ($projects['log'] ?? [] as $key => $item) {\n                    $config['log'][\"plugin.$firm.$key\"] = $item;\n                }\n            }\n            foreach ($projects as $name => $project) {\n                if (!is_array($project)) {\n                    continue;\n                }\n                foreach ($project['log'] ?? [] as $key => $item) {\n                    $config['log'][\"plugin.$firm.$name.$key\"] = $item;\n                }\n            }\n        }\n        // Merge database config\n        foreach ($config['plugin'] ?? [] as $firm => $projects) {\n            if (isset($projects['app'])) {\n                foreach ($projects['database']['connections'] ?? [] as $key => $connection) {\n                    $config['database']['connections'][\"plugin.$firm.$key\"] = $connection;\n                }\n            }\n            foreach ($projects as $name => $project) {\n                if (!is_array($project)) {\n                    continue;\n                }\n                foreach ($project['database']['connections'] ?? [] as $key => $connection) {\n                    $config['database']['connections'][\"plugin.$firm.$name.$key\"] = $connection;\n                }\n            }\n        }\n        if (!empty($config['database']['connections'])) {\n            $config['database']['default'] = $config['database']['default'] ?? key($config['database']['connections']);\n        }\n        // Merge thinkorm config\n        foreach ($config['plugin'] ?? [] as $firm => $projects) {\n            if (isset($projects['app'])) {\n                foreach ($projects['thinkorm']['connections'] ?? [] as $key => $connection) {\n                    $config['thinkorm']['connections'][\"plugin.$firm.$key\"] = $connection;\n                }\n                foreach ($projects['think-orm']['connections'] ?? [] as $key => $connection) {\n                    $config['think-orm']['connections'][\"plugin.$firm.$key\"] = $connection;\n                }\n            }\n            foreach ($projects as $name => $project) {\n                if (!is_array($project)) {\n                    continue;\n                }\n                foreach ($project['thinkorm']['connections'] ?? [] as $key => $connection) {\n                    $config['thinkorm']['connections'][\"plugin.$firm.$name.$key\"] = $connection;\n                }\n                foreach ($project['think-orm']['connections'] ?? [] as $key => $connection) {\n                    $config['think-orm']['connections'][\"plugin.$firm.$name.$key\"] = $connection;\n                }\n            }\n        }\n        if (!empty($config['thinkorm']['connections'])) {\n            $config['thinkorm']['default'] = $config['thinkorm']['default'] ?? key($config['thinkorm']['connections']);\n        }\n        if (!empty($config['think-orm']['connections'])) {\n            $config['think-orm']['default'] = $config['think-orm']['default'] ?? key($config['think-orm']['connections']);\n        }\n        // Merge redis config\n        foreach ($config['plugin'] ?? [] as $firm => $projects) {\n            if (isset($projects['app'])) {\n                foreach ($projects['redis'] ?? [] as $key => $connection) {\n                    $config['redis'][\"plugin.$firm.$key\"] = $connection;\n                }\n            }\n            foreach ($projects as $name => $project) {\n                if (!is_array($project)) {\n                    continue;\n                }\n                foreach ($project['redis'] ?? [] as $key => $connection) {\n                    $config['redis'][\"plugin.$firm.$name.$key\"] = $connection;\n                }\n            }\n        }\n        static::$config = $config;\n    }\n\n    /**\n     * LoadFromDir.\n     * @param string $configPath\n     * @param array $excludeFile\n     * @return array\n     */\n    public static function loadFromDir(string $configPath, array $excludeFile = []): array\n    {\n        $allConfig = [];\n        $dirIterator = new RecursiveDirectoryIterator($configPath, FilesystemIterator::FOLLOW_SYMLINKS);\n        $iterator = new RecursiveIteratorIterator($dirIterator);\n        foreach ($iterator as $file) {\n            /** var SplFileInfo $file */\n            if (is_dir($file) || $file->getExtension() != 'php' || in_array($file->getBaseName('.php'), $excludeFile)) {\n                continue;\n            }\n            $appConfigFile = $file->getPath() . '/app.php';\n            if (!is_file($appConfigFile)) {\n                continue;\n            }\n            $relativePath = str_replace($configPath . DIRECTORY_SEPARATOR, '', substr($file, 0, -4));\n            $explode = array_reverse(explode(DIRECTORY_SEPARATOR, $relativePath));\n            if (count($explode) >= 2) {\n                $appConfig = include $appConfigFile;\n                if (empty($appConfig['enable'])) {\n                    continue;\n                }\n            }\n            $config = include $file;\n            foreach ($explode as $section) {\n                $tmp = [];\n                $tmp[$section] = $config;\n                $config = $tmp;\n            }\n            $allConfig = array_replace_recursive($allConfig, $config);\n        }\n        return $allConfig;\n    }\n\n    /**\n     * Get.\n     * @param string|null $key\n     * @param mixed $default\n     * @return mixed\n     */\n    public static function get(?string $key = null, mixed $default = null)\n    {\n        if ($key === null) {\n            return static::$config;\n        }\n        if (isset(static::$flatCache[$key])) {\n            return static::$flatCache[$key];\n        }\n        $keyArray = explode('.', $key);\n        $value = static::$config;\n        $found = true;\n        foreach ($keyArray as $index) {\n            if (!isset($value[$index])) {\n                if (static::$loaded) {\n                    return $default;\n                }\n                $found = false;\n                break;\n            }\n            $value = $value[$index];\n        }\n        if ($found) {\n            if (static::$loaded) {\n                static::$flatCache[$key] = $value;\n                if (count(static::$flatCache) > 1024) {\n                    unset(static::$flatCache[key(static::$flatCache)]);\n                }\n            }\n            return $value;\n        }\n        return static::read($key, $default);\n    }\n\n    /**\n     * Read.\n     * @param string $key\n     * @param mixed $default\n     * @return mixed\n     */\n    protected static function read(string $key, mixed $default = null)\n    {\n        $path = static::$configPath;\n        if ($path === '') {\n            return $default;\n        }\n        $keys = $keyArray = explode('.', $key);\n        foreach ($keyArray as $index => $section) {\n            unset($keys[$index]);\n            if (is_file($file = \"$path/$section.php\")) {\n                $config = include $file;\n                return static::find($keys, $config, $default);\n            }\n            if (!is_dir($path = \"$path/$section\")) {\n                return $default;\n            }\n        }\n        return $default;\n    }\n\n    /**\n     * Find.\n     * @param array $keyArray\n     * @param mixed $stack\n     * @param mixed $default\n     * @return array|mixed\n     */\n    protected static function find(array $keyArray, $stack, $default)\n    {\n        if (!is_array($stack)) {\n            return $default;\n        }\n        $value = $stack;\n        foreach ($keyArray as $index) {\n            if (!isset($value[$index])) {\n                return $default;\n            }\n            $value = $value[$index];\n        }\n        return $value;\n    }\n\n}\n"
  },
  {
    "path": "src/Container.php",
    "content": "<?php\n\nnamespace Webman;\n\nuse Psr\\Container\\ContainerInterface;\nuse Webman\\Exception\\NotFoundException;\nuse function array_key_exists;\nuse function class_exists;\n\n/**\n * Class Container\n * @package Webman\n */\nclass Container implements ContainerInterface\n{\n\n    /**\n     * @var array\n     */\n    protected $instances = [];\n    /**\n     * @var array\n     */\n    protected $definitions = [];\n\n    /**\n     * Get.\n     * @param string $name\n     * @return mixed\n     * @throws NotFoundException\n     */\n    public function get(string $name)\n    {\n        if (!isset($this->instances[$name])) {\n            if (isset($this->definitions[$name])) {\n                $this->instances[$name] = call_user_func($this->definitions[$name], $this);\n            } else {\n                if (!class_exists($name)) {\n                    throw new NotFoundException(\"Class '$name' not found\");\n                }\n                $this->instances[$name] = new $name();\n            }\n        }\n        return $this->instances[$name];\n    }\n\n    /**\n     * Has.\n     * @param string $name\n     * @return bool\n     */\n    public function has(string $name): bool\n    {\n        return array_key_exists($name, $this->instances)\n            || array_key_exists($name, $this->definitions);\n    }\n\n    /**\n     * Make.\n     * @param string $name\n     * @param array $constructor\n     * @return mixed\n     * @throws NotFoundException\n     */\n    public function make(string $name, array $constructor = [])\n    {\n        if (!class_exists($name)) {\n            throw new NotFoundException(\"Class '$name' not found\");\n        }\n        return new $name(... array_values($constructor));\n    }\n\n    /**\n     * AddDefinitions.\n     * @param array $definitions\n     * @return $this\n     */\n    public function addDefinitions(array $definitions): Container\n    {\n        $this->definitions = array_merge($this->definitions, $definitions);\n        return $this;\n    }\n\n}\n"
  },
  {
    "path": "src/Context.php",
    "content": "<?php\n\nnamespace Webman;\n\nuse Workerman\\Coroutine\\Context as WorkermanContext;\nuse Workerman\\Coroutine\\Utils\\DestructionWatcher;\nuse Closure;\n\n/**\n * Class Context\n * @package Webman\n */\nclass Context extends WorkermanContext\n{\n    public static function onDestroy(Closure $closure): void\n    {\n        $obj = static::get('context.onDestroy');\n        if (!$obj) {\n            $obj = new \\stdClass();\n            static::set('context.onDestroy', $obj);\n        }\n        DestructionWatcher::watch($obj, $closure);\n    }\n}"
  },
  {
    "path": "src/Exception/BusinessException.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Exception;\n\nuse RuntimeException;\nuse Throwable;\nuse Webman\\Http\\Request;\nuse Webman\\Http\\Response;\nuse function json_encode;\n\n/**\n * Class BusinessException\n * @package support\\exception\n */\nclass BusinessException extends RuntimeException\n{\n\n    /**\n     * @var array\n     */\n    protected $data = [];\n\n    /**\n     * @var bool\n     */\n    protected $debug = false;\n\n    /**\n     * Render an exception into an HTTP response.\n     * @param Request $request\n     * @return Response|null\n     */\n    public function render(Request $request): ?Response\n    {\n        if ($request->expectsJson()) {\n            $code = $this->getCode();\n            $json = ['code' => $code ?: 500, 'msg' => $this->getMessage(), 'data' => $this->data];\n            return new Response(200, ['Content-Type' => 'application/json'],\n                json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));\n        }\n        return new Response(200, [], $this->getMessage());\n    }\n\n    /**\n     * Set data.\n     * @param array|null $data\n     * @return array|$this\n     */\n    public function data(?array $data = null): array|static\n    {\n        if ($data === null) {\n            return $this->data;\n        }\n        $this->data = $data;\n        return $this;\n    }\n\n    /**\n     * Set debug.\n     * @param bool|null $value\n     * @return $this|bool\n     */\n    public function debug(?bool $value = null): bool|static\n    {\n        if ($value === null) {\n            return $this->debug;\n        }\n        $this->debug = $value;\n        return $this;\n    }\n\n    /**\n     * Get data.\n     * @return array\n     */\n    public function getData(): array\n    {\n        return $this->data;\n    }\n\n    /**\n     * Translate message.\n     * @param string $message\n     * @param array $parameters\n     * @param string|null $domain\n     * @param string|null $locale\n     * @return string\n     */\n    protected function trans(string $message, array $parameters = [], ?string $domain = null, ?string $locale = null): string\n    {\n        $args = [];\n        foreach ($parameters as $key => $parameter) {\n            $args[\":$key\"] = $parameter;\n        }\n        try {\n            $message = trans($message, $args, $domain, $locale);\n        } catch (Throwable $e) {\n        }\n        foreach ($parameters as $key => $value) {\n            $message = str_replace(\":$key\", $value, $message);\n        }\n        return $message;\n    }\n\n}\n"
  },
  {
    "path": "src/Exception/ExceptionHandler.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Exception;\n\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\nuse Webman\\Http\\Request;\nuse Webman\\Http\\Response;\nuse function json_encode;\nuse function nl2br;\nuse function trim;\n\n/**\n * Class Handler\n * @package support\\exception\n */\nclass ExceptionHandler implements ExceptionHandlerInterface\n{\n    /**\n     * @var LoggerInterface\n     */\n    protected $logger = null;\n\n    /**\n     * @var bool\n     */\n    protected $debug = false;\n\n    /**\n     * @var array\n     */\n    public $dontReport = [];\n\n    /**\n     * ExceptionHandler constructor.\n     * @param $logger\n     * @param $debug\n     */\n    public function __construct($logger, $debug)\n    {\n        $this->logger = $logger;\n        $this->debug = $debug;\n    }\n\n    /**\n     * @param Throwable $exception\n     * @return void\n     */\n    public function report(Throwable $exception)\n    {\n        if ($this->shouldntReport($exception)) {\n            return;\n        }\n        $logs = '';\n        if ($request = \\request()) {\n            $logs = $request->getRealIp() . ' ' . $request->method() . ' ' . trim($request->fullUrl(), '/');\n        }\n        $this->logger->error($logs . PHP_EOL . $exception);\n    }\n\n    /**\n     * @param Request $request\n     * @param Throwable $exception\n     * @return Response\n     */\n    public function render(Request $request, Throwable $exception): Response\n    {\n        if (method_exists($exception, 'render') && ($response = $exception->render($request))) {\n            return $response;\n        }\n        $code = $exception->getCode();\n        if ($request->expectsJson()) {\n            $json = ['code' => $code ?: 500, 'msg' => $this->debug ? $exception->getMessage() : 'Server internal error'];\n            $this->debug && $json['traces'] = (string)$exception;\n            return new Response(200, ['Content-Type' => 'application/json'],\n                json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));\n        }\n        $error = $this->debug ? nl2br((string)$exception) : 'Server internal error';\n        return new Response(500, [], $error);\n    }\n\n    /**\n     * @param Throwable $e\n     * @return bool\n     */\n    protected function shouldntReport(Throwable $e): bool\n    {\n        foreach ($this->dontReport as $type) {\n            if ($e instanceof $type) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Compatible $this->_debug\n     *\n     * @param string $name\n     * @return bool|null\n     */\n    public function __get(string $name)\n    {\n        if ($name === '_debug') {\n            return $this->debug;\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Exception/ExceptionHandlerInterface.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Exception;\n\nuse Throwable;\nuse Webman\\Http\\Request;\nuse Webman\\Http\\Response;\n\ninterface ExceptionHandlerInterface\n{\n    /**\n     * @param Throwable $exception\n     * @return mixed\n     */\n    public function report(Throwable $exception);\n\n    /**\n     * @param Request $request\n     * @param Throwable $exception\n     * @return Response\n     */\n    public function render(Request $request, Throwable $exception): Response;\n}"
  },
  {
    "path": "src/Exception/FileException.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Exception;\n\nuse RuntimeException;\n\n/**\n * Class FileException\n * @package Webman\\Exception\n */\nclass FileException extends RuntimeException\n{\n}"
  },
  {
    "path": "src/Exception/NotFoundException.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Exception;\n\nuse Psr\\Container\\NotFoundExceptionInterface;\n\n/**\n * Class NotFoundException\n * @package Webman\\Exception\n */\nclass NotFoundException extends \\Exception implements NotFoundExceptionInterface\n{\n}\n"
  },
  {
    "path": "src/File.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman;\n\nuse SplFileInfo;\nuse Webman\\Exception\\FileException;\nuse function chmod;\nuse function is_dir;\nuse function mkdir;\nuse function pathinfo;\nuse function restore_error_handler;\nuse function set_error_handler;\nuse function sprintf;\nuse function strip_tags;\nuse function umask;\n\nclass File extends SplFileInfo\n{\n\n    /**\n     * Move.\n     * @param string $destination\n     * @return File\n     */\n    public function move(string $destination): File\n    {\n        set_error_handler(function ($type, $msg) use (&$error) {\n            $error = $msg;\n        });\n        $path = pathinfo($destination, PATHINFO_DIRNAME);\n        if (!is_dir($path) && !mkdir($path, 0777, true)) {\n            restore_error_handler();\n            throw new FileException(sprintf('Unable to create the \"%s\" directory (%s)', $path, strip_tags($error)));\n        }\n        if (!rename($this->getPathname(), $destination)) {\n            restore_error_handler();\n            throw new FileException(sprintf('Could not move the file \"%s\" to \"%s\" (%s)', $this->getPathname(), $destination, strip_tags($error)));\n        }\n        restore_error_handler();\n        @chmod($destination, 0666 & ~umask());\n        return new self($destination);\n    }\n\n}"
  },
  {
    "path": "src/Finder/ControllerFinder.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Finder;\n\nuse InvalidArgumentException;\nuse Webman\\Config;\n\nuse function is_array;\nuse function is_dir;\nuse function preg_match;\nuse function preg_quote;\nuse function scandir;\nuse function str_starts_with;\n\n/**\n * ControllerFinder\n *\n * Discover controller files in main app and/or plugins.\n *\n * Scope examples:\n * - null        : main app only\n * - '*'         : main app + all enabled plugins\n * - 'plugin.*'  : all enabled plugins\n * - 'plugin.xxx': single plugin (strict: throws when plugin directory/config missing)\n */\nclass ControllerFinder\n{\n    /**\n     * Find controller files by scope.\n     *\n     * @param string|null $scope\n     * @return FileInfo[]\n     */\n    public static function files(?string $scope = null): array\n    {\n        $roots = static::resolveRoots($scope);\n        if (!$roots) {\n            return [];\n        }\n\n        $resultsByPath = [];\n        foreach ($roots as $root) {\n            $dir = $root['dir'];\n            $suffix = $root['suffix'] ?? '';\n            $controllerFiles = static::findControllerFiles($dir, $suffix);\n            foreach ($controllerFiles as $file) {\n                $resultsByPath[$file->getPathname()] = $file;\n            }\n        }\n\n        return array_values($resultsByPath);\n    }\n\n    /**\n     * Resolve search roots by scope.\n     *\n     * @param string|null $scope\n     * @return array<int, array{dir: string, suffix: string}>\n     */\n    protected static function resolveRoots(?string $scope): array\n    {\n        if ($scope === null) {\n            return static::mainAppRoots();\n        }\n\n        if ($scope === '*') {\n            return array_merge(static::mainAppRoots(), static::allPluginRoots());\n        }\n\n        if ($scope === 'plugin.*') {\n            return static::allPluginRoots();\n        }\n\n        if (str_starts_with($scope, 'plugin.')) {\n            $plugin = substr($scope, strlen('plugin.'));\n            if ($plugin === '' || $plugin === '*') {\n                throw new InvalidArgumentException(\"Invalid controller scope: $scope\");\n            }\n            return static::singlePluginRoots($plugin);\n        }\n\n        throw new InvalidArgumentException(\"Invalid controller scope: $scope\");\n    }\n\n    /**\n     * Main app roots.\n     *\n     * @return array<int, array{dir: string, suffix: string}>\n     */\n    protected static function mainAppRoots(): array\n    {\n        $roots = [];\n        $appRoot = app_path();\n        if (is_dir($appRoot)) {\n            $roots[] = [\n                'dir' => $appRoot,\n                'suffix' => (string)Config::get('app.controller_suffix', ''),\n            ];\n        }\n        return $roots;\n    }\n\n    /**\n     * Roots for all enabled plugins.\n     *\n     * Rule (A): if plugin app config is missing/empty, skip it silently.\n     *\n     * @return array<int, array{dir: string, suffix: string}>\n     */\n    protected static function allPluginRoots(): array\n    {\n        $roots = [];\n        $pluginBase = base_path('plugin');\n        if (!is_dir($pluginBase)) {\n            return [];\n        }\n\n        foreach (scandir($pluginBase) ?: [] as $entry) {\n            if ($entry === '.' || $entry === '..') {\n                continue;\n            }\n            if (!static::isValidIdentifier($entry)) {\n                continue;\n            }\n\n            $pluginDir = $pluginBase . DIRECTORY_SEPARATOR . $entry;\n            if (!is_dir($pluginDir)) {\n                continue;\n            }\n\n            // Only load enabled plugins (same semantics as Route::loadAnnotationRoutes()).\n            $pluginAppConfig = Config::get(\"plugin.$entry.app\");\n            if (!$pluginAppConfig) {\n                continue;\n            }\n\n            $pluginAppDir = $pluginDir . DIRECTORY_SEPARATOR . 'app';\n            if (!is_dir($pluginAppDir)) {\n                continue;\n            }\n\n            $roots[] = [\n                'dir' => $pluginAppDir,\n                'suffix' => is_array($pluginAppConfig)\n                    ? (string)($pluginAppConfig['controller_suffix'] ?? '')\n                    : (string)Config::get(\"plugin.$entry.app.controller_suffix\", ''),\n            ];\n        }\n\n        return $roots;\n    }\n\n    /**\n     * Roots for a single plugin (strict).\n     *\n     * @param string $plugin\n     * @return array<int, array{dir: string, suffix: string}>\n     */\n    protected static function singlePluginRoots(string $plugin): array\n    {\n        if (!static::isValidIdentifier($plugin)) {\n            throw new InvalidArgumentException(\"Invalid plugin identifier: $plugin\");\n        }\n\n        $pluginBase = base_path('plugin');\n        $pluginDir = $pluginBase . DIRECTORY_SEPARATOR . $plugin;\n        if (!is_dir($pluginDir)) {\n            throw new InvalidArgumentException(\"Plugin directory not found: $plugin\");\n        }\n\n        $pluginAppConfig = Config::get(\"plugin.$plugin.app\");\n        if (!$pluginAppConfig) {\n            throw new InvalidArgumentException(\"Plugin app config not found or empty: plugin.$plugin.app\");\n        }\n\n        $pluginAppDir = $pluginDir . DIRECTORY_SEPARATOR . 'app';\n        if (!is_dir($pluginAppDir)) {\n            throw new InvalidArgumentException(\"Plugin app directory not found: plugin/$plugin/app\");\n        }\n\n        return [[\n            'dir' => $pluginAppDir,\n            'suffix' => is_array($pluginAppConfig)\n                ? (string)($pluginAppConfig['controller_suffix'] ?? '')\n                : (string)Config::get(\"plugin.$plugin.app.controller_suffix\", ''),\n        ]];\n    }\n\n    /**\n     * Find controller files.\n     *\n     * @param string $rootDir\n     * @param string $controllerSuffix\n     * @return FileInfo[]\n     */\n    protected static function findControllerFiles(string $rootDir, string $controllerSuffix = ''): array\n    {\n        $controllerPathRegex = $controllerSuffix !== ''\n            ? ('/(^|[\\/\\\\\\\\])controller[\\/\\\\\\\\].*' . preg_quote($controllerSuffix, '/') . '\\.php$/i')\n            : '/(^|[\\/\\\\\\\\])controller[\\/\\\\\\\\].+\\.php$/i';\n\n        $finder = Finder::in($rootDir)\n            ->files()\n            ->path($controllerPathRegex)\n            ->hasAttributes(true)\n            ->typeIn(['class'])\n            ->psr4(true);\n\n        return $finder->find();\n    }\n\n    /**\n     * Is valid identifier (plugin name).\n     *\n     * @param string $name\n     * @return bool\n     */\n    protected static function isValidIdentifier(string $name): bool\n    {\n        return $name !== '' && (bool)preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name);\n    }\n}\n\n"
  },
  {
    "path": "src/Finder/FileInfo.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Finder;\n\nuse Webman\\File;\n\n/**\n * Class FileInfo\n * @package Webman\\Finder\n */\nclass FileInfo extends File\n{\n    /**\n     * @var array PHP file meta info (hasAttributes, type, class, psr4, etc.)\n     */\n    protected array $meta = [];\n\n    /**\n     * @var string Root directory this file belongs to\n     */\n    protected string $rootDir = '';\n\n    // Root namespace is intentionally not stored.\n    /**\n     * Constructor.\n     * @param string $path\n     * @param array $meta\n     * @param string $rootDir\n     */\n    public function __construct(string $path, array $meta = [], string $rootDir = '')\n    {\n        parent::__construct($path);\n        $this->meta = $meta;\n        $this->rootDir = $rootDir;\n    }\n\n    /**\n     * Get PHP file meta info.\n     * @return array\n     */\n    public function meta(): array\n    {\n        return $this->meta;\n    }\n\n    /**\n     * Get declared class (FQCN) from meta.\n     * Example: \"App\\\\Controller\\\\IndexController\"\n     *\n     * @return string|null\n     */\n    public function class(): ?string\n    {\n        $class = $this->meta['class'] ?? null;\n        if (!is_string($class) || $class === '') {\n            return null;\n        }\n        return ltrim($class, '\\\\');\n    }\n\n    /**\n     * Get declared short class name (without namespace).\n     * Example: \"IndexController\"\n     *\n     * @return string|null\n     */\n    public function className(): ?string\n    {\n        $fqcn = $this->class();\n        if ($fqcn === null) {\n            return null;\n        }\n        $pos = strrpos($fqcn, '\\\\');\n        return $pos === false ? $fqcn : substr($fqcn, $pos + 1);\n    }\n\n    /**\n     * Get declared namespace.\n     * Example: \"App\\\\Controller\"\n     *\n     * @return string|null\n     */\n    public function namespace(): ?string\n    {\n        $fqcn = $this->class();\n        if ($fqcn === null) {\n            return null;\n        }\n        $pos = strrpos($fqcn, '\\\\');\n        return $pos === false ? null : substr($fqcn, 0, $pos);\n    }\n\n    /**\n     * Set meta info.\n     * @param array $meta\n     * @return $this\n     */\n    public function setMeta(array $meta): static\n    {\n        $this->meta = $meta;\n        return $this;\n    }\n\n    /**\n     * Get root directory.\n     * @return string\n     */\n    public function rootDir(): string\n    {\n        return $this->rootDir;\n    }\n\n    /**\n     * Get relative pathname from root directory.\n     * @return string\n     */\n    public function relativePathname(): string\n    {\n        if ($this->rootDir === '') {\n            return $this->getPathname();\n        }\n        $rootDir = rtrim(str_replace('\\\\', '/', $this->rootDir), '/');\n        $pathname = str_replace('\\\\', '/', $this->getPathname());\n        $rootLen = strlen($rootDir);\n        if (strncasecmp($pathname, $rootDir, $rootLen) === 0) {\n            return ltrim(substr($pathname, $rootLen), '/');\n        }\n        return $this->getPathname();\n    }\n}\n"
  },
  {
    "path": "src/Finder/Finder.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Finder;\n\nuse FilesystemIterator;\nuse RecursiveDirectoryIterator;\nuse RecursiveIteratorIterator;\nuse RecursiveCallbackFilterIterator;\n\n/**\n * Class Finder\n * File finder with PHP file caching support.\n * @package Webman\\Finder\n */\nclass Finder\n{\n    /**\n     * @var string[] Root directories\n     */\n    protected array $roots = [];\n\n    /**\n     * @var bool Only match files (not directories)\n     */\n    protected bool $onlyFiles = false;\n\n    /**\n     * @var array File name patterns (glob or regex)\n     */\n    protected array $names = [];\n\n    /**\n     * @var array Path patterns (regex)\n     */\n    protected array $paths = [];\n\n    /**\n     * @var array Directories to exclude\n     */\n    protected array $excludeDirs = ['vendor', 'runtime', '.git', 'storage', 'tests', 'node_modules'];\n\n    /**\n     * @var bool Enable PHP meta analysis\n     */\n    protected bool $phpMetaEnabled = false;\n\n    /**\n     * @var bool|null Filter by hasAttributes\n     */\n    protected ?bool $filterHasAttributes = null;\n\n    /**\n     * @var array|null Filter by type\n     */\n    protected ?array $filterTypes = null;\n\n    /**\n     * @var bool|null Filter by psr4\n     */\n    protected ?bool $filterPsr4 = null;\n\n    /**\n     * @var array<string, array> PHP file cache: [path => meta, ...]\n     */\n    protected static array $phpCache = [];\n\n    /**\n     * @var array<string, bool> Cache dirty flags by root\n     */\n    protected static array $cacheDirty = [];\n\n    /**\n     * @var string Cache directory\n     */\n    protected static string $cacheDir = '';\n\n    /**\n     * Create a new Finder instance with root directories.\n     * @param string|array $dirs\n     * @return static\n     */\n    public static function in(string|array $dirs): static\n    {\n        $instance = new static();\n        foreach ((array)$dirs as $dir) {\n            if (is_dir($dir)) {\n                $instance->roots[] = static::normalizePath($dir);\n            }\n        }\n        return $instance;\n    }\n\n    /**\n     * Create a new Finder instance.\n     * Use in() to set search directories.\n     * @return static\n     */\n    public static function create(): static\n    {\n        return new static();\n    }\n\n    /**\n     * Set cache directory.\n     * @param string $dir\n     * @return void\n     */\n    public static function setCacheDir(string $dir): void\n    {\n        static::$cacheDir = rtrim($dir, '/\\\\');\n    }\n\n    /**\n     * Get cache directory.\n     * @return string\n     */\n    protected static function getCacheDir(): string\n    {\n        if (static::$cacheDir === '') {\n            static::$cacheDir = rtrim(runtime_path('cache/framework/finder'), '/\\\\');\n            if (!is_dir(static::$cacheDir)) {\n                @mkdir(static::$cacheDir, 0755, true);\n            }\n        }\n        return static::$cacheDir;\n    }\n\n    /**\n     * Only match files.\n     * @return $this\n     */\n    public function files(): static\n    {\n        $this->onlyFiles = true;\n        return $this;\n    }\n\n    /**\n     * Filter by file name pattern (glob or regex).\n     * @param string|array $patterns\n     * @return $this\n     */\n    public function name(string|array $patterns): static\n    {\n        $this->names = array_merge($this->names, (array)$patterns);\n        return $this;\n    }\n\n    /**\n     * Filter by path pattern (regex).\n     * @param string|array $patterns\n     * @return $this\n     */\n    public function path(string|array $patterns): static\n    {\n        $this->paths = array_merge($this->paths, (array)$patterns);\n        return $this;\n    }\n\n    /**\n     * Exclude directories.\n     * @param string|array $dirs\n     * @return $this\n     */\n    public function exclude(string|array $dirs): static\n    {\n        $this->excludeDirs = array_merge($this->excludeDirs, (array)$dirs);\n        return $this;\n    }\n\n    /**\n     * Set exclude directories (replace default).\n     * @param array $dirs\n     * @return $this\n     */\n    public function excludeDirs(array $dirs): static\n    {\n        $this->excludeDirs = $dirs;\n        return $this;\n    }\n\n    /**\n     * Enable PHP meta analysis and caching.\n     * @return $this\n     */\n    public function withPhpMeta(): static\n    {\n        $this->phpMetaEnabled = true;\n        return $this;\n    }\n\n    /**\n     * Whether any PHP meta filters are requested.\n     * @return bool\n     */\n    protected function phpFiltersRequested(): bool\n    {\n        return $this->filterHasAttributes !== null\n            || $this->filterTypes !== null\n            || $this->filterPsr4 !== null;\n    }\n\n    /**\n     * Filter by hasAttributes.\n     * @param bool $value\n     * @return $this\n     */\n    public function hasAttributes(bool $value): static\n    {\n        // PHP-specific filter: enable PHP meta automatically.\n        $this->phpMetaEnabled = true;\n        $this->filterHasAttributes = $value;\n        return $this;\n    }\n\n    /**\n     * Filter by type (class, interface, trait, enum, non_class).\n     * @param array $types\n     * @return $this\n     */\n    public function typeIn(array $types): static\n    {\n        // PHP-specific filter: enable PHP meta automatically.\n        $this->phpMetaEnabled = true;\n        $this->filterTypes = $types;\n        return $this;\n    }\n\n    /**\n     * Filter by PSR-4 compliance.\n     * @param bool $value\n     * @return $this\n     */\n    public function psr4(bool $value): static\n    {\n        // PHP-specific filter: enable PHP meta automatically.\n        $this->phpMetaEnabled = true;\n        $this->filterPsr4 = $value;\n        return $this;\n    }\n\n    /**\n     * Find files and return FileInfo array.\n     * @return FileInfo[]\n     */\n    public function find(): array\n    {\n        $results = [];\n        $phpFiltersRequested = $this->phpFiltersRequested();\n        // phpMetaEnabled can be turned on explicitly (withPhpMeta) or implicitly (by PHP filters).\n        $phpMetaEnabled = $this->phpMetaEnabled || $phpFiltersRequested;\n\n        foreach ($this->roots as $rootDir) {\n\n            // Load cache for this root\n            if ($phpMetaEnabled) {\n                $this->loadCache($rootDir);\n            }\n\n            // Scan directory\n            $files = $this->scanDirectory($rootDir);\n\n            // Apply filters\n            foreach ($files as $filePath) {\n                // Apply name filter\n                if (!$this->matchesName($filePath)) {\n                    continue;\n                }\n\n                // Apply path filter\n                if (!$this->matchesPath($filePath, $rootDir)) {\n                    continue;\n                }\n\n                // Get or compute PHP meta\n                $meta = [];\n                if ($phpMetaEnabled) {\n                    // If any PHP filters were requested, skip non-PHP files.\n                    if ($phpFiltersRequested && !$this->isPhpFile($filePath)) {\n                        continue;\n                    }\n                    if ($this->isPhpFile($filePath)) {\n                        $meta = $this->getPhpMeta($filePath, $rootDir);\n\n                        // Apply PHP meta filters\n                        if ($phpFiltersRequested && !$this->matchesPhpFilters($meta, $filePath, $rootDir)) {\n                            continue;\n                        }\n                    }\n                }\n\n                $results[] = new FileInfo($filePath, $meta, $rootDir);\n            }\n\n            // Save cache if dirty\n            if ($phpMetaEnabled) {\n                $this->saveCache($rootDir);\n            }\n        }\n\n        return $results;\n    }\n\n    /**\n     * Find files and return paths array.\n     * @return string[]\n     */\n    public function findPaths(): array\n    {\n        return array_map(fn(FileInfo $f) => $f->getPathname(), $this->find());\n    }\n\n    /**\n     * Scan directory recursively.\n     * @param string $dir\n     * @return array\n     */\n    protected function scanDirectory(string $dir): array\n    {\n        $files = [];\n        $excludeSet = array_flip($this->excludeDirs);\n\n        try {\n            $directoryIterator = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);\n            $filterIterator = new RecursiveCallbackFilterIterator(\n                $directoryIterator,\n                function (\\SplFileInfo $current) use ($excludeSet) {\n                    if ($current->isDir()) {\n                        return !isset($excludeSet[$current->getBasename()]);\n                    }\n                    return true;\n                }\n            );\n            $iterator = new RecursiveIteratorIterator($filterIterator, RecursiveIteratorIterator::SELF_FIRST);\n\n            foreach ($iterator as $item) {\n                /** @var \\SplFileInfo $item */\n                $basename = $item->getBasename();\n\n                // Skip excluded directories\n                if ($item->isDir()) {\n                    continue;\n                }\n\n                // Skip if only files mode and not a file\n                if ($this->onlyFiles && !$item->isFile()) {\n                    continue;\n                }\n\n                $files[] = static::normalizePath($item->getPathname());\n            }\n        } catch (\\Throwable $e) {\n            // Ignore unreadable directories\n        }\n\n        return $files;\n    }\n\n    /**\n     * Check if file matches name patterns.\n     * @param string $filePath\n     * @return bool\n     */\n    protected function matchesName(string $filePath): bool\n    {\n        if (empty($this->names)) {\n            return true;\n        }\n\n        $basename = basename($filePath);\n        foreach ($this->names as $pattern) {\n            if ($this->matchPattern($basename, $pattern)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Check if file matches path patterns.\n     * @param string $filePath\n     * @param string $rootDir\n     * @return bool\n     */\n    protected function matchesPath(string $filePath, string $rootDir): bool\n    {\n        if (empty($this->paths)) {\n            return true;\n        }\n\n        // Use relative path for matching\n        $relativePath = $this->getRelativePath($filePath, $rootDir);\n\n        foreach ($this->paths as $pattern) {\n            if ($this->isRegex($pattern) && preg_match($pattern, $relativePath)) {\n                return true;\n            }\n            if (!$this->isRegex($pattern) && stripos($relativePath, $pattern) !== false) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Match a pattern (glob or regex).\n     * @param string $value\n     * @param string $pattern\n     * @return bool\n     */\n    protected function matchPattern(string $value, string $pattern): bool\n    {\n        // Regex pattern\n        if ($this->isRegex($pattern)) {\n            return (bool)preg_match($pattern, $value);\n        }\n\n        // Glob pattern\n        return fnmatch($pattern, $value, FNM_CASEFOLD);\n    }\n\n    /**\n     * Check if pattern is a regex.\n     * @param string $pattern\n     * @return bool\n     */\n    protected function isRegex(string $pattern): bool\n    {\n        if ($pattern === '') {\n            return false;\n        }\n        $delimiter = $pattern[0];\n        if (!in_array($delimiter, ['/', '#', '~', '%'], true)) {\n            return false;\n        }\n        return (bool)preg_match('/^' . preg_quote($delimiter, '/') . '.*' . preg_quote($delimiter, '/') . '[imsxuADU]*$/', $pattern);\n    }\n\n    /**\n     * Check if file is a PHP file.\n     * @param string $filePath\n     * @return bool\n     */\n    protected function isPhpFile(string $filePath): bool\n    {\n        return strtolower(pathinfo($filePath, PATHINFO_EXTENSION)) === 'php';\n    }\n\n    /**\n     * Get PHP file meta, using cache when possible.\n     * @param string $filePath\n     * @param string $rootDir\n     * @return array\n     */\n    protected function getPhpMeta(string $filePath, string $rootDir): array\n    {\n        $cacheKey = $this->getCacheKey($rootDir);\n        $mtime = @filemtime($filePath);\n\n        // Check cache\n        if (isset(static::$phpCache[$cacheKey][$filePath])) {\n            $cached = static::$phpCache[$cacheKey][$filePath];\n            if (isset($cached['mtime']) && $cached['mtime'] === $mtime) {\n                return $cached;\n            }\n        }\n\n        // Compute new meta\n        $meta = $this->computePhpMeta($filePath, $mtime);\n\n        // Update cache\n        static::$phpCache[$cacheKey][$filePath] = $meta;\n        static::$cacheDirty[$cacheKey] = true;\n\n        return $meta;\n    }\n\n    /**\n     * Ensure psr4 is computed and cached in meta.\n     * Only computes once per (file, mtime) cache entry.\n     * @param string $filePath\n     * @param string $rootDir\n     * @param array $meta\n     * @return array Updated meta\n     */\n    protected function ensurePsr4Cached(string $filePath, string $rootDir, array $meta): array\n    {\n        if (array_key_exists('psr4', $meta)) {\n            return $meta;\n        }\n\n        $psr4 = $this->checkPsr4($filePath, $rootDir, $meta['class'] ?? null);\n        $meta['psr4'] = $psr4;\n\n        $cacheKey = $this->getCacheKey($rootDir);\n        static::$phpCache[$cacheKey][$filePath] = $meta;\n        static::$cacheDirty[$cacheKey] = true;\n\n        return $meta;\n    }\n\n    /**\n     * Compute PHP file meta info.\n     * @param string $filePath\n     * @param int|false $mtime\n     * @return array\n     */\n    protected function computePhpMeta(string $filePath, int|false $mtime): array\n    {\n        $meta = [\n            'mtime' => $mtime ?: 0,\n            'hasAttributes' => false,\n            'class' => null,\n        ];\n\n        $code = @file_get_contents($filePath);\n        if ($code === false || $code === '') {\n            $meta['type'] = 'non_class';\n            return $meta;\n        }\n\n        // Fast check for attributes\n        $meta['hasAttributes'] = str_contains($code, '#[');\n\n        // Parse to get type and declared class\n        $parseResult = $this->parsePhpFile($code);\n        $meta['type'] = $parseResult['type'];\n        $meta['class'] = $parseResult['class'];\n\n        return $meta;\n    }\n\n    /**\n     * Parse PHP file to extract type and declared class.\n     * @param string $code\n     * @return array{type: string, class: string|null}\n     */\n    protected function parsePhpFile(string $code): array\n    {\n        $result = [\n            'type' => 'non_class',\n            'class' => null,\n        ];\n\n        try {\n            $tokens = token_get_all($code);\n        } catch (\\Throwable $e) {\n            return $result;\n        }\n\n        $namespace = '';\n        $count = count($tokens);\n        $prevSignificant = null;\n\n        for ($i = 0; $i < $count; $i++) {\n            $token = $tokens[$i];\n            $id = is_array($token) ? $token[0] : null;\n\n            // Extract namespace\n            if ($id === T_NAMESPACE) {\n                $ns = '';\n                for ($j = $i + 1; $j < $count; $j++) {\n                    $t = $tokens[$j];\n                    if (is_array($t)) {\n                        if ($t[0] === T_STRING || $t[0] === T_NS_SEPARATOR) {\n                            $ns .= $t[1];\n                            continue;\n                        }\n                        if (defined('T_NAME_QUALIFIED') && $t[0] === T_NAME_QUALIFIED) {\n                            $ns .= $t[1];\n                            continue;\n                        }\n                        if ($t[0] === T_WHITESPACE) {\n                            continue;\n                        }\n                    } else {\n                        if ($t === ';' || $t === '{') {\n                            break;\n                        }\n                    }\n                }\n                $namespace = trim($ns, '\\\\');\n                continue;\n            }\n\n            // Detect class/interface/trait/enum\n            if ($id === T_CLASS || $id === T_INTERFACE || $id === T_TRAIT || (defined('T_ENUM') && $id === T_ENUM)) {\n                // Skip ::class usage\n                if ($prevSignificant === T_DOUBLE_COLON) {\n                    $prevSignificant = null;\n                    continue;\n                }\n                // Skip anonymous class\n                if ($id === T_CLASS && $prevSignificant === T_NEW) {\n                    continue;\n                }\n\n                // Determine type\n                $type = match ($id) {\n                    T_CLASS => 'class',\n                    T_INTERFACE => 'interface',\n                    T_TRAIT => 'trait',\n                    default => defined('T_ENUM') && $id === T_ENUM ? 'enum' : 'class',\n                };\n\n                // Extract class name\n                for ($j = $i + 1; $j < $count; $j++) {\n                    $t = $tokens[$j];\n                    if (!is_array($t)) {\n                        continue;\n                    }\n                    if ($t[0] === T_WHITESPACE) {\n                        continue;\n                    }\n                    if ($t[0] === T_STRING) {\n                        $className = $t[1];\n                        $result['type'] = $type;\n                        $result['class'] = $namespace !== '' ? ($namespace . '\\\\' . $className) : $className;\n                        return $result; // Return first declaration only\n                    }\n                    break;\n                }\n            }\n\n            // Track previous significant token\n            if (is_array($token)) {\n                if ($id !== T_WHITESPACE && $id !== T_COMMENT && $id !== T_DOC_COMMENT) {\n                    $prevSignificant = $id;\n                }\n            } else {\n                if (trim($token) !== '') {\n                    $prevSignificant = $token;\n                }\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * Check if meta matches PHP filters.\n     * @param array $meta\n     * @param string $filePath\n     * @param string $rootDir\n     * @return bool\n     */\n    protected function matchesPhpFilters(array $meta, string $filePath, string $rootDir): bool\n    {\n        // Filter by hasAttributes\n        if ($this->filterHasAttributes !== null && $meta['hasAttributes'] !== $this->filterHasAttributes) {\n            return false;\n        }\n\n        // Filter by type\n        if ($this->filterTypes !== null && !in_array($meta['type'], $this->filterTypes, true)) {\n            return false;\n        }\n\n        // Filter by PSR-4\n        if ($this->filterPsr4 !== null) {\n            $meta = $this->ensurePsr4Cached($filePath, $rootDir, $meta);\n            $psr4Ok = $meta['psr4'];\n            if ($psr4Ok !== $this->filterPsr4) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Check if file complies with PSR-4.\n     * @param string $filePath\n     * @param string $rootDir\n     * @param string|null $declaredClass\n     * @return bool\n     */\n    protected function checkPsr4(string $filePath, string $rootDir, ?string $declaredClass): bool\n    {\n        if ($declaredClass === null) {\n            return false;\n        }\n\n        // Namespace-agnostic check:\n        // declared class must end with the PSR-4 relative class path derived from file path.\n        $relativePath = $this->getRelativePath($filePath, $rootDir);\n        if ($relativePath === '' || !str_ends_with($relativePath, '.php')) {\n            return false;\n        }\n        $relativeClassPath = substr($relativePath, 0, -4);\n        $relativeClassPath = str_replace('/', '\\\\', $relativeClassPath);\n        if (!$this->isValidPsr4ClassPath($relativeClassPath)) {\n            return false;\n        }\n\n        $declaredClass = ltrim($declaredClass, '\\\\');\n        $suffix = '\\\\' . $relativeClassPath;\n        return $declaredClass === $relativeClassPath || str_ends_with($declaredClass, $suffix);\n    }\n\n    /**\n     * Derive expected class name from file path (PSR-4 style).\n     * @param string $filePath\n     * @param string $rootDir\n     * @param string $rootNamespace\n     * @return string|null\n     */\n    protected function classFromFile(string $filePath, string $rootDir, string $rootNamespace): ?string\n    {\n        $rootDir = rtrim(static::normalizePath($rootDir), '/');\n        $filePath = static::normalizePath($filePath);\n\n        $rootLen = strlen($rootDir);\n        if (strncasecmp($filePath, $rootDir, $rootLen) !== 0) {\n            return null;\n        }\n\n        $relative = ltrim(substr($filePath, $rootLen), '/');\n        if ($relative === '' || !str_ends_with($relative, '.php')) {\n            return null;\n        }\n\n        $relative = substr($relative, 0, -4); // Remove .php\n        $relative = str_replace('/', '\\\\', $relative);\n\n        if (!$this->isValidPsr4ClassPath($relative)) {\n            return null;\n        }\n\n        return rtrim($rootNamespace, '\\\\') . '\\\\' . $relative;\n    }\n\n    /**\n     * Check if relative class path is valid PSR-4.\n     * @param string $relativeClassPath\n     * @return bool\n     */\n    protected function isValidPsr4ClassPath(string $relativeClassPath): bool\n    {\n        return (bool)preg_match('/^[A-Za-z_][A-Za-z0-9_]*(\\\\\\\\[A-Za-z_][A-Za-z0-9_]*)*$/', $relativeClassPath);\n    }\n\n    /**\n     * Get relative path from root directory.\n     * @param string $filePath\n     * @param string $rootDir\n     * @return string\n     */\n    protected function getRelativePath(string $filePath, string $rootDir): string\n    {\n        $rootDir = rtrim(static::normalizePath($rootDir), '/');\n        $filePath = static::normalizePath($filePath);\n        $rootLen = strlen($rootDir);\n\n        if (strncasecmp($filePath, $rootDir, $rootLen) === 0) {\n            return ltrim(substr($filePath, $rootLen), '/');\n        }\n\n        return $filePath;\n    }\n\n    /**\n     * Get cache key for a root directory.\n     * @param string $rootDir\n     * @return string\n     */\n    protected function getCacheKey(string $rootDir): string\n    {\n        return hash('sha256', $rootDir);\n    }\n\n    /**\n     * Get cache file path for a root directory.\n     * @param string $rootDir\n     * @return string\n     */\n    protected function getCacheFile(string $rootDir): string\n    {\n        $cacheKey = $this->getCacheKey($rootDir);\n        return static::getCacheDir() . DIRECTORY_SEPARATOR . \"php_files_{$cacheKey}.php\";\n    }\n\n    /**\n     * Load cache for a root directory.\n     * @param string $rootDir\n     * @return void\n     */\n    protected function loadCache(string $rootDir): void\n    {\n        $cacheKey = $this->getCacheKey($rootDir);\n        if (isset(static::$phpCache[$cacheKey])) {\n            return; // Already loaded\n        }\n\n        $cacheFile = $this->getCacheFile($rootDir);\n        if (is_file($cacheFile)) {\n            try {\n                $data = include $cacheFile;\n                if (is_array($data)) {\n                    static::$phpCache[$cacheKey] = $data;\n                    return;\n                }\n            } catch (\\Throwable $e) {\n                // Ignore corrupted cache\n            }\n        }\n\n        static::$phpCache[$cacheKey] = [];\n    }\n\n    /**\n     * Save cache for a root directory.\n     * @param string $rootDir\n     * @return void\n     */\n    protected function saveCache(string $rootDir): void\n    {\n        $cacheKey = $this->getCacheKey($rootDir);\n        if (empty(static::$cacheDirty[$cacheKey])) {\n            return;\n        }\n\n        $cacheFile = $this->getCacheFile($rootDir);\n        $cacheDir = dirname($cacheFile);\n\n        if (!is_dir($cacheDir)) {\n            @mkdir($cacheDir, 0755, true);\n        }\n\n        $data = static::$phpCache[$cacheKey] ?? [];\n\n        // Clean up deleted files\n        foreach ($data as $path => $meta) {\n            if (!is_file($path)) {\n                unset($data[$path]);\n            }\n        }\n\n        static::$phpCache[$cacheKey] = $data;\n\n        $content = \"<?php\\nreturn \" . var_export($data, true) . \";\\n\";\n        $suffix = (string)getmypid();\n        try {\n            $suffix .= '.' . bin2hex(random_bytes(6));\n        } catch (\\Throwable $e) {\n            $suffix .= '.' . uniqid('', true);\n        }\n        $tempFile = $cacheFile . '.tmp.' . $suffix;\n\n        // No locks: write temp file then atomic rename.\n        if (@file_put_contents($tempFile, $content) !== false) {\n            // On Windows, rename() fails if target exists. Use a backup to swap.\n            if (!@rename($tempFile, $cacheFile)) {\n                $backupFile = $cacheFile . '.bak.' . $suffix;\n                // Best-effort: move old cache away, then move new cache in.\n                @rename($cacheFile, $backupFile);\n                if (@rename($tempFile, $cacheFile)) {\n                    @unlink($backupFile);\n                } else {\n                    // Restore old cache if possible.\n                    @rename($backupFile, $cacheFile);\n                    @unlink($tempFile);\n                }\n            }\n        }\n\n        static::$cacheDirty[$cacheKey] = false;\n    }\n\n    /**\n     * Clear all caches.\n     * @return void\n     */\n    public static function clearCache(): void\n    {\n        static::$phpCache = [];\n        static::$cacheDirty = [];\n    }\n\n    /**\n     * Normalize path separators.\n     * @param string $path\n     * @return string\n     */\n    protected static function normalizePath(string $path): string\n    {\n        $realPath = realpath($path);\n        if ($realPath !== false) {\n            return str_replace('\\\\', '/', $realPath);\n        }\n        return str_replace('\\\\', '/', $path);\n    }\n}\n"
  },
  {
    "path": "src/Http/Request.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Http;\n\nuse Webman\\Route\\Route;\nuse function current;\nuse function filter_var;\nuse function ip2long;\nuse function is_array;\nuse function strpos;\nuse const FILTER_FLAG_IPV4;\nuse const FILTER_FLAG_NO_PRIV_RANGE;\nuse const FILTER_FLAG_NO_RES_RANGE;\nuse const FILTER_VALIDATE_IP;\n\n/**\n * Class Request\n * @package Webman\\Http\n */\nclass Request extends \\Workerman\\Protocols\\Http\\Request\n{\n    /**\n     * @var string\n     */\n    public $plugin = null;\n\n    /**\n     * @var string\n     */\n    public $app = null;\n\n    /**\n     * @var string\n     */\n    public $controller = null;\n\n    /**\n     * @var string\n     */\n    public $action = null;\n\n    /**\n     * @var Route\n     */\n    public $route = null;\n\n    /**\n     * @var bool\n     */\n    protected $isDirty = false;\n\n    /**\n     * @return mixed|null\n     */\n    public function all()\n    {\n        return $this->get() + $this->post();\n    }\n\n    /**\n     * Input\n     * @param string $name\n     * @param mixed $default\n     * @return mixed\n     */\n    public function input(string $name, mixed $default = null)\n    {\n        return $this->get($name, $this->post($name, $default));\n    }\n\n    /**\n     * Only\n     * @param array $keys\n     * @return array\n     */\n    public function only(array $keys): array\n    {\n        $all = $this->all();\n        $result = [];\n        foreach ($keys as $key) {\n            if (isset($all[$key])) {\n                $result[$key] = $all[$key];\n            }\n        }\n        return $result;\n    }\n\n    /**\n     * Except\n     * @param array $keys\n     * @return mixed|null\n     */\n    public function except(array $keys)\n    {\n        $all = $this->all();\n        foreach ($keys as $key) {\n            unset($all[$key]);\n        }\n        return $all;\n    }\n\n    /**\n     * File\n     * @param string|null $name\n     * @return UploadFile|UploadFile[]|null\n     */\n    public function file(?string $name = null): array|null|UploadFile\n    {\n        $files = parent::file($name);\n        if (null === $files) {\n            return $name === null ? [] : null;\n        }\n        if ($name !== null) {\n            // Multi files\n            if (is_array(current($files))) {\n                return $this->parseFiles($files);\n            }\n            return $this->parseFile($files);\n        }\n        $uploadFiles = [];\n        foreach ($files as $name => $file) {\n            // Multi files\n            if (is_array(current($file))) {\n                $uploadFiles[$name] = $this->parseFiles($file);\n            } else {\n                $uploadFiles[$name] = $this->parseFile($file);\n            }\n        }\n        return $uploadFiles;\n    }\n\n    /**\n     * ParseFile\n     * @param array $file\n     * @return UploadFile\n     */\n    protected function parseFile(array $file): UploadFile\n    {\n        return new UploadFile($file['tmp_name'], $file['name'], $file['type'], $file['error']);\n    }\n\n    /**\n     * ParseFiles\n     * @param array $files\n     * @return array\n     */\n    protected function parseFiles(array $files): array\n    {\n        $uploadFiles = [];\n        foreach ($files as $key => $file) {\n            if (is_array(current($file))) {\n                $uploadFiles[$key] = $this->parseFiles($file);\n            } else {\n                $uploadFiles[$key] = $this->parseFile($file);\n            }\n        }\n        return $uploadFiles;\n    }\n\n    /**\n     * GetRemoteIp\n     * @return string\n     */\n    public function getRemoteIp(): string\n    {\n        return $this->connection ? $this->connection->getRemoteIp() : '0.0.0.0';\n    }\n\n    /**\n     * GetRemotePort\n     * @return int\n     */\n    public function getRemotePort(): int\n    {\n        return $this->connection ? $this->connection->getRemotePort() : 0;\n    }\n\n    /**\n     * GetLocalIp\n     * @return string\n     */\n    public function getLocalIp(): string\n    {\n        return $this->connection ? $this->connection->getLocalIp() : '0.0.0.0';\n    }\n\n    /**\n     * GetLocalPort\n     * @return int\n     */\n    public function getLocalPort(): int\n    {\n        return $this->connection ? $this->connection->getLocalPort() : 0;\n    }\n\n    /**\n     * GetRealIp\n     * @param bool $safeMode\n     * @return string\n     */\n    public function getRealIp(bool $safeMode = true): string\n    {\n        $remoteIp = $this->getRemoteIp();\n        if ($safeMode && !static::isIntranetIp($remoteIp)) {\n            return $remoteIp;\n        }\n        $ip = $this->header('x-forwarded-for')\n           ?? $this->header('x-real-ip')\n           ?? $this->header('client-ip')\n           ?? $this->header('x-client-ip')\n           ?? $this->header('via')\n           ?? $remoteIp;\n        if (is_string($ip)) {\n            $ip = current(explode(',', $ip));\n        }\n        return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : $remoteIp;\n    }\n\n    /**\n     * Url\n     * @return string\n     */\n    public function url(): string\n    {\n        return '//' . $this->host() . $this->path();\n    }\n\n    /**\n     * FullUrl\n     * @return string\n     */\n    public function fullUrl(): string\n    {\n        return '//' . $this->host() . $this->uri();\n    }\n\n    /**\n     * IsAjax\n     * @return bool\n     */\n    public function isAjax(): bool\n    {\n        return $this->header('X-Requested-With') === 'XMLHttpRequest';\n    }\n\n    /**\n     * IsGet\n     * @return bool\n     */\n    public function isGet(): bool\n    {\n        return $this->method() === 'GET';\n    }\n\n\n    /**\n     * IsPost\n     * @return bool\n     */\n    public function isPost(): bool\n    {\n        return $this->method() === 'POST';\n    }\n\n\n    /**\n     * IsPjax\n     * @return bool\n     */\n    public function isPjax(): bool\n    {\n        return (bool)$this->header('X-PJAX');\n    }\n\n    /**\n     * ExpectsJson\n     * @return bool\n     */\n    public function expectsJson(): bool\n    {\n        return ($this->isAjax() && !$this->isPjax()) || $this->acceptJson();\n    }\n\n    /**\n     * AcceptJson\n     * @return bool\n     */\n    public function acceptJson(): bool\n    {\n        return false !== strpos($this->header('accept', ''), 'json');\n    }\n\n    /**\n     * IsIntranetIp\n     * @param string $ip\n     * @return bool\n     */\n    public static function isIntranetIp(string $ip): bool\n    {\n        // Not validate ip .\n        if (!filter_var($ip, FILTER_VALIDATE_IP)) {\n            return false;\n        }\n        // Is intranet ip ? For IPv4, the result of false may not be accurate, so we need to check it manually later .\n        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {\n            return true;\n        }\n        // Manual check only for IPv4 .\n        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {\n            return false;\n        }\n        // Manual check .\n        $reservedIps = [\n            1681915904 => 1686110207, // 100.64.0.0 -  100.127.255.255\n            3221225472 => 3221225727, // 192.0.0.0 - 192.0.0.255\n            3221225984 => 3221226239, // 192.0.2.0 - 192.0.2.255\n            3227017984 => 3227018239, // 192.88.99.0 - 192.88.99.255\n            3323068416 => 3323199487, // 198.18.0.0 - 198.19.255.255\n            3325256704 => 3325256959, // 198.51.100.0 - 198.51.100.255\n            3405803776 => 3405804031, // 203.0.113.0 - 203.0.113.255\n            3758096384 => 4026531839, // 224.0.0.0 - 239.255.255.255\n        ];\n        $ipLong = ip2long($ip);\n        foreach ($reservedIps as $ipStart => $ipEnd) {\n            if (($ipLong >= $ipStart) && ($ipLong <= $ipEnd)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Set get.\n     * @param array|string $input\n     * @param mixed $value\n     * @return Request\n     */\n    public function setGet(array|string $input, mixed $value = null): Request\n    {\n        $this->isDirty = true;\n        $input = is_array($input) ? $input : array_merge($this->get(), [$input => $value]);\n        if (isset($this->data)) {\n            $this->data['get'] = $input;\n        } else {\n            $this->_data['get'] = $input;\n        }\n        return $this;\n    }\n\n    /**\n     * Set post.\n     * @param array|string $input\n     * @param mixed $value\n     * @return Request\n     */\n    public function setPost(array|string $input, mixed $value = null): Request\n    {\n        $this->isDirty = true;\n        $input = is_array($input) ? $input : array_merge($this->post(), [$input => $value]);\n        if (isset($this->data)) {\n            $this->data['post'] = $input;\n        } else {\n            $this->_data['post'] = $input;\n        }\n        return $this;\n    }\n\n    /**\n     * Set header.\n     * @param array|string $input\n     * @param mixed $value\n     * @return Request\n     */\n    public function setHeader(array|string $input, mixed $value = null): Request\n    {\n        $this->isDirty = true;\n        $input = is_array($input) ? $input : array_merge($this->header(), [$input => $value]);\n        if (isset($this->data)) {\n            $this->data['headers'] = $input;\n        } else {\n            $this->_data['headers'] = $input;\n        }\n        return $this;\n    }\n\n    /**\n     * Destroy\n     */\n    public function destroy(): void\n    {\n        if ($this->isDirty) {\n            unset($this->data['get'], $this->data['post'], $this->data['headers']);\n        }\n        parent::destroy();\n    }\n\n}\n"
  },
  {
    "path": "src/Http/Response.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Http;\n\nuse Throwable;\nuse Webman\\App;\nuse function filemtime;\nuse function gmdate;\n\n/**\n * Class Response\n * @package Webman\\Http\n */\nclass Response extends \\Workerman\\Protocols\\Http\\Response\n{\n    /**\n     * @var Throwable\n     */\n    protected $exception = null;\n\n    /**\n     * File\n     * @param string $file\n     * @return $this\n     */\n    public function file(string $file): Response\n    {\n        if ($this->notModifiedSince($file)) {\n            return $this->withStatus(304);\n        }\n        return $this->withFile($file);\n    }\n\n    /**\n     * Download\n     * @param string $file\n     * @param string $downloadName\n     * @return $this\n     */\n    public function download(string $file, string $downloadName = ''): Response\n    {\n        $this->withFile($file);\n        if ($downloadName) {\n            // Sanitize to prevent header injection\n            $downloadName = str_replace(['\"', \"\\r\", \"\\n\", \"\\0\"], '', $downloadName);\n            $this->header('Content-Disposition', \"attachment; filename=\\\"$downloadName\\\"\");\n        }\n        return $this;\n    }\n\n    /**\n     * NotModifiedSince\n     * @param string $file\n     * @return bool\n     */\n    protected function notModifiedSince(string $file): bool\n    {\n        $ifModifiedSince = App::request()->header('if-modified-since');\n        if ($ifModifiedSince === null || !is_file($file) || !($mtime = filemtime($file))) {\n            return false;\n        }\n        return $ifModifiedSince === gmdate('D, d M Y H:i:s', $mtime) . ' GMT';\n    }\n\n    /**\n     * Exception\n     * @param Throwable|null $exception\n     * @return Throwable|null\n     */\n    public function exception(?Throwable $exception = null): ?Throwable\n    {\n        if ($exception) {\n            $this->exception = $exception;\n        }\n        return $this->exception;\n    }\n}\n"
  },
  {
    "path": "src/Http/UploadFile.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Http;\n\nuse Webman\\File;\nuse function pathinfo;\n\n/**\n * Class UploadFile\n * @package Webman\\Http\n */\nclass UploadFile extends File\n{\n    /**\n     * @var string\n     */\n    protected $uploadName = null;\n\n    /**\n     * @var string\n     */\n    protected $uploadMimeType = null;\n\n    /**\n     * @var int\n     */\n    protected $uploadErrorCode = null;\n\n    /**\n     * UploadFile constructor.\n     *\n     * @param string $fileName\n     * @param string $uploadName\n     * @param string $uploadMimeType\n     * @param int $uploadErrorCode\n     */\n    public function __construct(string $fileName, string $uploadName, string $uploadMimeType, int $uploadErrorCode)\n    {\n        $this->uploadName = $uploadName;\n        $this->uploadMimeType = $uploadMimeType;\n        $this->uploadErrorCode = $uploadErrorCode;\n        parent::__construct($fileName);\n    }\n\n    /**\n     * GetUploadName\n     * @return string\n     */\n    public function getUploadName(): ?string\n    {\n        return $this->uploadName;\n    }\n\n    /**\n     * GetUploadMimeType\n     * @return string\n     */\n    public function getUploadMimeType(): ?string\n    {\n        return $this->uploadMimeType;\n    }\n\n    /**\n     * GetUploadExtension\n     * @return string\n     */\n    public function getUploadExtension(): string\n    {\n        return pathinfo($this->uploadName, PATHINFO_EXTENSION);\n    }\n\n    /**\n     * GetUploadErrorCode\n     * @return int\n     */\n    public function getUploadErrorCode(): ?int\n    {\n        return $this->uploadErrorCode;\n    }\n\n    /**\n     * IsValid\n     * @return bool\n     */\n    public function isValid(): bool\n    {\n        return $this->uploadErrorCode === UPLOAD_ERR_OK;\n    }\n\n    /**\n     * GetUploadMineType\n     * @return string\n     * @deprecated\n     */\n    public function getUploadMineType(): ?string\n    {\n        return $this->uploadMimeType;\n    }\n}\n"
  },
  {
    "path": "src/Install.php",
    "content": "<?php\n\nnamespace Webman;\n\nclass Install\n{\n    const WEBMAN_PLUGIN = true;\n\n    /**\n     * @var array\n     */\n    protected static $pathRelation = [\n        'start.php' => 'start.php',\n        'windows.php' => 'windows.php',\n    ];\n\n    /**\n     * Install\n     * @return void\n     */\n    public static function install()\n    {\n        static::installByRelation();\n    }\n\n    /**\n     * Uninstall\n     * @return void\n     */\n    public static function uninstall()\n    {\n\n    }\n\n    /**\n     * InstallByRelation\n     * @return void\n     */\n    public static function installByRelation()\n    {\n        foreach (static::$pathRelation as $source => $dest) {\n            $parentDir = base_path(dirname($dest));\n            if (!is_dir($parentDir)) {\n                mkdir($parentDir, 0777, true);\n            }\n            $sourceFile = __DIR__ . \"/$source\";\n            copy_dir($sourceFile, base_path($dest), true);\n            echo \"Create $dest\\r\\n\";\n            if (is_file($sourceFile)) {\n                @unlink($sourceFile);\n            }\n        }\n        if (is_file($file = base_path('support/helpers.php'))) {\n            file_put_contents($file, \"<?php\\n// This file is generated by Webman, please don't modify it.\\n\");\n            echo \"Clear helpers.php\\r\\n\";\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/Middleware.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman;\n\n\nuse Closure;\nuse ReflectionAttribute;\nuse support\\annotation\\Middleware as MiddlewareAttribute;\nuse Webman\\Route\\Route;\nuse ReflectionClass;\nuse ReflectionMethod;\nuse RuntimeException;\nuse function array_merge;\nuse function array_reverse;\nuse function is_array;\nuse function method_exists;\n\nclass Middleware\n{\n\n    /**\n     * @var array\n     */\n    protected static $instances = [];\n\n    /**\n     * @var array Cache for controller middleware resolved via reflection/attributes.\n     */\n    protected static $controllerMiddlewareCache = [];\n\n    /**\n     * @param mixed $allMiddlewares\n     * @param string $plugin\n     * @return void\n     */\n    public static function load($allMiddlewares, string $plugin = '')\n    {\n        if (!is_array($allMiddlewares)) {\n            return;\n        }\n        foreach ($allMiddlewares as $appName => $middlewares) {\n            if (!is_array($middlewares)) {\n                throw new RuntimeException('Bad middleware config');\n            }\n            $pluginKey = $plugin;\n            $appKey = $appName;\n            if ($appKey === '@') {\n                $pluginKey = '';\n            } elseif (strpos($appKey, 'plugin.') !== false) {\n                $explode = explode('.', $appKey, 4);\n                $pluginKey = $explode[1];\n                $appKey = $explode[2] ?? '';\n            }\n            foreach ($middlewares as $className) {\n                if (method_exists($className, 'process')) {\n                    static::$instances[$pluginKey][$appKey][] = [$className, 'process'];\n                } else {\n                    // @todo Log\n                    echo \"middleware $className::process not exsits\\n\";\n                }\n            }\n        }\n    }\n\n    /**\n     * @param string $plugin\n     * @param string $appName\n     * @param string|array|Closure $controller\n     * @param Route|null $route\n     * @param bool $withGlobalMiddleware\n     * @return array\n     */\n    public static function getMiddleware(string $plugin, string $appName, string|array|Closure $controller, Route|null $route, bool $withGlobalMiddleware = true): array\n    {\n        $isController = is_array($controller) && is_string($controller[0]);\n        $globalMiddleware = $withGlobalMiddleware ? static::$instances['']['@'] ?? [] : [];\n        $appGlobalMiddleware = $withGlobalMiddleware && isset(static::$instances[$plugin]['']) ? static::$instances[$plugin][''] : [];\n        $middlewares = $routeMiddlewares = [];\n        // Route middleware\n        if ($route) {\n            foreach (array_reverse($route->getMiddleware()) as $className) {\n                $routeMiddlewares[] = [$className, 'process'];\n            }\n        }\n        if ($isController && $controller[0] && class_exists($controller[0])) {\n            $cacheKey = $controller[0] . '::' . $controller[1];\n            if (isset(static::$controllerMiddlewareCache[$cacheKey])) {\n                $cached = static::$controllerMiddlewareCache[$cacheKey];\n                $middlewares = array_merge($cached['before_route'], $routeMiddlewares, $cached['after_route']);\n            } else {\n                $beforeRoute = [];\n                $afterRoute = [];\n                // Controller middleware annotation\n                $reflectionClass = new ReflectionClass($controller[0]);\n                self::prepareAttributeMiddlewares($beforeRoute, $reflectionClass);\n                // Controller middleware property\n                if ($reflectionClass->hasProperty('middleware')) {\n                    $defaultProperties = $reflectionClass->getDefaultProperties();\n                    $middlewaresClasses = $defaultProperties['middleware'];\n                    foreach ((array)$middlewaresClasses as $className) {\n                        $beforeRoute[] = [$className, 'process'];\n                    }\n                }\n                // Method middleware annotation (route must be between controller and method)\n                if ($reflectionClass->hasMethod($controller[1])) {\n                    self::prepareAttributeMiddlewares($afterRoute, $reflectionClass->getMethod($controller[1]));\n                }\n                $middlewares = array_merge($beforeRoute, $routeMiddlewares, $afterRoute);\n                static::$controllerMiddlewareCache[$cacheKey] = ['before_route' => $beforeRoute, 'after_route' => $afterRoute];\n                if (count(static::$controllerMiddlewareCache) > 1024) {\n                    unset(static::$controllerMiddlewareCache[key(static::$controllerMiddlewareCache)]);\n                }\n            }\n        } else {\n            // Route middleware\n            $middlewares = array_merge($middlewares, $routeMiddlewares);\n        }\n        if ($appName === '') {\n            return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $middlewares));\n        }\n        $appMiddleware = static::$instances[$plugin][$appName] ?? [];\n        return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $appMiddleware, $middlewares));\n    }\n\n    /**\n     * @param array $middlewares\n     * @param ReflectionClass|ReflectionMethod $reflection\n     * @return void\n     */\n    private static function prepareAttributeMiddlewares(array &$middlewares, ReflectionClass|ReflectionMethod $reflection): void\n    {\n        if ($reflection instanceof ReflectionClass && $parent_ref = $reflection->getParentClass()) {\n            self::prepareAttributeMiddlewares($middlewares, $parent_ref);\n        }\n        $middlewareAttributes = $reflection->getAttributes(MiddlewareAttribute::class, ReflectionAttribute::IS_INSTANCEOF);\n        foreach ($middlewareAttributes as $middlewareAttribute) {\n            $middlewareAttributeInstance = $middlewareAttribute->newInstance();\n            $middlewares = array_merge($middlewares, $middlewareAttributeInstance->getMiddlewares());\n        }\n    }\n\n    /**\n     * @return void\n     * @deprecated\n     */\n    public static function container($_)\n    {\n\n    }\n}\n"
  },
  {
    "path": "src/MiddlewareInterface.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman;\n\nuse Webman\\Http\\Request;\nuse Webman\\Http\\Response;\n\ninterface MiddlewareInterface\n{\n    /**\n     * Process an incoming server request.\n     *\n     * Processes an incoming server request in order to produce a response.\n     * If unable to produce the response itself, it may delegate to the provided\n     * request handler to do so.\n     */\n    public function process(Request $request, callable $handler): Response;\n}\n"
  },
  {
    "path": "src/Route/Route.php",
    "content": "<?php\r\n/**\r\n * This file is part of webman.\r\n *\r\n * Licensed under The MIT License\r\n * For full copyright and license information, please see the MIT-LICENSE.txt\r\n * Redistributions of files must retain the above copyright notice.\r\n *\r\n * @author    walkor<walkor@workerman.net>\r\n * @copyright walkor<walkor@workerman.net>\r\n * @link      http://www.workerman.net/\r\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\r\n */\r\n\r\nnamespace Webman\\Route;\r\n\r\nuse Webman\\Route as Router;\r\nuse function array_merge;\r\nuse function count;\r\nuse function preg_replace_callback;\r\nuse function str_replace;\r\n\r\n/**\r\n * Class Route\r\n * @package Webman\r\n */\r\nclass Route\r\n{\r\n    /**\r\n     * @var string|null\r\n     */\r\n    protected $name = null;\r\n\r\n    /**\r\n     * @var array\r\n     */\r\n    protected $methods = [];\r\n\r\n    /**\r\n     * @var string\r\n     */\r\n    protected $path = '';\r\n\r\n    /**\r\n     * @var callable\r\n     */\r\n    protected $callback = null;\r\n\r\n    /**\r\n     * @var array\r\n     */\r\n    protected $middlewares = [];\r\n\r\n    /**\r\n     * @var array\r\n     */\r\n    protected $params = [];\r\n\r\n    /**\r\n     * Route constructor.\r\n     * @param array $methods\r\n     * @param string $path\r\n     * @param callable $callback\r\n     */\r\n    public function __construct($methods, string $path, $callback)\r\n    {\r\n        $this->methods = (array)$methods;\r\n        $this->path = $path;\r\n        $this->callback = $callback;\r\n    }\r\n\r\n    /**\r\n     * Get name.\r\n     * @return string|null\r\n     */\r\n    public function getName(): ?string\r\n    {\r\n        return $this->name ?? null;\r\n    }\r\n\r\n    /**\r\n     * Name.\r\n     * @param string $name\r\n     * @return $this\r\n     */\r\n    public function name(string $name): Route\r\n    {\r\n        $this->name = $name;\r\n        Router::setByName($name, $this);\r\n        return $this;\r\n    }\r\n\r\n    /**\r\n     * Middleware.\r\n     * @param mixed $middleware\r\n     * @return $this|array\r\n     */\r\n    public function middleware(mixed $middleware = null)\r\n    {\r\n        if ($middleware === null) {\r\n            return $this->middlewares;\r\n        }\r\n        $this->middlewares = array_merge($this->middlewares, is_array($middleware) ? array_reverse($middleware) : [$middleware]);\r\n        return $this;\r\n    }\r\n\r\n    /**\r\n     * GetPath.\r\n     * @return string\r\n     */\r\n    public function getPath(): string\r\n    {\r\n        return $this->path;\r\n    }\r\n\r\n    /**\r\n     * GetMethods.\r\n     * @return array\r\n     */\r\n    public function getMethods(): array\r\n    {\r\n        return $this->methods;\r\n    }\r\n\r\n    /**\r\n     * GetCallback.\r\n     * @return callable|null\r\n     */\r\n    public function getCallback()\r\n    {\r\n        return $this->callback;\r\n    }\r\n\r\n    /**\r\n     * GetMiddleware.\r\n     * @return array\r\n     */\r\n    public function getMiddleware(): array\r\n    {\r\n        return $this->middlewares;\r\n    }\r\n\r\n    /**\r\n     * Param.\r\n     * @param string|null $name\r\n     * @param mixed $default\r\n     * @return mixed\r\n     */\r\n    public function param(?string $name = null, mixed $default = null)\r\n    {\r\n        if ($name === null) {\r\n            return $this->params;\r\n        }\r\n        return $this->params[$name] ?? $default;\r\n    }\r\n\r\n    /**\r\n     * SetParams.\r\n     * @param array $params\r\n     * @return $this\r\n     */\r\n    public function setParams(array $params): Route\r\n    {\r\n        $this->params = array_merge($this->params, $params);\r\n        return $this;\r\n    }\r\n\r\n    /**\r\n     * Url.\r\n     * @param array $parameters\r\n     * @return string\r\n     */\r\n    public function url(array $parameters = []): string\r\n    {\r\n        if (empty($parameters)) {\r\n            return $this->path;\r\n        }\r\n        $path = str_replace(['[', ']'], '', $this->path);\r\n        $path = preg_replace_callback('/\\{(.*?)(?:\\:[^\\}]*?)*?\\}/', function ($matches) use (&$parameters) {\r\n            if (!$parameters) {\r\n                return $matches[0];\r\n            }\r\n            if (isset($parameters[$matches[1]])) {\r\n                $value = $parameters[$matches[1]];\r\n                unset($parameters[$matches[1]]);\r\n                return $value;\r\n            }\r\n            $key = key($parameters);\r\n            if (is_int($key)) {\r\n                $value = $parameters[$key];\r\n                unset($parameters[$key]);\r\n                return $value;\r\n            }\r\n            return $matches[0];\r\n        }, $path);\r\n        return count($parameters) > 0 ? $path . '?' . http_build_query($parameters) : $path;\r\n    }\r\n\r\n}\r\n"
  },
  {
    "path": "src/Route.php",
    "content": "<?php\r\n/**\r\n * This file is part of webman.\r\n *\r\n * Licensed under The MIT License\r\n * For full copyright and license information, please see the MIT-LICENSE.txt\r\n * Redistributions of files must retain the above copyright notice.\r\n *\r\n * @author    walkor<walkor@workerman.net>\r\n * @copyright walkor<walkor@workerman.net>\r\n * @link      http://www.workerman.net/\r\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\r\n */\r\n\r\nnamespace Webman;\r\n\r\nuse FastRoute\\Dispatcher\\GroupCountBased;\r\nuse FastRoute\\RouteCollector;\r\nuse FilesystemIterator;\r\nuse Psr\\Container\\ContainerExceptionInterface;\r\nuse Psr\\Container\\NotFoundExceptionInterface;\r\nuse RecursiveDirectoryIterator;\r\nuse RecursiveIteratorIterator;\r\nuse ReflectionAttribute;\r\nuse ReflectionClass;\r\nuse ReflectionException;\r\nuse ReflectionMethod;\r\nuse RuntimeException;\r\nuse support\\annotation\\Middleware as MiddlewareAttribute;\r\nuse support\\annotation\\route\\DisableDefaultRoute;\r\nuse support\\annotation\\route\\Route as RouteAttribute;\r\nuse support\\annotation\\route\\RouteGroup as RouteGroupAttribute;\r\nuse Webman\\Finder\\FileInfo;\r\nuse Webman\\Finder\\ControllerFinder;\r\nuse Webman\\Route\\Route as RouteObject;\r\nuse function array_diff;\r\nuse function array_values;\r\nuse function class_exists;\r\nuse function explode;\r\nuse function FastRoute\\simpleDispatcher;\r\nuse function in_array;\r\nuse function is_array;\r\nuse function is_callable;\r\nuse function is_file;\r\nuse function is_scalar;\r\nuse function is_string;\r\nuse function json_encode;\r\nuse function method_exists;\r\nuse function strpos;\r\n\r\n/**\r\n * Class Route\r\n * @package Webman\r\n */\r\nclass Route\r\n{\r\n    /**\r\n     * @var Route\r\n     */\r\n    protected static $instance = null;\r\n\r\n    /**\r\n     * @var GroupCountBased\r\n     */\r\n    protected static $dispatcher = null;\r\n\r\n    /**\r\n     * @var RouteCollector\r\n     */\r\n    protected static $collector = null;\r\n\r\n    /**\r\n     * @var RouteObject[]\r\n     */\r\n    protected static $fallbackRoutes = [];\r\n\r\n    /**\r\n     * @var array\r\n     */\r\n    protected static $fallback = [];\r\n\r\n    /**\r\n     * @var array\r\n     */\r\n    protected static $nameList = [];\r\n\r\n    /**\r\n     * @var string\r\n     */\r\n    protected static $groupPrefix = '';\r\n\r\n    /**\r\n     * @var bool\r\n     */\r\n    protected static $disabledDefaultRoutes = [];\r\n\r\n    /**\r\n     * @var array\r\n     */\r\n    protected static $disabledDefaultRouteControllers = [];\r\n\r\n    /**\r\n     * @var array\r\n     */\r\n    protected static $disabledDefaultRouteActions = [];\r\n\r\n    /**\r\n     * @var RouteObject[]\r\n     */\r\n    protected static $allRoutes = [];\r\n\r\n    /**\r\n     * Index for conflict detection: [\"METHOD path\" => \"callback string\"]\r\n     * @var array<string, string>\r\n     */\r\n    protected static array $methodPathIndex = [];\r\n\r\n    /**\r\n     * @var string|null\r\n     */\r\n    protected static ?string $registeringSource = null;\r\n\r\n    /**\r\n     * @var RouteObject[]\r\n     */\r\n    protected $routes = [];\r\n\r\n    /**\r\n     * @var Route[]\r\n     */\r\n    protected $children = [];\r\n\r\n    /**\r\n     * Add GET route.\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    public static function get(string $path, $callback): RouteObject\r\n    {\r\n        return static::addRoute('GET', $path, $callback);\r\n    }\r\n\r\n    /**\r\n     * Add POST route.\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    public static function post(string $path, $callback): RouteObject\r\n    {\r\n        return static::addRoute('POST', $path, $callback);\r\n    }\r\n\r\n    /**\r\n     * Add PUT route.\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    public static function put(string $path, $callback): RouteObject\r\n    {\r\n        return static::addRoute('PUT', $path, $callback);\r\n    }\r\n\r\n    /**\r\n     * Add PATCH route.\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    public static function patch(string $path, $callback): RouteObject\r\n    {\r\n        return static::addRoute('PATCH', $path, $callback);\r\n    }\r\n\r\n    /**\r\n     * Add DELETE route.\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    public static function delete(string $path, $callback): RouteObject\r\n    {\r\n        return static::addRoute('DELETE', $path, $callback);\r\n    }\r\n\r\n    /**\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    public static function head(string $path, $callback): RouteObject\r\n    {\r\n        return static::addRoute('HEAD', $path, $callback);\r\n    }\r\n\r\n    /**\r\n     * Add HEAD route.\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    public static function options(string $path, $callback): RouteObject\r\n    {\r\n        return static::addRoute('OPTIONS', $path, $callback);\r\n    }\r\n\r\n    /**\r\n     * Add OPTIONS route.\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    public static function any(string $path, $callback): RouteObject\r\n    {\r\n        return static::addRoute(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], $path, $callback);\r\n    }\r\n\r\n    /**\r\n     * Add route.\r\n     * @param $method\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    public static function add($method, string $path, $callback): RouteObject\r\n    {\r\n        return static::addRoute($method, $path, $callback);\r\n    }\r\n\r\n    /**\r\n     * Add group.\r\n     * @param string|callable $path\r\n     * @param callable|null $callback\r\n     * @return static\r\n     */\r\n    public static function group($path, ?callable $callback = null): Route\r\n    {\r\n        if ($callback === null) {\r\n            $callback = $path;\r\n            $path = '';\r\n        }\r\n        $previousGroupPrefix = static::$groupPrefix;\r\n        static::$groupPrefix = $previousGroupPrefix . $path;\r\n        $previousInstance = static::$instance;\r\n        $instance = static::$instance = new static;\r\n        static::$collector->addGroup($path, $callback);\r\n        static::$groupPrefix = $previousGroupPrefix;\r\n        static::$instance = $previousInstance;\r\n        if ($previousInstance) {\r\n            $previousInstance->addChild($instance);\r\n        }\r\n        return $instance;\r\n    }\r\n\r\n    /**\r\n     * Add resource.\r\n     * @param string $name\r\n     * @param string $controller\r\n     * @param array $options\r\n     * @return void\r\n     */\r\n    public static function resource(string $name, string $controller, array $options = [])\r\n    {\r\n        $name = trim($name, '/');\r\n        if (is_array($options) && !empty($options)) {\r\n            $diffOptions = array_diff($options, ['index', 'create', 'store', 'update', 'show', 'edit', 'destroy', 'recovery']);\r\n            if (!empty($diffOptions)) {\r\n                foreach ($diffOptions as $action) {\r\n                    static::any(\"/$name/{$action}[/{id}]\", [$controller, $action])->name(\"$name.{$action}\");\r\n                }\r\n            }\r\n            // 注册路由 由于顺序不同会导致路由无效 因此不适用循环注册\r\n            if (in_array('index', $options)) static::get(\"/$name\", [$controller, 'index'])->name(\"$name.index\");\r\n            if (in_array('create', $options)) static::get(\"/$name/create\", [$controller, 'create'])->name(\"$name.create\");\r\n            if (in_array('store', $options)) static::post(\"/$name\", [$controller, 'store'])->name(\"$name.store\");\r\n            if (in_array('update', $options)) static::put(\"/$name/{id}\", [$controller, 'update'])->name(\"$name.update\");\r\n            if (in_array('patch', $options)) static::patch(\"/$name/{id}\", [$controller, 'patch'])->name(\"$name.patch\");\r\n            if (in_array('show', $options)) static::get(\"/$name/{id}\", [$controller, 'show'])->name(\"$name.show\");\r\n            if (in_array('edit', $options)) static::get(\"/$name/{id}/edit\", [$controller, 'edit'])->name(\"$name.edit\");\r\n            if (in_array('destroy', $options)) static::delete(\"/$name/{id}\", [$controller, 'destroy'])->name(\"$name.destroy\");\r\n            if (in_array('recovery', $options)) static::put(\"/$name/{id}/recovery\", [$controller, 'recovery'])->name(\"$name.recovery\");\r\n        } else {\r\n            //为空时自动注册所有常用路由\r\n            if (method_exists($controller, 'index')) static::get(\"/$name\", [$controller, 'index'])->name(\"$name.index\");\r\n            if (method_exists($controller, 'create')) static::get(\"/$name/create\", [$controller, 'create'])->name(\"$name.create\");\r\n            if (method_exists($controller, 'store')) static::post(\"/$name\", [$controller, 'store'])->name(\"$name.store\");\r\n            if (method_exists($controller, 'update')) static::put(\"/$name/{id}\", [$controller, 'update'])->name(\"$name.update\");\r\n            if (method_exists($controller, 'patch')) static::patch(\"/$name/{id}\", [$controller, 'patch'])->name(\"$name.patch\");\r\n            if (method_exists($controller, 'show')) static::get(\"/$name/{id}\", [$controller, 'show'])->name(\"$name.show\");\r\n            if (method_exists($controller, 'edit')) static::get(\"/$name/{id}/edit\", [$controller, 'edit'])->name(\"$name.edit\");\r\n            if (method_exists($controller, 'destroy')) static::delete(\"/$name/{id}\", [$controller, 'destroy'])->name(\"$name.destroy\");\r\n            if (method_exists($controller, 'recovery')) static::put(\"/$name/{id}/recovery\", [$controller, 'recovery'])->name(\"$name.recovery\");\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Get routes.\r\n     * @return RouteObject[]\r\n     */\r\n    public static function getRoutes(): array\r\n    {\r\n        return static::$allRoutes;\r\n    }\r\n\r\n    /**\r\n     * Disable default route.\r\n     * @param array|string $plugin\r\n     * @param string|null $app\r\n     * @return bool\r\n     */\r\n    public static function disableDefaultRoute(array|string $plugin = '', ?string $app = null): bool\r\n    {\r\n        // Is [controller action]\r\n        if (is_array($plugin)) {\r\n            $controllerAction = $plugin;\r\n            if (!isset($controllerAction[0]) || !is_string($controllerAction[0]) ||\r\n                !isset($controllerAction[1]) || !is_string($controllerAction[1])) {\r\n                return false;\r\n            }\r\n            $controller = $controllerAction[0];\r\n            $action = $controllerAction[1];\r\n            static::$disabledDefaultRouteActions[$controller][$action] = $action;\r\n            return true;\r\n        }\r\n        // Is plugin\r\n        if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) {\r\n            if (!isset(static::$disabledDefaultRoutes[$plugin])) {\r\n                static::$disabledDefaultRoutes[$plugin] = [];\r\n            }\r\n            $app = $app ?? '*';\r\n            static::$disabledDefaultRoutes[$plugin][$app] = $app;\r\n            return true;\r\n        }\r\n        // Is controller\r\n        if (is_string($plugin) && class_exists($plugin)) {\r\n            static::$disabledDefaultRouteControllers[$plugin] = $plugin;\r\n            return true;\r\n        }\r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Is default route disabled.\r\n     * @param array|string $plugin\r\n     * @param string|null $app\r\n     * @return bool\r\n     */\r\n    public static function isDefaultRouteDisabled(array|string $plugin = '', ?string $app = null): bool\r\n    {\r\n        // Is [controller action]\r\n        if (is_array($plugin)) {\r\n            if (!isset($plugin[0]) || !is_string($plugin[0]) ||\r\n                !isset($plugin[1]) || !is_string($plugin[1])) {\r\n                return false;\r\n            }\r\n            return isset(static::$disabledDefaultRouteActions[$plugin[0]][$plugin[1]]) || static::isDefaultRouteDisabledByAnnotation($plugin[0], $plugin[1]);\r\n        }\r\n        // Is plugin\r\n        if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) {\r\n            $app = $app ?? '*';\r\n            return isset(static::$disabledDefaultRoutes[$plugin]['*']) || isset(static::$disabledDefaultRoutes[$plugin][$app]);\r\n        }\r\n        // Is controller\r\n        if (is_string($plugin) && class_exists($plugin)) {\r\n            return isset(static::$disabledDefaultRouteControllers[$plugin]);\r\n        }\r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Is default route disabled by annotation.\r\n     * @param string $controller\r\n     * @param string|null $action\r\n     * @return bool\r\n     */\r\n    protected static function isDefaultRouteDisabledByAnnotation(string $controller, ?string $action = null): bool\r\n    {\r\n        if (class_exists($controller)) {\r\n            $reflectionClass = new ReflectionClass($controller);\r\n            if (static::isRefHasDefaultRouteDisabledAnnotation($reflectionClass)) {\r\n                return true;\r\n            }\r\n            if ($action && $reflectionClass->hasMethod($action)) {\r\n                $reflectionMethod = $reflectionClass->getMethod($action);\r\n                if ($reflectionMethod->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF)) {\r\n                    return true;\r\n                }\r\n            }\r\n        }\r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Is reflection class has default route disabled annotation.\r\n     * @param ReflectionClass $reflectionClass\r\n     * @return bool\r\n     */\r\n    protected static function isRefHasDefaultRouteDisabledAnnotation(ReflectionClass $reflectionClass): bool\r\n    {\r\n        $has = $reflectionClass->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF);\r\n        if ($has) {\r\n            return true;\r\n        }\r\n        if (method_exists($reflectionClass, 'getParentClass')) {\r\n            $parent = $reflectionClass->getParentClass();\r\n            if ($parent) {\r\n                return static::isRefHasDefaultRouteDisabledAnnotation($parent);\r\n            }\r\n        }\r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Add middleware.\r\n     * @param $middleware\r\n     * @return $this\r\n     */\r\n    public function middleware($middleware): Route\r\n    {\r\n        foreach ($this->routes as $route) {\r\n            $route->middleware($middleware);\r\n        }\r\n        foreach ($this->getChildren() as $child) {\r\n            $child->middleware($middleware);\r\n        }\r\n        return $this;\r\n    }\r\n\r\n    /**\r\n     * Collect route.\r\n     * @param RouteObject $route\r\n     */\r\n    public function collect(RouteObject $route)\r\n    {\r\n        $this->routes[] = $route;\r\n    }\r\n\r\n    /**\r\n     * Set by name.\r\n     * @param string $name\r\n     * @param RouteObject $instance\r\n     */\r\n    public static function setByName(string $name, RouteObject $instance)\r\n    {\r\n        static::$nameList[$name] = $instance;\r\n    }\r\n\r\n    /**\r\n     * Get by name.\r\n     * @param string $name\r\n     * @return null|RouteObject\r\n     */\r\n    public static function getByName(string $name): ?RouteObject\r\n    {\r\n        return static::$nameList[$name] ?? null;\r\n    }\r\n\r\n    /**\r\n     * Add child.\r\n     * @param Route $route\r\n     * @return void\r\n     */\r\n    public function addChild(Route $route)\r\n    {\r\n        $this->children[] = $route;\r\n    }\r\n\r\n    /**\r\n     * Get children.\r\n     * @return Route[]\r\n     */\r\n    public function getChildren()\r\n    {\r\n        return $this->children;\r\n    }\r\n\r\n    /**\r\n     * Dispatch.\r\n     * @param string $method\r\n     * @param string $path\r\n     * @return array\r\n     */\r\n    public static function dispatch(string $method, string $path): array\r\n    {\r\n        return static::$dispatcher->dispatch($method, $path);\r\n    }\r\n\r\n    /**\r\n     * Convert to callable.\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return callable|false|string[]\r\n     */\r\n    public static function convertToCallable(string $path, $callback)\r\n    {\r\n        if (is_string($callback) && strpos($callback, '@')) {\r\n            $callback = explode('@', $callback, 2);\r\n        }\r\n\r\n        if (!is_array($callback)) {\r\n            if (!is_callable($callback)) {\r\n                $callStr = is_scalar($callback) ? $callback : 'Closure';\r\n                echo \"Route $path $callStr is not callable\\n\";\r\n                return false;\r\n            }\r\n        } else {\r\n            $callback = array_values($callback);\r\n            if (!isset($callback[1]) || !class_exists($callback[0]) || !method_exists($callback[0], $callback[1])) {\r\n                echo \"Route $path \" . json_encode($callback) . \" is not callable\\n\";\r\n                return false;\r\n            }\r\n        }\r\n\r\n        return $callback;\r\n    }\r\n\r\n    /**\r\n     * Add route.\r\n     * @param array|string $methods\r\n     * @param string $path\r\n     * @param callable|mixed $callback\r\n     * @return RouteObject\r\n     */\r\n    protected static function addRoute($methods, string $path, $callback): RouteObject\r\n    {\r\n        $fullPath = static::$groupPrefix . $path;\r\n        foreach ((array)$methods as $method) {\r\n            $method = strtoupper((string)$method);\r\n            $key = $method . ' ' . $fullPath;\r\n            if (isset(static::$methodPathIndex[$key])) {\r\n                $old = static::$methodPathIndex[$key];\r\n                $new = static::callbackToString($callback);\r\n                $source = static::$registeringSource ? (' from ' . static::$registeringSource) : '';\r\n                throw new RuntimeException(\"Route conflict: [$key] already registered as $old, cannot register $new$source\");\r\n            }\r\n            static::$methodPathIndex[$key] = static::callbackToString($callback);\r\n        }\r\n\r\n        $route = new RouteObject($methods, static::$groupPrefix . $path, $callback);\r\n        static::$allRoutes[] = $route;\r\n\r\n        if ($callback = static::convertToCallable($path, $callback)) {\r\n            static::$collector->addRoute($methods, $path, ['callback' => $callback, 'route' => $route]);\r\n        }\r\n        if (static::$instance) {\r\n            static::$instance->collect($route);\r\n        }\r\n        return $route;\r\n    }\r\n\r\n    /**\r\n     * Load.\r\n     * @param mixed $paths\r\n     * @return void\r\n     */\r\n    public static function load($paths)\r\n    {\r\n        if (!is_array($paths)) {\r\n            return;\r\n        }\r\n        static::$dispatcher = null;\r\n        static::$collector = null;\r\n        static::$fallbackRoutes = [];\r\n        static::$fallback = [];\r\n        static::$nameList = [];\r\n        static::$disabledDefaultRoutes = [];\r\n        static::$disabledDefaultRouteControllers = [];\r\n        static::$disabledDefaultRouteActions = [];\r\n        static::$allRoutes = [];\r\n        static::$methodPathIndex = [];\r\n        static::$registeringSource = null;\r\n\r\n        static::$dispatcher = simpleDispatcher(function (RouteCollector $route) use ($paths) {\r\n            Route::setCollector($route);\r\n            foreach ($paths as $configPath) {\r\n                $routeConfigFile = $configPath . '/route.php';\r\n                if (is_file($routeConfigFile)) {\r\n                    require_once $routeConfigFile;\r\n                }\r\n                if (!is_dir($pluginConfigPath = $configPath . '/plugin')) {\r\n                    continue;\r\n                }\r\n                $dirIterator = new RecursiveDirectoryIterator($pluginConfigPath, FilesystemIterator::FOLLOW_SYMLINKS);\r\n                $iterator = new RecursiveIteratorIterator($dirIterator);\r\n                foreach ($iterator as $file) {\r\n                    if ($file->getBaseName('.php') !== 'route') {\r\n                        continue;\r\n                    }\r\n                    $appConfigFile = pathinfo($file, PATHINFO_DIRNAME) . '/app.php';\r\n                    if (!is_file($appConfigFile)) {\r\n                        continue;\r\n                    }\r\n                    $appConfig = include $appConfigFile;\r\n                    if (empty($appConfig['enable'])) {\r\n                        continue;\r\n                    }\r\n                    require_once $file;\r\n                }\r\n            }\r\n            static::loadAnnotationRoutes();\r\n        });\r\n    }\r\n\r\n    /**\r\n     * SetCollector.\r\n     * @param RouteCollector $route\r\n     * @return void\r\n     */\r\n    public static function setCollector(RouteCollector $route)\r\n    {\r\n        static::$collector = $route;\r\n    }\r\n\r\n    /**\r\n     * Fallback.\r\n     * @param callable|mixed $callback\r\n     * @param string $plugin\r\n     * @return RouteObject\r\n     */\r\n    public static function fallback(callable $callback, string $plugin = '')\r\n    {\r\n        $route = new RouteObject([], '', $callback);\r\n        static::$fallbackRoutes[$plugin] = $route;\r\n        return $route;\r\n    }\r\n\r\n    /**\r\n     * GetFallBack.\r\n     * @param string $plugin\r\n     * @param int $status\r\n     * @return callable|null\r\n     * @throws ContainerExceptionInterface\r\n     * @throws NotFoundExceptionInterface\r\n     * @throws ReflectionException\r\n     */\r\n    public static function getFallback(string $plugin = '', int $status = 404)\r\n    {\r\n        if (!isset(static::$fallback[$plugin])) {\r\n            $callback = null;\r\n            $route = static::$fallbackRoutes[$plugin] ?? null;\r\n            static::$fallback[$plugin] = $route ? App::getCallback($plugin, 'NOT_FOUND', $route->getCallback(), ['status' => $status], false, $route) : null;\r\n        }\r\n        return static::$fallback[$plugin];\r\n    }\r\n\r\n    /**\r\n     * Load annotation routes.\r\n     * @return void\r\n     */\r\n    protected static function loadAnnotationRoutes(): void\r\n    {\r\n        $controllerFiles = ControllerFinder::files('*');\r\n        if (!$controllerFiles) {\r\n            return;\r\n        }\r\n        $routes = static::buildAnnotationRouteDefinitions($controllerFiles);\r\n        static::registerAnnotationRouteDefinitions($routes);\r\n\r\n    }\r\n\r\n    /**\r\n     * Build annotation route definitions.\r\n     * @param FileInfo[] $controllerFiles\r\n     * @return array<int,array{methods: string[], path: string, callback: array{0:string,1:string}, name: ?string, middlewares: array}>\r\n     */\r\n    protected static function buildAnnotationRouteDefinitions(array $controllerFiles): array\r\n    {\r\n        $definitions = [];\r\n\r\n        foreach ($controllerFiles as $foundFile) {\r\n            $meta = $foundFile->meta();\r\n            $controllerClass = $meta['class'] ?? null;\r\n            if (!$controllerClass) {\r\n                continue;\r\n            }\r\n\r\n            $file = $foundFile->getPathname();\r\n            if (!class_exists($controllerClass)) {\r\n                require_once $file;\r\n            }\r\n            if (!class_exists($controllerClass)) {\r\n                continue;\r\n            }\r\n\r\n            $ref = new ReflectionClass($controllerClass);\r\n            if ($ref->isAbstract() || $ref->isInterface()) {\r\n                continue;\r\n            }\r\n\r\n            $prefix = '';\r\n            $groupAttrs = $ref->getAttributes(RouteGroupAttribute::class, ReflectionAttribute::IS_INSTANCEOF);\r\n            if ($groupAttrs) {\r\n                /** @var RouteGroupAttribute $group */\r\n                $group = $groupAttrs[0]->newInstance();\r\n                $prefix = static::normalizeRoutePrefix($group->prefix);\r\n            }\r\n\r\n            foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {\r\n                if ($method->isConstructor() || $method->isDestructor()) {\r\n                    continue;\r\n                }\r\n                if ($method->getDeclaringClass()->getName() !== $controllerClass) {\r\n                    continue;\r\n                }\r\n\r\n                $routeAttrs = $method->getAttributes(RouteAttribute::class, ReflectionAttribute::IS_INSTANCEOF);\r\n                if (!$routeAttrs) {\r\n                    continue;\r\n                }\r\n\r\n                foreach ($routeAttrs as $routeAttr) {\r\n                    /** @var RouteAttribute $route */\r\n                    $route = $routeAttr->newInstance();\r\n                    if ($route->path === null) {\r\n                        // Null path means \"method restriction only\" for default route, do not register.\r\n                        continue;\r\n                    }\r\n                    $source = $controllerClass . '::' . $method->getName();\r\n                    $path = static::normalizeRoutePath($route->path, $source);\r\n                    $fullPath = $prefix . $path;\r\n                    if ($fullPath === '') {\r\n                        throw new RuntimeException(\"Annotation route resolves to empty path: #[Get('')] requires a #[RouteGroup] prefix ($source)\");\r\n                    }\r\n\r\n                    $methods = [];\r\n                    foreach ($route->methods as $m) {\r\n                        $methods[] = strtoupper((string)$m);\r\n                    }\r\n\r\n                    $definitions[] = [\r\n                        'methods' => $methods,\r\n                        'path' => $fullPath,\r\n                        'callback' => [$controllerClass, $method->getName()],\r\n                        'name' => $route->name,\r\n                    ];\r\n                }\r\n            }\r\n        }\r\n\r\n        return $definitions;\r\n    }\r\n\r\n    /**\r\n     * Collect middlewares from attributes.\r\n     * @param array<ReflectionAttribute> $attributes\r\n     * @return array\r\n     */\r\n    protected static function collectMiddlewaresFromAttributes(array $attributes): array\r\n    {\r\n        $middlewares = [];\r\n        foreach ($attributes as $attribute) {\r\n            /** @var MiddlewareAttribute $instance */\r\n            $instance = $attribute->newInstance();\r\n            foreach ($instance->getMiddlewares() as $middleware) {\r\n                if (is_string($middleware)) {\r\n                    $middlewares[] = $middleware;\r\n                    continue;\r\n                }\r\n                if (is_array($middleware) && isset($middleware[0]) && is_string($middleware[0])) {\r\n                    $middlewares[] = $middleware[0];\r\n                }\r\n            }\r\n        }\r\n        return $middlewares;\r\n    }\r\n\r\n    /**\r\n     * Register annotation route definitions.\r\n     * @param array $definitions\r\n     * @return void\r\n     */\r\n    protected static function registerAnnotationRouteDefinitions(array $definitions): void\r\n    {\r\n        foreach ($definitions as $definition) {\r\n            static::$registeringSource = 'annotation ' . $definition['callback'][0] . '::' . $definition['callback'][1];\r\n            $route = static::add($definition['methods'], $definition['path'], $definition['callback']);\r\n            if (!empty($definition['name'])) {\r\n                $route->name($definition['name']);\r\n            }\r\n            if (!empty($definition['middlewares'])) {\r\n                $route->middleware($definition['middlewares']);\r\n            }\r\n            static::$registeringSource = null;\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Normalize route prefix.\r\n     * @param string $prefix\r\n     * @return string\r\n     */\r\n    protected static function normalizeRoutePrefix(string $prefix): string\r\n    {\r\n        $prefix = trim($prefix);\r\n        if ($prefix === '') {\r\n            return '';\r\n        }\r\n        if ($prefix[0] !== '/') {\r\n            $prefix = '/' . $prefix;\r\n        }\r\n        return rtrim($prefix, '/');\r\n    }\r\n\r\n    /**\r\n     * Normalize route path.\r\n     * Empty string is allowed (means \"use group prefix only\").\r\n     * Non-empty path must start with '/'.\r\n     * @param string $path\r\n     * @param string $source\r\n     * @return string\r\n     */\r\n    protected static function normalizeRoutePath(string $path, string $source): string\r\n    {\r\n        $path = trim($path);\r\n        if ($path === '') {\r\n            return '';\r\n        }\r\n        if ($path[0] !== '/') {\r\n            throw new RuntimeException(\"Annotation route path must start with '/': '$path' ($source)\");\r\n        }\r\n        return $path;\r\n    }\r\n\r\n    /**\r\n     * Callback to string.\r\n     * @param mixed $callback\r\n     * @return string\r\n     */\r\n    protected static function callbackToString(mixed $callback): string\r\n    {\r\n        if (is_array($callback)) {\r\n            $callback = array_values($callback);\r\n            $class = $callback[0] ?? '';\r\n            $method = $callback[1] ?? '';\r\n            return $class && $method ? ($class . '::' . $method) : json_encode($callback);\r\n        }\r\n        if ($callback instanceof \\Closure) {\r\n            return 'Closure';\r\n        }\r\n        if (is_string($callback)) {\r\n            return $callback;\r\n        }\r\n        return get_debug_type($callback);\r\n    }\r\n\r\n    /**\r\n     * @return void\r\n     * @deprecated\r\n     */\r\n    public static function container()\r\n    {\r\n\r\n    }\r\n\r\n}\r\n"
  },
  {
    "path": "src/Session/FileSessionHandler.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Session;\n\nuse Workerman\\Protocols\\Http\\Session\\FileSessionHandler as FileHandler;\n\n/**\n * Class FileSessionHandler\n * @package Webman\n */\nclass FileSessionHandler extends FileHandler\n{\n\n}\n"
  },
  {
    "path": "src/Session/RedisClusterSessionHandler.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Session;\n\nuse Workerman\\Protocols\\Http\\Session\\RedisClusterSessionHandler as RedisClusterHandler;\n\nclass RedisClusterSessionHandler extends RedisClusterHandler\n{\n\n}\n"
  },
  {
    "path": "src/Session/RedisSessionHandler.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman\\Session;\n\nuse Workerman\\Protocols\\Http\\Session\\RedisSessionHandler as RedisHandler;\n\n/**\n * Class FileSessionHandler\n * @package Webman\n */\nclass RedisSessionHandler extends RedisHandler\n{\n\n}\n"
  },
  {
    "path": "src/Util.php",
    "content": "<?php\r\n/**\r\n * This file is part of webman.\r\n *\r\n * Licensed under The MIT License\r\n * For full copyright and license information, please see the MIT-LICENSE.txt\r\n * Redistributions of files must retain the above copyright notice.\r\n *\r\n * @author    walkor<walkor@workerman.net>\r\n * @copyright walkor<walkor@workerman.net>\r\n * @link      http://www.workerman.net/\r\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\r\n */\r\n\r\nnamespace Webman;\r\n\r\nuse function array_diff;\r\nuse function array_map;\r\nuse function scandir;\r\n\r\n/**\r\n * Class Util\r\n * @package Webman\r\n */\r\nclass Util\r\n{\r\n    /**\r\n     * ScanDir.\r\n     * @param string $basePath\r\n     * @param bool $withBasePath\r\n     * @return array\r\n     */\r\n    public static function scanDir(string $basePath, bool $withBasePath = true): array\r\n    {\r\n        if (!is_dir($basePath)) {\r\n            return [];\r\n        }\r\n        $paths = array_diff(scandir($basePath), array('.', '..')) ?: [];\r\n        return $withBasePath ? array_map(static function ($path) use ($basePath) {\r\n            return $basePath . DIRECTORY_SEPARATOR . $path;\r\n        }, $paths) : $paths;\r\n    }\r\n\r\n}\r\n"
  },
  {
    "path": "src/View.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace Webman;\n\ninterface View\n{\n    /**\n     * Render.\n     * @param string $template\n     * @param array $vars\n     * @param string|null $app\n     * @return string\n     */\n    public static function render(string $template, array $vars, ?string $app = null): string;\n}\n"
  },
  {
    "path": "src/start.php",
    "content": "#!/usr/bin/env php\n<?php\nchdir(__DIR__);\nrequire_once __DIR__ . '/vendor/autoload.php';\nsupport\\App::run();\n"
  },
  {
    "path": "src/support/App.php",
    "content": "<?php\n\nnamespace support;\n\nuse Dotenv\\Dotenv;\nuse RuntimeException;\nuse Throwable;\nuse Webman\\Config;\nuse Webman\\Util;\nuse Workerman\\Connection\\TcpConnection;\nuse Workerman\\Worker;\nuse function base_path;\nuse function call_user_func;\nuse function is_dir;\nuse function opcache_get_status;\nuse function opcache_invalidate;\nuse const DIRECTORY_SEPARATOR;\n\nclass App\n{\n    /**\n     * Run.\n     * @return void\n     * @throws Throwable\n     */\n    public static function run()\n    {\n        ini_set('display_errors', 'on');\n        error_reporting(E_ALL);\n\n        if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {\n            if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {\n                Dotenv::createUnsafeImmutable(run_path())->load();\n            } else {\n                Dotenv::createMutable(run_path())->load();\n            }\n        }\n\n        if (!$appConfigFile = config_path('app.php')) {\n            throw new RuntimeException('Config file not found: app.php');\n        }\n        $appConfig = require $appConfigFile;\n        if ($timezone = $appConfig['default_timezone'] ?? '') {\n            date_default_timezone_set($timezone);\n        }\n\n        static::loadAllConfig(['route', 'container']);\n\n        if (!is_phar() && DIRECTORY_SEPARATOR === '\\\\' && empty(config('server.listen'))) {\n            echo \"Please run 'php windows.php' on windows system.\" . PHP_EOL;\n            exit;\n        }\n\n        $errorReporting = config('app.error_reporting');\n        if (isset($errorReporting)) {\n            error_reporting($errorReporting);\n        }\n\n        $runtimeLogsPath = runtime_path() . DIRECTORY_SEPARATOR . 'logs';\n        if (!file_exists($runtimeLogsPath) || !is_dir($runtimeLogsPath)) {\n            if (!mkdir($runtimeLogsPath, 0777, true)) {\n                throw new RuntimeException(\"Failed to create runtime logs directory. Please check the permission.\");\n            }\n        }\n\n        $runtimeViewsPath = runtime_path() . DIRECTORY_SEPARATOR . 'views';\n        if (!file_exists($runtimeViewsPath) || !is_dir($runtimeViewsPath)) {\n            if (!mkdir($runtimeViewsPath, 0777, true)) {\n                throw new RuntimeException(\"Failed to create runtime views directory. Please check the permission.\");\n            }\n        }\n\n        Worker::$onMasterReload = function () {\n            if (function_exists('opcache_get_status')) {\n                if ($status = opcache_get_status()) {\n                    if (isset($status['scripts']) && $scripts = $status['scripts']) {\n                        foreach (array_keys($scripts) as $file) {\n                            opcache_invalidate($file, true);\n                        }\n                    }\n                }\n            }\n        };\n\n        $config = config('server');\n        Worker::$pidFile = $config['pid_file'];\n        Worker::$stdoutFile = $config['stdout_file'];\n        Worker::$logFile = $config['log_file'];\n        Worker::$eventLoopClass = $config['event_loop'] ?? '';\n        TcpConnection::$defaultMaxPackageSize = $config['max_package_size'] ?? 10 * 1024 * 1024;\n        if (property_exists(Worker::class, 'statusFile')) {\n            Worker::$statusFile = $config['status_file'] ?? '';\n        }\n        if (property_exists(Worker::class, 'stopTimeout')) {\n            Worker::$stopTimeout = $config['stop_timeout'] ?? 2;\n        }\n\n        if ($config['listen'] ?? false) {\n            $worker = new Worker($config['listen'], $config['context'] ?? []);\n            $propertyMap = [\n                'name',\n                'count',\n                'user',\n                'group',\n                'reusePort',\n                'transport',\n                'protocol'\n            ];\n            foreach ($propertyMap as $property) {\n                if (isset($config[$property])) {\n                    $worker->$property = $config[$property];\n                }\n            }\n\n            $worker->onWorkerStart = function ($worker) {\n                require_once base_path() . '/support/bootstrap.php';\n                $app = new \\Webman\\App(config('app.request_class', Request::class), Log::channel('default'), app_path(), public_path());\n                $worker->onMessage = [$app, 'onMessage'];\n                call_user_func([$app, 'onWorkerStart'], $worker);\n            };\n        }\n\n        $windowsWithoutServerListen = is_phar() && DIRECTORY_SEPARATOR === '\\\\' && empty($config['listen']);\n        $process = config('process', []);\n        if ($windowsWithoutServerListen && $process) {\n            $processName = isset($process['webman']) ? 'webman' : key($process);\n            worker_start($processName, $process[$processName]);\n        } else if (DIRECTORY_SEPARATOR === '/') {\n            foreach (config('process', []) as $processName => $config) {\n                worker_start($processName, $config);\n            }\n            foreach (config('plugin', []) as $firm => $projects) {\n                foreach ($projects as $name => $project) {\n                    if (!is_array($project)) {\n                        continue;\n                    }\n                    foreach ($project['process'] ?? [] as $processName => $config) {\n                        worker_start(\"plugin.$firm.$name.$processName\", $config);\n                    }\n                }\n                foreach ($projects['process'] ?? [] as $processName => $config) {\n                    worker_start(\"plugin.$firm.$processName\", $config);\n                }\n            }\n        }\n\n        Worker::runAll();\n    }\n\n    /**\n     * LoadAllConfig.\n     * @param array $excludes\n     * @return void\n     */\n    public static function loadAllConfig(array $excludes = [])\n    {\n        Config::load(config_path(), $excludes);\n        $directory = base_path() . '/plugin';\n        foreach (Util::scanDir($directory, false) as $name) {\n            $dir = \"$directory/$name/config\";\n            if (is_dir($dir)) {\n                Config::load($dir, $excludes, \"plugin.$name\");\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/support/Container.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support;\n\nuse Webman\\Config;\n\n/**\n * Class Container\n * @package support\n * @method static mixed get($name)\n * @method static mixed make($name, array $parameters)\n * @method static bool has($name)\n */\nclass Container\n{\n    /**\n     * Instance\n     * @param string $plugin\n     * @return array|mixed|void|null\n     */\n    public static function instance(string $plugin = '')\n    {\n        return Config::get($plugin ? \"plugin.$plugin.container\" : 'container');\n    }\n\n    /**\n     * @param string $name\n     * @param array $arguments\n     * @return mixed\n     */\n    public static function __callStatic(string $name, array $arguments)\n    {\n        return static::instance()->{$name}(... $arguments);\n    }\n}\n"
  },
  {
    "path": "src/support/Context.php",
    "content": "<?php\n\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support;\n\n/**\n * Class Context\n * @package Webman\n */\nclass Context extends \\Webman\\Context\n{\n\n}\n"
  },
  {
    "path": "src/support/Log.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support;\n\nuse Monolog\\Formatter\\FormatterInterface;\nuse Monolog\\Handler\\FormattableHandlerInterface;\nuse Monolog\\Handler\\HandlerInterface;\nuse Monolog\\Logger;\nuse function array_values;\nuse function config;\nuse function is_array;\n\n/**\n * Class Log\n * @package support\n *\n * @method static void log($level, $message, array $context = [])\n * @method static void debug($message, array $context = [])\n * @method static void info($message, array $context = [])\n * @method static void notice($message, array $context = [])\n * @method static void warning($message, array $context = [])\n * @method static void error($message, array $context = [])\n * @method static void critical($message, array $context = [])\n * @method static void alert($message, array $context = [])\n * @method static void emergency($message, array $context = [])\n */\nclass Log\n{\n    /**\n     * @var array\n     */\n    protected static $instance = [];\n\n    /**\n     * Channel.\n     * @param string $name\n     * @return Logger\n     */\n    public static function channel(string $name = 'default'): Logger\n    {\n        if (!isset(static::$instance[$name])) {\n            $config = config('log', [])[$name];\n            $handlers = self::handlers($config);\n            $processors = self::processors($config);\n            $logger = new Logger($name, $handlers, $processors);\n            if (method_exists($logger, 'useLoggingLoopDetection')) {\n                $logger->useLoggingLoopDetection(false);\n            }\n            static::$instance[$name] = $logger;\n        }\n        return static::$instance[$name];\n    }\n\n    /**\n     * Handlers.\n     * @param array $config\n     * @return array\n     */\n    protected static function handlers(array $config): array\n    {\n        $handlerConfigs = $config['handlers'] ?? [[]];\n        $handlers = [];\n        foreach ($handlerConfigs as $value) {\n            $class = $value['class'] ?? [];\n            $constructor = $value['constructor'] ?? [];\n\n            $formatterConfig = $value['formatter'] ?? [];\n\n            $class && $handlers[] = self::handler($class, $constructor, $formatterConfig);\n        }\n\n        return $handlers;\n    }\n\n    /**\n     * Handler.\n     * @param string $class\n     * @param array $constructor\n     * @param array $formatterConfig\n     * @return HandlerInterface\n     */\n    protected static function handler(string $class, array $constructor, array $formatterConfig): HandlerInterface\n    {\n        /** @var HandlerInterface $handler */\n        $handler = new $class(... array_values($constructor));\n\n        if ($handler instanceof FormattableHandlerInterface && $formatterConfig) {\n            $formatterClass = $formatterConfig['class'];\n            $formatterConstructor = $formatterConfig['constructor'];\n\n            /** @var FormatterInterface $formatter */\n            $formatter = new $formatterClass(... array_values($formatterConstructor));\n\n            $handler->setFormatter($formatter);\n        }\n\n        return $handler;\n    }\n\n    /**\n     * Processors.\n     * @param array $config\n     * @return array\n     */\n    protected static function processors(array $config): array\n    {\n        $result = [];\n        if (!isset($config['processors']) && isset($config['processor'])) {\n            $config['processors'] = [$config['processor']];\n        }\n\n        foreach ($config['processors'] ?? [] as $value) {\n            if (is_array($value) && isset($value['class'])) {\n                $value = new $value['class'](... array_values($value['constructor'] ?? []));\n            }\n            $result[] = $value;\n        }\n\n        return $result;\n    }\n\n    /**\n     * @param string $name\n     * @param array $arguments\n     * @return mixed\n     */\n    public static function __callStatic(string $name, array $arguments)\n    {\n        return static::channel()->{$name}(... $arguments);\n    }\n}\n"
  },
  {
    "path": "src/support/Plugin.php",
    "content": "<?php\n\nnamespace support;\n\nuse function defined;\nuse function is_callable;\nuse function is_file;\nuse function method_exists;\n\nclass Plugin\n{\n    /**\n     * Install.\n     * @param mixed $event\n     * @return void\n     */\n    public static function install($event)\n    {\n        static::findHelper();\n        $psr4 = static::getPsr4($event);\n        foreach ($psr4 as $namespace => $path) {\n            $pluginConst = \"\\\\{$namespace}Install::WEBMAN_PLUGIN\";\n            if (!defined($pluginConst)) {\n                continue;\n            }\n            $installFunction = \"\\\\{$namespace}Install::install\";\n            if (is_callable($installFunction)) {\n                $installFunction(true);\n            }\n        }\n    }\n\n    /**\n     * Update.\n     * @param mixed $event\n     * @return void\n     */\n    public static function update($event)\n    {\n        static::findHelper();\n        $psr4 = static::getPsr4($event);\n        foreach ($psr4 as $namespace => $path) {\n            $pluginConst = \"\\\\{$namespace}Install::WEBMAN_PLUGIN\";\n            if (!defined($pluginConst)) {\n                continue;\n            }\n            $updateFunction = \"\\\\{$namespace}Install::update\";\n            if (is_callable($updateFunction)) {\n                $updateFunction();\n                continue;\n            }\n            $installFunction = \"\\\\{$namespace}Install::install\";\n            if (is_callable($installFunction)) {\n                $installFunction(false);\n            }\n        }\n    }\n\n    /**\n     * Uninstall.\n     * @param mixed $event\n     * @return void\n     */\n    public static function uninstall($event)\n    {\n        static::findHelper();\n        $psr4 = static::getPsr4($event);\n        foreach ($psr4 as $namespace => $path) {\n            $pluginConst = \"\\\\{$namespace}Install::WEBMAN_PLUGIN\";\n            if (!defined($pluginConst)) {\n                continue;\n            }\n            $uninstallFunction = \"\\\\{$namespace}Install::uninstall\";\n            if (is_callable($uninstallFunction)) {\n                $uninstallFunction();\n            }\n        }\n    }\n\n    /**\n     * Get psr-4 info\n     *\n     * @param mixed $event\n     * @return array\n     */\n    protected static function getPsr4($event)\n    {\n        $operation = $event->getOperation();\n        $autoload = method_exists($operation, 'getPackage') ? $operation->getPackage()->getAutoload() : $operation->getTargetPackage()->getAutoload();\n        return $autoload['psr-4'] ?? [];\n    }\n\n    /**\n     * FindHelper.\n     * @return void\n     */\n    protected static function findHelper()\n    {\n        // Plugin.php in webman\n        require_once __DIR__ . '/helpers.php';\n    }\n}\n"
  },
  {
    "path": "src/support/Request.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support;\n\n/**\n * Class Request\n * @package support\n */\nclass Request extends \\Webman\\Http\\Request\n{\n\n}"
  },
  {
    "path": "src/support/Response.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support;\n\n/**\n * Class Response\n * @package support\n */\nclass Response extends \\Webman\\Http\\Response\n{\n\n}"
  },
  {
    "path": "src/support/Translation.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support;\n\nuse FilesystemIterator;\nuse RecursiveDirectoryIterator;\nuse RecursiveIteratorIterator;\nuse RegexIterator;\nuse Symfony\\Component\\Translation\\Translator;\nuse Webman\\Exception\\NotFoundException;\nuse function basename;\nuse function config;\nuse function get_realpath;\nuse function pathinfo;\nuse function request;\nuse function substr;\n\n/**\n * Class Translation\n * @package support\n * @method static string trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)\n * @method static void setLocale(string $locale)\n * @method static string getLocale()\n */\nclass Translation\n{\n\n    /**\n     * @var Translator[]\n     */\n    protected static $instance = [];\n\n    /**\n     * Instance.\n     * @param string $plugin\n     * @param array|null $config\n     * @return Translator\n     * @throws NotFoundException\n     */\n    public static function instance(string $plugin = '', ?array $config = null): Translator\n    {\n        if (!isset(static::$instance[$plugin])) {\n            $config = $config ?? config($plugin ? \"plugin.$plugin.translation\" : 'translation', []);\n            $paths = (array)($config['path'] ?? []);\n\n            static::$instance[$plugin] = $translator = new Translator($config['locale']);\n            $translator->setFallbackLocales($config['fallback_locale']);\n\n            $classes = $config['loader'] ?? [\n                'Symfony\\Component\\Translation\\Loader\\PhpFileLoader' => [\n                    'extension' => '.php',\n                    'format' => 'phpfile'\n                ],\n                'Symfony\\Component\\Translation\\Loader\\PoFileLoader' => [\n                    'extension' => '.po',\n                    'format' => 'pofile'\n                ]\n            ];\n            foreach ($paths as $path) {\n                // Phar support. Compatible with the 'realpath' function in the phar file.\n                if (!$translationsPath = get_realpath($path)) {\n                    continue;\n                }\n\n                foreach ($classes as $class => $opts) {\n                    $translator->addLoader($opts['format'], new $class);\n                    $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($translationsPath, FilesystemIterator::SKIP_DOTS));\n                    $files = new RegexIterator($iterator, '/^.+' . preg_quote($opts['extension']) . '$/i', RegexIterator::GET_MATCH);\n                    foreach ($files as $file) {\n                        $file = $file[0];\n                        $domain = basename($file, $opts['extension']);\n                        $dirName = pathinfo($file, PATHINFO_DIRNAME);\n                        $locale = substr(strrchr($dirName, DIRECTORY_SEPARATOR), 1);\n                        if ($domain && $locale) {\n                            $translator->addResource($opts['format'], $file, $locale, $domain);\n                        }\n                    }\n                }\n            }\n        }\n        return static::$instance[$plugin];\n    }\n\n    /**\n     * @param string $name\n     * @param array $arguments\n     * @return mixed\n     * @throws NotFoundException\n     */\n    public static function __callStatic(string $name, array $arguments)\n    {\n        $request = request();\n        $plugin = $request->plugin ?? '';\n        return static::instance($plugin)->{$name}(... $arguments);\n    }\n}\n"
  },
  {
    "path": "src/support/View.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support;\n\nuse function config;\nuse function request;\n\nclass View\n{\n    /**\n     * Assign.\n     * @param mixed $name\n     * @param mixed $value\n     * @return void\n     */\n    public static function assign($name, mixed $value = null)\n    {\n        $request = request();\n        $plugin = $request->plugin ?? '';\n        $handler = config($plugin ? \"plugin.$plugin.view.handler\" : 'view.handler');\n        $handler::assign($name, $value);\n    }\n}"
  },
  {
    "path": "src/support/annotation/DisableDefaultRoute.php",
    "content": "<?php\n\nnamespace support\\annotation;\n\nuse Attribute;\n\n/**\n * @deprecated Use support\\annotation\\route\\DisableDefaultRoute instead.\n */\n#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]\nclass DisableDefaultRoute extends \\support\\annotation\\route\\DisableDefaultRoute\n{\n}\n"
  },
  {
    "path": "src/support/annotation/Middleware.php",
    "content": "<?php\n\nnamespace support\\annotation;\n\nuse Attribute;\n\n/**\n * Attach middlewares to routes/controllers/functions via attributes.\n *\n * Example:\n *   #[Middleware(AuthMiddleware::class, RateLimitMiddleware::class)]\n */\n#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]\nclass Middleware\n{\n    /**\n     * @var array\n     */\n    protected array $middlewares = [];\n\n    /**\n     * @param mixed ...$middlewares Middleware class names.\n     */\n    public function __construct(...$middlewares)\n    {\n        $this->middlewares = $middlewares;\n    }\n\n    /**\n     * Convert to webman middleware callable format: [MiddlewareClass, 'process'].\n     * @return array\n     */\n    public function getMiddlewares(): array\n    {\n        $middlewares = [];\n        foreach ($this->middlewares as $middleware) {\n            $middlewares[] = [$middleware, 'process'];\n        }\n        return $middlewares;\n    }\n}"
  },
  {
    "path": "src/support/annotation/route/Any.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: ['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS'], ...)].\n */\n#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]\nclass Any extends Route\n{\n    /**\n     * @param string|null $path Route path. Null means default-route method restriction only.\n     * @param string|null $name Route name\n     */\n    public function __construct(?string $path = null, ?string $name = null)\n    {\n        parent::__construct($path, ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], $name);\n    }\n}\n\n"
  },
  {
    "path": "src/support/annotation/route/Delete.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'DELETE', ...)].\n */\n#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]\nclass Delete extends Route\n{\n    /**\n     * @param string|null $path Route path. Null means default-route method restriction only.\n     * @param string|null $name Route name\n     */\n    public function __construct(?string $path = null, ?string $name = null)\n    {\n        parent::__construct($path, 'DELETE', $name);\n    }\n}\n\n"
  },
  {
    "path": "src/support/annotation/route/DisableDefaultRoute.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Disable webman's default route mapping for a controller or action.\n */\n#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]\nclass DisableDefaultRoute\n{\n}"
  },
  {
    "path": "src/support/annotation/route/Get.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'GET', ...)].\n */\n#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]\nclass Get extends Route\n{\n    /**\n     * @param string|null $path Route path. Null means default-route method restriction only.\n     * @param string|null $name Route name\n     */\n    public function __construct(?string $path = null, ?string $name = null)\n    {\n        parent::__construct($path, 'GET', $name);\n    }\n}\n\n"
  },
  {
    "path": "src/support/annotation/route/Head.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'HEAD', ...)].\n */\n#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]\nclass Head extends Route\n{\n    /**\n     * @param string|null $path Route path. Null means default-route method restriction only.\n     * @param string|null $name Route name\n     */\n    public function __construct(?string $path = null, ?string $name = null)\n    {\n        parent::__construct($path, 'HEAD', $name);\n    }\n}\n\n"
  },
  {
    "path": "src/support/annotation/route/Options.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'OPTIONS', ...)].\n */\n#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]\nclass Options extends Route\n{\n    /**\n     * @param string|null $path Route path. Null means default-route method restriction only.\n     * @param string|null $name Route name\n     */\n    public function __construct(?string $path = null, ?string $name = null)\n    {\n        parent::__construct($path, 'OPTIONS', $name);\n    }\n}\n\n"
  },
  {
    "path": "src/support/annotation/route/Patch.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'PATCH', ...)].\n */\n#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]\nclass Patch extends Route\n{\n    /**\n     * @param string|null $path Route path. Null means default-route method restriction only.\n     * @param string|null $name Route name\n     */\n    public function __construct(?string $path = null, ?string $name = null)\n    {\n        parent::__construct($path, 'PATCH', $name);\n    }\n}\n\n"
  },
  {
    "path": "src/support/annotation/route/Post.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'POST', ...)].\n */\n#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]\nclass Post extends Route\n{\n    /**\n     * @param string|null $path Route path. Null means default-route method restriction only.\n     * @param string|null $name Route name\n     */\n    public function __construct(?string $path = null, ?string $name = null)\n    {\n        parent::__construct($path, 'POST', $name);\n    }\n}\n\n"
  },
  {
    "path": "src/support/annotation/route/Put.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'PUT', ...)].\n */\n#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]\nclass Put extends Route\n{\n    /**\n     * @param string|null $path Route path. Null means default-route method restriction only.\n     * @param string|null $name Route name\n     */\n    public function __construct(?string $path = null, ?string $name = null)\n    {\n        parent::__construct($path, 'PUT', $name);\n    }\n}\n\n"
  },
  {
    "path": "src/support/annotation/route/Route.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Define an explicit route, or restrict allowed HTTP methods for default route when path is null.\n */\n#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]\nclass Route\n{\n    /**\n     * Route path. Null means \"method restriction only\" for default route.\n     */\n    public ?string $path;\n\n    /**\n     * @var string[]\n     */\n    public array $methods;\n\n    /**\n     * Route name for URL generation.\n     */\n    public ?string $name;\n\n    /**\n     * @param string|null $path Route path, must start with \"/\". Null means \"method restriction only\" for default route.\n     * @param string|string[] $methods HTTP methods\n     * @param string|null $name Route name\n     */\n    public function __construct(?string $path = null, array|string $methods = ['GET'], ?string $name = null)\n    {\n        $this->path = $path;\n        $this->methods = is_array($methods) ? $methods : [$methods];\n        $this->name = $name;\n    }\n}\n\n"
  },
  {
    "path": "src/support/annotation/route/RouteGroup.php",
    "content": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Group routes by controller-level prefix.\n */\n#[Attribute(Attribute::TARGET_CLASS)]\nclass RouteGroup\n{\n    /**\n     * Prefix for all routes in this controller.\n     */\n    public string $prefix;\n\n    /**\n     * @param string $prefix Route group prefix, e.g. \"/api/v1\"\n     */\n    public function __construct(string $prefix = '')\n    {\n        $this->prefix = $prefix;\n    }\n}\n\n"
  },
  {
    "path": "src/support/bootstrap/Session.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support\\bootstrap;\n\nuse Webman\\Bootstrap;\nuse Workerman\\Protocols\\Http;\nuse Workerman\\Protocols\\Http\\Session as SessionBase;\nuse Workerman\\Worker;\nuse function config;\nuse function property_exists;\n\n/**\n * Class Session\n * @package support\n */\nclass Session implements Bootstrap\n{\n\n    /**\n     * @param Worker|null $worker\n     * @return void\n     */\n    public static function start(?Worker $worker)\n    {\n        $config = config('session');\n        if (property_exists(SessionBase::class, 'name')) {\n            SessionBase::$name = $config['session_name'];\n        } else {\n            Http::sessionName($config['session_name']);\n        }\n        SessionBase::handlerClass($config['handler'], $config['config'][$config['type']]);\n        $map = [\n            'auto_update_timestamp' => 'autoUpdateTimestamp',\n            'cookie_lifetime' => 'cookieLifetime',\n            'gc_probability' => 'gcProbability',\n            'cookie_path' => 'cookiePath',\n            'http_only' => 'httpOnly',\n            'same_site' => 'sameSite',\n            'lifetime' => 'lifetime',\n            'domain' => 'domain',\n            'secure' => 'secure',\n        ];\n        foreach ($map as $key => $name) {\n            if (isset($config[$key]) && property_exists(SessionBase::class, $name)) {\n                SessionBase::${$name} = $config[$key];\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/support/bootstrap.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nuse Dotenv\\Dotenv;\nuse support\\Log;\nuse Webman\\Bootstrap;\nuse Webman\\Config;\nuse Webman\\Middleware;\nuse Webman\\Route;\nuse Webman\\Util;\nuse Workerman\\Events\\Select;\nuse Workerman\\Worker;\n\n$worker = $worker ?? null;\n\nif (empty(Worker::$eventLoopClass)) {\n    Worker::$eventLoopClass = Select::class;\n}\n\nset_error_handler(function ($level, $message, $file = '', $line = 0) {\n    if (error_reporting() & $level) {\n        throw new ErrorException($message, 0, $level, $file, $line);\n    }\n});\n\nif ($worker) {\n    register_shutdown_function(function ($startTime) {\n        if (time() - $startTime <= 0.1) {\n            sleep(1);\n        }\n    }, time());\n}\n\nif (class_exists('Dotenv\\Dotenv') && file_exists(base_path(false) . '/.env')) {\n    if (method_exists('Dotenv\\Dotenv', 'createUnsafeMutable')) {\n        Dotenv::createUnsafeMutable(base_path(false))->load();\n    } else {\n        Dotenv::createMutable(base_path(false))->load();\n    }\n}\n\nConfig::clear();\nsupport\\App::loadAllConfig(['route']);\nif ($timezone = config('app.default_timezone')) {\n    date_default_timezone_set($timezone);\n}\n\nforeach (config('autoload.files', []) as $file) {\n    include_once $file;\n}\nforeach (config('plugin', []) as $firm => $projects) {\n    foreach ($projects as $name => $project) {\n        if (!is_array($project)) {\n            continue;\n        }\n        foreach ($project['autoload']['files'] ?? [] as $file) {\n            include_once $file;\n        }\n    }\n    foreach ($projects['autoload']['files'] ?? [] as $file) {\n        include_once $file;\n    }\n}\n\nMiddleware::load(config('middleware', []));\nforeach (config('plugin', []) as $firm => $projects) {\n    foreach ($projects as $name => $project) {\n        if (!is_array($project) || $name === 'static') {\n            continue;\n        }\n        Middleware::load($project['middleware'] ?? []);\n    }\n    Middleware::load($projects['middleware'] ?? [], $firm);\n    if ($staticMiddlewares = config(\"plugin.$firm.static.middleware\")) {\n        Middleware::load(['__static__' => $staticMiddlewares], $firm);\n    }\n}\nMiddleware::load(['__static__' => config('static.middleware', [])]);\n\nforeach (config('bootstrap', []) as $className) {\n    if (!class_exists($className)) {\n        $log = \"Warning: Class $className setting in config/bootstrap.php not found\\r\\n\";\n        echo $log;\n        Log::error($log);\n        continue;\n    }\n    /** @var Bootstrap $className */\n    $className::start($worker);\n}\n\nforeach (config('plugin', []) as $firm => $projects) {\n    foreach ($projects as $name => $project) {\n        if (!is_array($project)) {\n            continue;\n        }\n        foreach ($project['bootstrap'] ?? [] as $className) {\n            if (!class_exists($className)) {\n                $log = \"Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\\r\\n\";\n                echo $log;\n                Log::error($log);\n                continue;\n            }\n            /** @var Bootstrap $className */\n            $className::start($worker);\n        }\n    }\n    foreach ($projects['bootstrap'] ?? [] as $className) {\n        /** @var string $className */\n        if (!class_exists($className)) {\n            $log = \"Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\\r\\n\";\n            echo $log;\n            Log::error($log);\n            continue;\n        }\n        /** @var Bootstrap $className */\n        $className::start($worker);\n    }\n}\n\n$directory = base_path() . '/plugin';\n$paths = [config_path()];\nforeach (Util::scanDir($directory) as $path) {\n    if (is_dir($path = \"$path/config\")) {\n        $paths[] = $path;\n    }\n}\nRoute::load($paths);\n\n"
  },
  {
    "path": "src/support/exception/BusinessException.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support\\exception;\n\n/**\n * Class BusinessException\n * @package support\\exception\n */\nclass BusinessException extends \\Webman\\Exception\\BusinessException\n{\n\n}\n"
  },
  {
    "path": "src/support/exception/Handler.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support\\exception;\n\nuse Throwable;\nuse Webman\\Exception\\ExceptionHandler;\nuse Webman\\Http\\Request;\nuse Webman\\Http\\Response;\nuse Webman\\Exception\\BusinessException;\n\n/**\n * Class Handler\n * @package support\\exception\n */\nclass Handler extends ExceptionHandler\n{\n    public $dontReport = [\n        BusinessException::class,\n    ];\n\n    public function report(Throwable $exception)\n    {\n        parent::report($exception);\n    }\n\n    public function render(Request $request, Throwable $exception): Response\n    {\n        return parent::render($request, $exception);\n    }\n\n}"
  },
  {
    "path": "src/support/exception/InputTypeException.php",
    "content": "<?php\n\nnamespace support\\exception;\n\nuse Throwable;\n\nclass InputTypeException extends PageNotFoundException\n{\n\n    /**\n     * @var string\n     */\n    protected $template = '/app/view/400';\n\n    /**\n     * InputTypeException constructor.\n     * @param string $message\n     * @param int $code\n     * @param Throwable|null $previous\n     */\n    public function __construct(string $message = 'Input :parameter must be of type :exceptType, :actualType given', int $code = 400, ?Throwable $previous = null) {\n        parent::__construct($message, $code, $previous);\n    }\n}"
  },
  {
    "path": "src/support/exception/InputValueException.php",
    "content": "<?php\n\nnamespace support\\exception;\n\nuse Throwable;\n\nclass InputValueException extends PageNotFoundException\n{\n\n    /**\n     * @var string\n     */\n    protected $template = '/app/view/400';\n\n    /**\n     * InputTypeException constructor.\n     * @param string $message\n     * @param int $code\n     * @param Throwable|null $previous\n     */\n    public function __construct(string $message = 'Input :parameter is invalid.', int $code = 400, ?Throwable $previous = null) {\n        parent::__construct($message, $code, $previous);\n    }\n}"
  },
  {
    "path": "src/support/exception/MissingInputException.php",
    "content": "<?php\n\nnamespace support\\exception;\n\nuse Throwable;\nuse Webman\\Http\\Request;\nuse Webman\\Http\\Response;\n\nclass MissingInputException extends PageNotFoundException\n{\n    /**\n     * @var string\n     */\n    protected $template = '/app/view/400';\n\n    /**\n     * MissingInputException constructor.\n     * @param string $message\n     * @param int $code\n     * @param Throwable|null $previous\n     */\n    public function __construct(string $message = 'Missing input parameter :parameter', int $code = 400, ?Throwable $previous = null) {\n        parent::__construct($message, $code, $previous);\n    }\n\n    /**\n     * Render an exception into an HTTP response.\n     * @param Request $request\n     * @return Response|null\n     * @throws Throwable\n     */\n    public function render(Request $request): ?Response\n    {\n        $code = $this->getCode() ?: 404;\n        $debug = config($request->plugin ? \"plugin.$request->plugin.app.debug\" : 'app.debug');\n        $data = $debug ? $this->data : ['parameter' => ''];\n        $message = $this->trans($this->getMessage(), $data);\n        if ($request->expectsJson()) {\n            $json = ['code' => $code, 'msg' => $message, 'data' => $data];\n            return new Response(200, ['Content-Type' => 'application/json'],\n                json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));\n        }\n        return new Response($code, [], $this->html($message));\n    }\n\n}"
  },
  {
    "path": "src/support/exception/NotFoundException.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support\\exception;\n\nclass NotFoundException extends BusinessException\n{\n\n}\n"
  },
  {
    "path": "src/support/exception/PageNotFoundException.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support\\exception;\n\nuse Throwable;\nuse Webman\\Http\\Request;\nuse Webman\\Http\\Response;\n\nclass PageNotFoundException extends NotFoundException\n{\n\n    /**\n     * @var string\n     */\n    protected $template = '/app/view/404';\n\n    /**\n     * PageNotFoundException constructor.\n     * @param string $message\n     * @param int $code\n     * @param Throwable|null $previous\n     */\n    public function __construct(string $message = '404 Not Found', int $code = 404, ?Throwable $previous = null) {\n        parent::__construct($message, $code, $previous);\n    }\n\n    /**\n     * Render an exception into an HTTP response.\n     * @param Request $request\n     * @return Response|null\n     * @throws Throwable\n     */\n    public function render(Request $request): ?Response\n    {\n        $code = $this->getCode() ?: 404;\n        $data = $this->data;\n        $message = $this->trans($this->getMessage(), $data);\n        if ($request->expectsJson()) {\n            $json = ['code' => $code, 'msg' => $message, 'data' => $data];\n            return new Response(200, ['Content-Type' => 'application/json'],\n                json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));\n        }\n        return new Response($code, [], $this->html($message));\n    }\n\n    /**\n     * Get the HTML representation of the exception.\n     * @param string $message\n     * @return string\n     * @throws Throwable\n     */\n    protected function html(string $message): string\n    {\n        $message = htmlspecialchars($message);\n        if (is_file(base_path(\"$this->template.html\"))) {\n            return raw_view($this->template, ['message' => $message])->rawBody();\n        }\n        return <<<EOF\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>$message</title>\n    <style>\n        .center {\n            text-align: center;\n        }\n    </style>\n</head>\n<body>\n    <h1 class=\"center\">$message</h1>\n    <hr>\n    <div class=\"center\">webman</div>\n</body>\n</html>\nEOF;\n\n    }\n\n}\n"
  },
  {
    "path": "src/support/helpers.php",
    "content": "<?php\n\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nuse support\\Container;\nuse support\\Request;\nuse support\\Response;\nuse support\\Translation;\nuse support\\view\\Blade;\nuse support\\view\\Raw;\nuse support\\view\\ThinkPHP;\nuse support\\view\\Twig;\nuse Twig\\Error\\LoaderError;\nuse Twig\\Error\\RuntimeError;\nuse Twig\\Error\\SyntaxError;\nuse Webman\\App;\nuse Webman\\Config;\nuse Webman\\Route;\nuse Workerman\\Protocols\\Http\\Session;\nuse Workerman\\Worker;\n\n/**\n * Get the base path of the application\n */\nif (!defined('BASE_PATH')) {\n    if (!$basePath = Phar::running()) {\n        $basePath = getcwd();\n        while ($basePath !== dirname($basePath)) {\n            if (is_dir(\"$basePath/vendor\") && is_file(\"$basePath/start.php\")) {\n                break;\n            }\n            $basePath = dirname($basePath);\n        }\n        if ($basePath === dirname($basePath)) {\n            $basePath = __DIR__ . '/../../../../../';\n        }\n    }\n    define('BASE_PATH', realpath($basePath) ?: $basePath);\n}\n\nif (!function_exists('run_path')) {\n    /**\n     * return the program execute directory\n     * @param string $path\n     * @return string\n     */\n    function run_path(string $path = ''): string\n    {\n        static $runPath = '';\n        if (!$runPath) {\n            $runPath = is_phar() ? dirname(Phar::running(false)) : BASE_PATH;\n        }\n        return path_combine($runPath, $path);\n    }\n}\n\nif (!function_exists('base_path')) {\n    /**\n     * if the param $path equal false,will return this program current execute directory\n     * @param string|false $path\n     * @return string\n     */\n    function base_path($path = ''): string\n    {\n        if (false === $path) {\n            return run_path();\n        }\n        return path_combine(BASE_PATH, $path);\n    }\n}\n\nif (!function_exists('app_path')) {\n    /**\n     * App path\n     * @param string $path\n     * @return string\n     */\n    function app_path(string $path = ''): string\n    {\n        return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'app', $path);\n    }\n}\n\nif (!function_exists('public_path')) {\n    /**\n     * Public path\n     * @param string $path\n     * @param string|null $plugin\n     * @return string\n     */\n    function public_path(string $path = '', ?string $plugin = null): string\n    {\n        static $publicPaths = [];\n        $plugin = $plugin ?? '';\n        if (isset($publicPaths[$plugin])) {\n            $publicPath = $publicPaths[$plugin];\n        } else {\n            $prefix = $plugin ? \"plugin.$plugin.\" : '';\n            $pathPrefix = $plugin ? 'plugin' . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR : '';\n            $publicPath = \\config(\"{$prefix}app.public_path\", run_path(\"{$pathPrefix}public\"));\n            if (count($publicPaths) > 32) {\n                $publicPaths = [];\n            }\n            $publicPaths[$plugin] = $publicPath;\n        }\n        return $path === '' ? $publicPath : path_combine($publicPath, $path);\n    }\n}\n\nif (!function_exists('config_path')) {\n    /**\n     * Config path\n     * @param string $path\n     * @return string\n     */\n    function config_path(string $path = ''): string\n    {\n        return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'config', $path);\n    }\n}\n\nif (!function_exists('runtime_path')) {\n    /**\n     * Runtime path\n     * @param string $path\n     * @return string\n     */\n    function runtime_path(string $path = ''): string\n    {\n        static $runtimePath = '';\n        if (!$runtimePath) {\n            $runtimePath = \\config('app.runtime_path') ?: run_path('runtime');\n        }\n        return path_combine($runtimePath, $path);\n    }\n}\n\nif (!function_exists('path_combine')) {\n    /**\n     * Generate paths based on given information\n     * @param string $front\n     * @param string $back\n     * @return string\n     */\n    function path_combine(string $front, string $back): string\n    {\n        return $front . ($back ? (DIRECTORY_SEPARATOR . ltrim($back, DIRECTORY_SEPARATOR)) : $back);\n    }\n}\n\nif (!function_exists('response')) {\n    /**\n     * Response\n     * @param int $status\n     * @param array $headers\n     * @param string $body\n     * @return Response\n     */\n    function response(string $body = '', int $status = 200, array $headers = []): Response\n    {\n        return new Response($status, $headers, $body);\n    }\n}\n\nif (!function_exists('json')) {\n    /**\n     * Json response\n     * @param $data\n     * @param int $options\n     * @return Response\n     */\n    function json($data, int $options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR): Response\n    {\n        return new Response(200, ['Content-Type' => 'application/json'], json_encode($data, $options));\n    }\n}\n\nif (!function_exists('xml')) {\n    /**\n     * Xml response\n     * @param $xml\n     * @return Response\n     */\n    function xml($xml): Response\n    {\n        if ($xml instanceof SimpleXMLElement) {\n            $xml = $xml->asXML();\n        }\n        return new Response(200, ['Content-Type' => 'text/xml'], $xml);\n    }\n}\n\nif (!function_exists('jsonp')) {\n    /**\n     * Jsonp response\n     * @param $data\n     * @param string $callbackName\n     * @return Response\n     */\n    function jsonp($data, string $callbackName = 'callback'): Response\n    {\n        if (!is_scalar($data) && null !== $data) {\n            $data = json_encode($data);\n        }\n        return new Response(200, [], \"$callbackName($data)\");\n    }\n}\n\nif (!function_exists('redirect')) {\n    /**\n     * Redirect response\n     * @param string $location\n     * @param int $status\n     * @param array $headers\n     * @return Response\n     */\n    function redirect(string $location, int $status = 302, array $headers = []): Response\n    {\n        $response = new Response($status, ['Location' => $location]);\n        if (!empty($headers)) {\n            $response->withHeaders($headers);\n        }\n        return $response;\n    }\n}\n\nif (!function_exists('view')) {\n    /**\n     * View response\n     * @param mixed $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return Response\n     */\n    function view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response\n    {\n        [$template, $vars, $app, $plugin] = template_inputs($template, $vars, $app, $plugin);\n        $handler = \\config($plugin ? \"plugin.$plugin.view.handler\" : 'view.handler');\n        return new Response(200, [], $handler::render($template, $vars, $app, $plugin));\n    }\n}\n\nif (!function_exists('raw_view')) {\n    /**\n     * Raw view response\n     * @param mixed $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return Response\n     * @throws Throwable\n     */\n    function raw_view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response\n    {\n        return new Response(200, [], Raw::render(...template_inputs($template, $vars, $app, $plugin)));\n    }\n}\n\nif (!function_exists('blade_view')) {\n    /**\n     * Blade view response\n     * @param mixed $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return Response\n     */\n    function blade_view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response\n    {\n        return new Response(200, [], Blade::render(...template_inputs($template, $vars, $app, $plugin)));\n    }\n}\n\nif (!function_exists('think_view')) {\n    /**\n     * Think view response\n     * @param mixed $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return Response\n     */\n    function think_view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response\n    {\n        return new Response(200, [], ThinkPHP::render(...template_inputs($template, $vars, $app, $plugin)));\n    }\n}\n\nif (!function_exists('twig_view')) {\n    /**\n     * Twig view response\n     * @param mixed $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return Response\n     */\n    function twig_view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response\n    {\n        return new Response(200, [], Twig::render(...template_inputs($template, $vars, $app, $plugin)));\n    }\n}\n\nif (!function_exists('request')) {\n    /**\n     * Get request\n     * @return \\Webman\\Http\\Request|Request|null\n     */\n    function request()\n    {\n        return App::request();\n    }\n}\n\nif (!function_exists('config')) {\n    /**\n     * Get config\n     * @param string|null $key\n     * @param mixed $default\n     * @return mixed\n     */\n    function config(?string $key = null, mixed $default = null)\n    {\n        return Config::get($key, $default);\n    }\n}\n\nif (!function_exists('route')) {\n    /**\n     * Create url\n     * @param string $name\n     * @param ...$parameters\n     * @return string\n     */\n    function route(string $name, ...$parameters): string\n    {\n        $route = Route::getByName($name);\n        if (!$route) {\n            return '';\n        }\n\n        if (!$parameters) {\n            return $route->url();\n        }\n\n        if (is_array(current($parameters))) {\n            $parameters = current($parameters);\n        }\n\n        return $route->url($parameters);\n    }\n}\n\nif (!function_exists('session')) {\n    /**\n     * Session\n     * @param array|string|null $key\n     * @param mixed $default\n     * @return mixed|bool|Session\n     * @throws Exception\n     */\n    function session(array|string|null $key = null, mixed $default = null): mixed\n    {\n        $session = \\request()->session();\n        if (null === $key) {\n            return $session;\n        }\n        if (is_array($key)) {\n            $session->put($key);\n            return null;\n        }\n        if (strpos($key, '.')) {\n            $keyArray = explode('.', $key);\n            $value = $session->all();\n            foreach ($keyArray as $index) {\n                if (!isset($value[$index])) {\n                    return $default;\n                }\n                $value = $value[$index];\n            }\n            return $value;\n        }\n        return $session->get($key, $default);\n    }\n}\n\nif (!function_exists('trans')) {\n    /**\n     * Translation\n     * @param string $id\n     * @param array $parameters\n     * @param string|null $domain\n     * @param string|null $locale\n     * @return string\n     */\n    function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string\n    {\n        $res = Translation::trans($id, $parameters, $domain, $locale);\n        return $res === '' ? $id : $res;\n    }\n}\n\nif (!function_exists('locale')) {\n    /**\n     * Locale\n     * @param string|null $locale\n     * @return string\n     */\n    function locale(?string $locale = null): string\n    {\n        if (!$locale) {\n            return Translation::getLocale();\n        }\n        Translation::setLocale($locale);\n        return $locale;\n    }\n}\n\nif (!function_exists('not_found')) {\n    /**\n     * 404 not found\n     * @return Response\n     */\n    function not_found(): Response\n    {\n        return new Response(404, [], file_get_contents(public_path() . '/404.html'));\n    }\n}\n\nif (!function_exists('copy_dir')) {\n    /**\n     * Copy dir\n     * @param string $source\n     * @param string $dest\n     * @param bool $overwrite\n     * @return void\n     */\n    function copy_dir(string $source, string $dest, bool $overwrite = false)\n    {\n        if (is_dir($source)) {\n            if (!is_dir($dest)) {\n                mkdir($dest);\n            }\n            $files = scandir($source);\n            foreach ($files as $file) {\n                if ($file !== \".\" && $file !== \"..\") {\n                    copy_dir(\"$source/$file\", \"$dest/$file\", $overwrite);\n                }\n            }\n        } else if (file_exists($source) && ($overwrite || !file_exists($dest))) {\n            copy($source, $dest);\n        }\n    }\n}\n\nif (!function_exists('remove_dir')) {\n    /**\n     * Remove dir\n     * @param string $dir\n     * @return bool\n     */\n    function remove_dir(string $dir): bool\n    {\n        if (is_link($dir) || is_file($dir)) {\n            return unlink($dir);\n        }\n        $files = array_diff(scandir($dir), array('.', '..'));\n        foreach ($files as $file) {\n            (is_dir(\"$dir/$file\") && !is_link($dir)) ? remove_dir(\"$dir/$file\") : unlink(\"$dir/$file\");\n        }\n        return rmdir($dir);\n    }\n}\n\nif (!function_exists('worker_bind')) {\n    /**\n     * Bind worker\n     * @param $worker\n     * @param $class\n     */\n    function worker_bind($worker, $class)\n    {\n        $callbackMap = [\n            'onConnect',\n            'onMessage',\n            'onClose',\n            'onError',\n            'onBufferFull',\n            'onBufferDrain',\n            'onWorkerStop',\n            'onWebSocketConnect',\n            'onWorkerReload'\n        ];\n        foreach ($callbackMap as $name) {\n            if (method_exists($class, $name)) {\n                $worker->$name = [$class, $name];\n            }\n        }\n        if (method_exists($class, 'onWorkerStart')) {\n            call_user_func([$class, 'onWorkerStart'], $worker);\n        }\n    }\n}\n\nif (!function_exists('worker_start')) {\n    /**\n     * Start worker\n     * @param $processName\n     * @param $config\n     * @return void\n     */\n    function worker_start($processName, $config)\n    {\n        if (isset($config['enable']) && !$config['enable']) {\n            return;\n        }\n        // feat：custom worker class [default: Workerman\\Worker]\n        $class = is_a($class = $config['workerClass'] ?? '', Worker::class, true) ? $class : Worker::class;\n        $worker = new $class($config['listen'] ?? null, $config['context'] ?? []);\n        $properties = [\n            'count',\n            'user',\n            'group',\n            'reloadable',\n            'reusePort',\n            'transport',\n            'protocol',\n            'eventLoop',\n        ];\n        $worker->name = $processName;\n        foreach ($properties as $property) {\n            if (isset($config[$property])) {\n                $worker->$property = $config[$property];\n            }\n        }\n\n        $worker->onWorkerStart = function ($worker) use ($config) {\n            require_once base_path('/support/bootstrap.php');\n            if (isset($config['handler'])) {\n                if (!class_exists($config['handler'])) {\n                    echo \"process error: class {$config['handler']} not exists\\r\\n\";\n                    return;\n                }\n\n                $instance = Container::make($config['handler'], $config['constructor'] ?? []);\n                worker_bind($worker, $instance);\n            }\n        };\n    }\n}\n\nif (!function_exists('get_realpath')) {\n    /**\n     * Get realpath\n     * @param string $filePath\n     * @return string\n     */\n    function get_realpath(string $filePath): string\n    {\n        if (strpos($filePath, 'phar://') === 0) {\n            return $filePath;\n        } else {\n            return realpath($filePath);\n        }\n    }\n}\n\nif (!function_exists('is_phar')) {\n    /**\n     * Is phar\n     * @return bool\n     */\n    function is_phar(): bool\n    {\n        return class_exists(Phar::class, false) && Phar::running();\n    }\n}\n\nif (!function_exists('template_inputs')) {\n    /**\n     * Get template vars\n     * @param mixed $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return array\n     */\n    function template_inputs(mixed $template, array $vars, ?string $app, ?string $plugin): array\n    {\n        $request = \\request();\n        $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;\n        if (is_array($template)) {\n            $vars = $template;\n            $template = null;\n        }\n        if ($template === null && $controller = $request->controller) {\n            $controllerSuffix = config($plugin ? \"plugin.$plugin.app.controller_suffix\" : \"app.controller_suffix\", '');\n            $controllerName = $controllerSuffix !== '' ? substr($controller, 0, -strlen($controllerSuffix)) : $controller;\n            $path = str_replace(['controller', 'Controller', '\\\\'], ['view', 'view', '/'], $controllerName);\n            $path = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $path));\n            $action = $request->action;\n            $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);\n            foreach ($backtrace as $backtraceItem) {\n                if (!isset($backtraceItem['class']) || !isset($backtraceItem['function'])) {\n                    continue;\n                }\n                if ($backtraceItem['class'] === App::class) {\n                    break;\n                }\n                if (preg_match('/\\\\\\\\controller\\\\\\\\/i', $backtraceItem['class'])) {\n                    $action = $backtraceItem['function'];\n                    break;\n                }\n            }\n            $actionFileBaseName = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $action));\n            $template = \"/$path/$actionFileBaseName\";\n        }\n        return [$template, $vars, $app, $plugin];\n    }\n}\n\nif (!function_exists('cpu_count')) {\n    /**\n     * Get cpu count\n     * @return int\n     */\n    function cpu_count(): int\n    {\n        // Windows does not support the number of processes setting.\n        if (DIRECTORY_SEPARATOR === '\\\\') {\n            return 1;\n        }\n        $count = 4;\n        if (is_callable('shell_exec')) {\n            if (strtolower(PHP_OS) === 'darwin') {\n                $count = (int)shell_exec('sysctl -n machdep.cpu.core_count');\n            } else {\n                try {\n                    $count = (int)shell_exec('nproc');\n                } catch (\\Throwable $ex) {\n                    // Do nothing\n                }\n            }\n        }\n        return $count > 0 ? $count : 4;\n    }\n}\n\nif (!function_exists('input')) {\n    /**\n     * Get request parameters, if no parameter name is passed, an array of all values is returned, default values is supported\n     * @param string|null $param param's name\n     * @param mixed $default default value\n     * @return mixed\n     */\n    function input(?string $param = null, mixed $default = null): mixed\n    {\n        return is_null($param) ? request()->all() : request()->input($param, $default);\n    }\n}\n"
  },
  {
    "path": "src/support/view/Blade.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support\\view;\n\nuse Jenssegers\\Blade\\Blade as BladeView;\nuse Webman\\View;\nuse function app_path;\nuse function array_merge;\nuse function base_path;\nuse function config;\nuse function is_array;\nuse function request;\nuse function runtime_path;\n\n/**\n * Class Blade\n * composer require jenssegers/blade\n * @package support\\view\n */\nclass Blade implements View\n{\n    /**\n     * Assign.\n     * @param string|array $name\n     * @param mixed $value\n     */\n    public static function assign(string|array $name, mixed $value = null): void\n    {\n        $request = request();\n        $request->_view_vars = array_merge((array) $request->_view_vars, is_array($name) ? $name : [$name => $value]);\n    }\n\n    /**\n     * Render.\n     * @param string $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return string\n     */\n    public static function render(string $template, array $vars, ?string $app = null, ?string $plugin = null): string\n    {\n        static $views = [];\n        $request = request();\n        $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;\n        $app = $app === null ? ($request->app ?? '') : $app;\n        $configPrefix = $plugin ? \"plugin.$plugin.\" : '';\n        $baseViewPath = $plugin ? base_path() . \"/plugin/$plugin/app\" : app_path();\n        if ($template[0] === '/') {\n            if (strpos($template, '/view/') !== false) {\n                [$viewPath, $template] = explode('/view/', $template, 2);\n                $viewPath = base_path(\"$viewPath/view\");\n            } else {\n                $viewPath = base_path();\n                $template = ltrim($template, '/');\n            }\n        } else {\n            $viewPath = $app === '' ? \"$baseViewPath/view\" : \"$baseViewPath/$app/view\";\n        }\n        if (!isset($views[$viewPath])) {\n            $views[$viewPath] = new BladeView($viewPath, runtime_path() . '/views');\n            $extension = config(\"{$configPrefix}view.extension\");\n            if ($extension) {\n                $extension($views[$viewPath]);\n            }\n        }\n        if(isset($request->_view_vars)) {\n            $vars = array_merge((array)$request->_view_vars, $vars);\n        }\n        return $views[$viewPath]->render($template, $vars);\n    }\n}\n"
  },
  {
    "path": "src/support/view/Raw.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support\\view;\n\nuse Throwable;\nuse Webman\\View;\nuse function app_path;\nuse function array_merge;\nuse function base_path;\nuse function config;\nuse function extract;\nuse function is_array;\nuse function ob_end_clean;\nuse function ob_get_clean;\nuse function ob_start;\nuse function request;\n\n/**\n * Class Raw\n * @package support\\view\n */\nclass Raw implements View\n{\n    /**\n     * Assign.\n     * @param string|array $name\n     * @param mixed $value\n     */\n    public static function assign(string|array $name, mixed $value = null): void\n    {\n        $request = request();\n        $request->_view_vars = array_merge((array) $request->_view_vars, is_array($name) ? $name : [$name => $value]);\n    }\n\n    /**\n     * Render.\n     * @param string $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return string\n     */\n    public static function render(string $template, array $vars, ?string $app = null, ?string $plugin = null): string\n    {\n        $request = request();\n        $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;\n        $configPrefix = $plugin ? \"plugin.$plugin.\" : '';\n        $viewSuffix = config(\"{$configPrefix}view.options.view_suffix\", 'html');\n        $app = $app === null ? ($request->app ?? '') : $app;\n        $baseViewPath = $plugin ? base_path() . \"/plugin/$plugin/app\" : app_path();\n        $__template_path__ = $template[0] === '/' ? base_path() . \"$template.$viewSuffix\" : ($app === '' ? \"$baseViewPath/view/$template.$viewSuffix\" : \"$baseViewPath/$app/view/$template.$viewSuffix\");\n        if(isset($request->_view_vars)) {\n            extract((array)$request->_view_vars);\n        }\n        extract($vars);\n        ob_start();\n        // Try to include php file.\n        try {\n            include $__template_path__;\n        } catch (Throwable $e) {\n            ob_end_clean();\n            throw $e;\n        }\n\n        return ob_get_clean();\n    }\n}\n"
  },
  {
    "path": "src/support/view/ThinkPHP.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support\\view;\n\nuse think\\Template;\nuse Webman\\View;\nuse function app_path;\nuse function array_merge;\nuse function base_path;\nuse function config;\nuse function is_array;\nuse function ob_get_clean;\nuse function ob_start;\nuse function request;\nuse function runtime_path;\n\n/**\n * Class Blade\n * @package support\\view\n */\nclass ThinkPHP implements View\n{\n    /**\n     * Assign.\n     * @param string|array $name\n     * @param mixed $value\n     */\n    public static function assign(string|array $name, mixed $value = null): void\n    {\n        $request = request();\n        $request->_view_vars = array_merge((array) $request->_view_vars, is_array($name) ? $name : [$name => $value]);\n    }\n\n    /**\n     * Render.\n     * @param string $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return string\n     */\n    public static function render(string $template, array $vars, ?string $app = null, ?string $plugin = null): string\n    {\n        $request = request();\n        $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;\n        $app = $app === null ? ($request->app ?? '') : $app;\n        $configPrefix = $plugin ? \"plugin.$plugin.\" : '';\n        $viewSuffix = config(\"{$configPrefix}view.options.view_suffix\", 'html');\n        $baseViewPath = $plugin ? base_path() . \"/plugin/$plugin/app\" : app_path();\n        if ($template[0] === '/') {\n            if (strpos($template, '/view/') !== false) {\n                [$viewPath, $template] = explode('/view/', $template, 2);\n                $viewPath = base_path(\"$viewPath/view/\");\n            } else {\n                $viewPath = base_path() . dirname($template) . '/';\n                $template = basename($template);\n            }\n        } else {\n            $viewPath = $app === '' ? \"$baseViewPath/view/\" : \"$baseViewPath/$app/view/\";\n        }\n        $defaultOptions = [\n            'view_path' => $viewPath,\n            'cache_path' => runtime_path() . '/views/',\n            'view_suffix' => $viewSuffix\n        ];\n        $options = array_merge($defaultOptions, config(\"{$configPrefix}view.options\", []));\n        $views = new Template($options);\n        ob_start();\n        if(isset($request->_view_vars)) {\n            $vars = array_merge((array)$request->_view_vars, $vars);\n        }\n        $views->fetch($template, $vars);\n        return ob_get_clean();\n    }\n}\n"
  },
  {
    "path": "src/support/view/Twig.php",
    "content": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license information, please see the MIT-LICENSE.txt\n * Redistributions of files must retain the above copyright notice.\n *\n * @author    walkor<walkor@workerman.net>\n * @copyright walkor<walkor@workerman.net>\n * @link      http://www.workerman.net/\n * @license   http://www.opensource.org/licenses/mit-license.php MIT License\n */\n\nnamespace support\\view;\n\nuse Twig\\Environment;\nuse Twig\\Error\\LoaderError;\nuse Twig\\Error\\RuntimeError;\nuse Twig\\Error\\SyntaxError;\nuse Twig\\Loader\\FilesystemLoader;\nuse Webman\\View;\nuse function app_path;\nuse function array_merge;\nuse function base_path;\nuse function config;\nuse function is_array;\nuse function request;\n\n/**\n * Class Blade\n * @package support\\view\n */\nclass Twig implements View\n{\n    /**\n     * Assign.\n     * @param string|array $name\n     * @param mixed $value\n     */\n    public static function assign(string|array $name, mixed $value = null): void\n    {\n        $request = request();\n        $request->_view_vars = array_merge((array) $request->_view_vars, is_array($name) ? $name : [$name => $value]);\n    }\n\n    /**\n     * Render.\n     * @param string $template\n     * @param array $vars\n     * @param string|null $app\n     * @param string|null $plugin\n     * @return string\n     */\n    public static function render(string $template, array $vars, ?string $app = null, ?string $plugin = null): string\n    {\n        static $views = [];\n        $request = request();\n        $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;\n        $app = $app === null ? ($request->app ?? '') : $app;\n        $configPrefix = $plugin ? \"plugin.$plugin.\" : '';\n        $viewSuffix = config(\"{$configPrefix}view.options.view_suffix\", 'html');\n        $baseViewPath = $plugin ? base_path() . \"/plugin/$plugin/app\" : app_path();\n        if ($template[0] === '/') {\n            $template = ltrim($template, '/');\n            if (strpos($template, '/view/') !== false) {\n                [$viewPath, $template] = explode('/view/', $template, 2);\n                $viewPath = base_path(\"$viewPath/view\");\n            } else {\n                $viewPath = base_path();\n            }\n        } else {\n            $viewPath = $app === '' ? \"$baseViewPath/view/\" : \"$baseViewPath/$app/view/\";\n        }\n        if (!isset($views[$viewPath])) {\n            $views[$viewPath] = new Environment(new FilesystemLoader($viewPath), config(\"{$configPrefix}view.options\", []));\n            $extension = config(\"{$configPrefix}view.extension\");\n            if ($extension) {\n                $extension($views[$viewPath]);\n            }\n        }\n        if(isset($request->_view_vars)) {\n            $vars = array_merge((array)$request->_view_vars, $vars);\n        }\n        return $views[$viewPath]->render(\"$template.$viewSuffix\", $vars);\n    }\n}\n"
  },
  {
    "path": "src/windows.php",
    "content": "<?php\n/**\n * Start file for windows\n */\nchdir(__DIR__);\nrequire_once __DIR__ . '/vendor/autoload.php';\n\nuse Dotenv\\Dotenv;\nuse support\\App;\nuse Workerman\\Worker;\n\nini_set('display_errors', 'on');\nerror_reporting(E_ALL);\n\nif (class_exists('Dotenv\\Dotenv') && file_exists(base_path() . '/.env')) {\n    if (method_exists('Dotenv\\Dotenv', 'createUnsafeImmutable')) {\n        Dotenv::createUnsafeImmutable(base_path())->load();\n    } else {\n        Dotenv::createMutable(base_path())->load();\n    }\n}\n\nApp::loadAllConfig(['route']);\n\n$errorReporting = config('app.error_reporting');\nif (isset($errorReporting)) {\n    error_reporting($errorReporting);\n}\n\n$runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows';\n$paths = [\n    $runtimeProcessPath,\n    runtime_path('logs'),\n    runtime_path('views')\n];\nforeach ($paths as $path) {\n    if (!is_dir($path)) {\n        mkdir($path, 0777, true);\n    }\n}\n\n$processFiles = [];\nif (config('server.listen')) {\n    $processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php';\n}\nforeach (config('process', []) as $processName => $config) {\n    $processFiles[] = write_process_file($runtimeProcessPath, $processName, '');\n}\n\nforeach (config('plugin', []) as $firm => $projects) {\n    foreach ($projects as $name => $project) {\n        if (!is_array($project)) {\n            continue;\n        }\n        foreach ($project['process'] ?? [] as $processName => $config) {\n            $processFiles[] = write_process_file($runtimeProcessPath, $processName, \"$firm.$name\");\n        }\n    }\n    foreach ($projects['process'] ?? [] as $processName => $config) {\n        $processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm);\n    }\n}\n\nfunction write_process_file($runtimeProcessPath, $processName, $firm): string\n{\n    $processParam = $firm ? \"plugin.$firm.$processName\" : $processName;\n    $configParam = $firm ? \"config('plugin.$firm.process')['$processName']\" : \"config('process')['$processName']\";\n    $fileContent = <<<EOF\n<?php\nrequire_once __DIR__ . '/../../vendor/autoload.php';\n\nuse Workerman\\Worker;\nuse Workerman\\Connection\\TcpConnection;\nuse Webman\\Config;\nuse support\\App;\n\nini_set('display_errors', 'on');\nerror_reporting(E_ALL);\n\nif (is_callable('opcache_reset')) {\n    opcache_reset();\n}\n\nif (!\\$appConfigFile = config_path('app.php')) {\n    throw new RuntimeException('Config file not found: app.php');\n}\n\\$appConfig = require \\$appConfigFile;\nif (\\$timezone = \\$appConfig['default_timezone'] ?? '') {\n    date_default_timezone_set(\\$timezone);\n}\n\nApp::loadAllConfig(['route']);\n\nworker_start('$processParam', $configParam);\n\nif (DIRECTORY_SEPARATOR != \"/\") {\n    Worker::\\$logFile = config('server')['log_file'] ?? Worker::\\$logFile;\n    TcpConnection::\\$defaultMaxPackageSize = config('server')['max_package_size'] ?? 10*1024*1024;\n}\n\nWorker::runAll();\n\nEOF;\n    $processFile = $runtimeProcessPath . DIRECTORY_SEPARATOR . \"start_$processParam.php\";\n    file_put_contents($processFile, $fileContent);\n    return $processFile;\n}\n\nif ($monitorConfig = config('process.monitor.constructor')) {\n    $monitorHandler = config('process.monitor.handler');\n    $monitor = new $monitorHandler(...array_values($monitorConfig));\n}\n\nfunction popen_processes($processFiles)\n{\n    $cmd = '\"' . PHP_BINARY . '\" ' . implode(' ', $processFiles);\n    $descriptorspec = [STDIN, STDOUT, STDOUT];\n    $resource = proc_open($cmd, $descriptorspec, $pipes, null, null, ['bypass_shell' => true]);\n    if (!$resource) {\n        exit(\"Can not execute $cmd\\r\\n\");\n    }\n    return $resource;\n}\n\n$resource = popen_processes($processFiles);\necho \"\\r\\n\";\nwhile (1) {\n    sleep(1);\n    if (!empty($monitor) && $monitor->checkAllFilesChange()) {\n        $status = proc_get_status($resource);\n        $pid = $status['pid'];\n        shell_exec(\"taskkill /F /T /PID $pid\");\n        proc_close($resource);\n        $resource = popen_processes($processFiles);\n    }\n}\n"
  }
]