Full Code of walkor/webman-framework for AI

master 313d5fe1071a cached
68 files
237.9 KB
57.9k tokens
352 symbols
1 requests
Download .txt
Showing preview only (256K chars total). Download the full file or copy to clipboard to get everything.
Repository: walkor/webman-framework
Branch: master
Commit: 313d5fe1071a
Files: 68
Total size: 237.9 KB

Directory structure:
gitextract_e73a02zd/

├── .gitignore
├── README.md
├── composer.json
└── src/
    ├── App.php
    ├── Bootstrap.php
    ├── Config.php
    ├── Container.php
    ├── Context.php
    ├── Exception/
    │   ├── BusinessException.php
    │   ├── ExceptionHandler.php
    │   ├── ExceptionHandlerInterface.php
    │   ├── FileException.php
    │   └── NotFoundException.php
    ├── File.php
    ├── Finder/
    │   ├── ControllerFinder.php
    │   ├── FileInfo.php
    │   └── Finder.php
    ├── Http/
    │   ├── Request.php
    │   ├── Response.php
    │   └── UploadFile.php
    ├── Install.php
    ├── Middleware.php
    ├── MiddlewareInterface.php
    ├── Route/
    │   └── Route.php
    ├── Route.php
    ├── Session/
    │   ├── FileSessionHandler.php
    │   ├── RedisClusterSessionHandler.php
    │   └── RedisSessionHandler.php
    ├── Util.php
    ├── View.php
    ├── start.php
    ├── support/
    │   ├── App.php
    │   ├── Container.php
    │   ├── Context.php
    │   ├── Log.php
    │   ├── Plugin.php
    │   ├── Request.php
    │   ├── Response.php
    │   ├── Translation.php
    │   ├── View.php
    │   ├── annotation/
    │   │   ├── DisableDefaultRoute.php
    │   │   ├── Middleware.php
    │   │   └── route/
    │   │       ├── Any.php
    │   │       ├── Delete.php
    │   │       ├── DisableDefaultRoute.php
    │   │       ├── Get.php
    │   │       ├── Head.php
    │   │       ├── Options.php
    │   │       ├── Patch.php
    │   │       ├── Post.php
    │   │       ├── Put.php
    │   │       ├── Route.php
    │   │       └── RouteGroup.php
    │   ├── bootstrap/
    │   │   └── Session.php
    │   ├── bootstrap.php
    │   ├── exception/
    │   │   ├── BusinessException.php
    │   │   ├── Handler.php
    │   │   ├── InputTypeException.php
    │   │   ├── InputValueException.php
    │   │   ├── MissingInputException.php
    │   │   ├── NotFoundException.php
    │   │   └── PageNotFoundException.php
    │   ├── helpers.php
    │   └── view/
    │       ├── Blade.php
    │       ├── Raw.php
    │       ├── ThinkPHP.php
    │       └── Twig.php
    └── windows.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
composer.lock
vendor
vendor/
.idea
.idea/

================================================
FILE: README.md
================================================
# webman-framework
Note: 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.

## LICENSE
MIT


================================================
FILE: composer.json
================================================
{
  "name": "workerman/webman-framework",
  "type": "library",
  "keywords": [
    "high performance",
    "http service"
  ],
  "homepage": "https://www.workerman.net",
  "license": "MIT",
  "description": "High performance HTTP Service Framework.",
  "authors": [
    {
      "name": "walkor",
      "email": "walkor@workerman.net",
      "homepage": "https://www.workerman.net",
      "role": "Developer"
    }
  ],
  "support": {
    "email": "walkor@workerman.net",
    "issues": "https://github.com/walkor/webman/issues",
    "forum": "https://wenda.workerman.net/",
    "wiki": "https://doc.workerman.net/",
    "source": "https://github.com/walkor/webman-framework"
  },
  "require": {
    "php": ">=8.1",
    "ext-json": "*",
    "workerman/workerman": "^5.1 || dev-master",
    "nikic/fast-route": "^1.3",
    "psr/container": ">=1.0",
    "psr/log": "^2.0 || ^3.0"
  },
  "suggest": {
    "ext-event": "For better performance. "
  },
  "autoload": {
    "psr-4": {
      "Webman\\": "./src",
      "support\\": "./src/support",
      "Support\\": "./src/support",
      "Support\\Bootstrap\\": "./src/support/bootstrap",
      "Support\\Exception\\": "./src/support/exception",
      "Support\\View\\": "./src/support/view"
    },
    "files": [
      "./src/support/helpers.php"
    ]
  },
  "minimum-stability": "dev",
  "prefer-stable": true
}


================================================
FILE: src/App.php
================================================
<?php

/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman;

use ArrayObject;
use Closure;
use Exception;
use FastRoute\Dispatcher;
use Illuminate\Database\Eloquent\Model;
use Psr\Log\LoggerInterface;
use ReflectionEnum;
use support\exception\InputValueException;
use support\exception\PageNotFoundException;
use think\Model as ThinkModel;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use support\exception\MissingInputException;
use support\exception\RecordNotFoundException;
use support\exception\InputTypeException;
use Throwable;
use Webman\Exception\ExceptionHandler;
use Webman\Exception\ExceptionHandlerInterface;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\Route\Route as RouteObject;
use support\annotation\route\Route as RouteAttribute;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http;
use Workerman\Worker;
use function array_merge;
use function array_pop;
use function array_reduce;
use function array_splice;
use function array_values;
use function class_exists;
use function clearstatcache;
use function count;
use function current;
use function end;
use function explode;
use function get_class_methods;
use function gettype;
use function implode;
use function is_a;
use function is_array;
use function is_dir;
use function is_file;
use function is_numeric;
use function is_object;
use function is_string;
use function key;
use function method_exists;
use function ob_get_clean;
use function ob_start;
use function pathinfo;
use function scandir;
use function str_replace;
use function strpos;
use function strtolower;
use function substr;
use function trim;

/**
 * Class App
 * @package Webman
 */
class App
{

    /**
     * @var callable[]
     */
    protected static $callbacks = [];

    /**
     * @var array<string, ReflectionFunctionAbstract>
     */
    protected static array $reflectorCache = [];

    /**
     * @var array<string, array<int, array<string, mixed>>>
     */
    protected static array $parameterMetadataCache = [];

    /**
     * @var Worker
     */
    protected static $worker = null;

    /**
     * @var ?LoggerInterface
     */
    protected static ?LoggerInterface $logger = null;

    /**
     * @var string
     */
    protected static $appPath = '';

    /**
     * @var string
     */
    protected static $publicPath = '';

    /**
     * @var string
     */
    protected static $requestClass = '';

    /**
     * App constructor.
     * @param string $requestClass
     * @param LoggerInterface $logger
     * @param string $appPath
     * @param string $publicPath
     */
    public function __construct(string $requestClass, LoggerInterface $logger, string $appPath, string $publicPath)
    {
        static::$requestClass = $requestClass;
        static::$logger = $logger;
        static::$publicPath = $publicPath;
        static::$appPath = $appPath;
    }

    /**
     * OnMessage.
     * @param TcpConnection|mixed $connection
     * @param Request|mixed $request
     * @return null
     * @throws Throwable
     */
    public function onMessage($connection, $request)
    {
        try {
            Context::reset(new ArrayObject([Request::class => $request]));
            $path = $request->path();
            $key = $request->method() . $path;
            if (isset(static::$callbacks[$key])) {
                [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
                static::send($connection, $callback($request), $request);
                return null;
            }

            $status = 200;
            if (
                static::unsafeUri($connection, $path, $request) ||
                static::findFile($connection, $path, $key, $request) ||
                static::findRoute($connection, $path, $key, $request, $status)
            ) {
                return null;
            }

            $controllerAndAction = static::parseControllerAction($path);
            $plugin = $controllerAndAction['plugin'] ?? static::getPluginByPath($path);
            if (!$controllerAndAction || Route::isDefaultRouteDisabled($plugin, $controllerAndAction['app'] ?: '*') ||
                Route::isDefaultRouteDisabled($controllerAndAction['controller']) ||
                Route::isDefaultRouteDisabled([$controllerAndAction['controller'], $controllerAndAction['action']])) {
                $request->plugin = $plugin;
                $callback = static::getFallback($plugin, $status);
                $request->app = $request->controller = $request->action = '';
                static::send($connection, $callback($request), $request);
                return null;
            }
            $app = $controllerAndAction['app'];
            $controller = $controllerAndAction['controller'];
            $action = $controllerAndAction['action'];

            if ($methodNotAllowed = static::defaultRouteMethodNotAllowedResponse($controller, $action, $request->method())) {
                $callback = $methodNotAllowed;
                static::collectCallbacks($key, [$callback, $plugin, $app, $controller, $action, null]);
                [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
                static::send($connection, $callback($request), $request);
                return null;
            }

            $callback = static::getCallback($plugin, $app, [$controller, $action]);
            static::collectCallbacks($key, [$callback, $plugin, $app, $controller, $action, null]);
            [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
            static::send($connection, $callback($request), $request);
        } catch (Throwable $e) {
            static::send($connection, static::exceptionResponse($e, $request), $request);
        }
        return null;
    }

    /**
     * Method allowlist for default route (no explicit route path).
     * If action method has Route attributes with empty path, they are treated as allowed methods.
     * Returns a cached callback producing 405 response when current method is not allowed, otherwise null.
     * @param string $controllerClass
     * @param string $action
     * @param string $httpMethod
     * @return callable|null
     */
    protected static function defaultRouteMethodNotAllowedResponse(string $controllerClass, string $action, string $httpMethod): ?callable
    {
        $httpMethod = strtoupper($httpMethod);
        static $allowedCache = [];
        $cacheKey = $controllerClass . '::' . $action;

        if (!isset($allowedCache[$cacheKey])) {
            $allowed = [];
            try {
                if (method_exists($controllerClass, $action)) {
                    $ref = new ReflectionMethod($controllerClass, $action);
                    $attrs = $ref->getAttributes(RouteAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);
                    foreach ($attrs as $attr) {
                        /** @var RouteAttribute $route */
                        $route = $attr->newInstance();
                        if ($route->path !== null) {
                            continue;
                        }
                        foreach ($route->methods as $m) {
                            $m = strtoupper((string)$m);
                            $allowed[$m] = $m;
                        }
                    }
                }
            } catch (Throwable $e) {
            }
            $allowedCache[$cacheKey] = $allowed;
            if (count($allowedCache) > 1024) {
                unset($allowedCache[key($allowedCache)]);
            }
        } else {
            $allowed = $allowedCache[$cacheKey];
        }

        if (!$allowed || isset($allowed[$httpMethod])) {
            return null;
        }

        $allowHeader = implode(', ', array_values($allowed));
        return static function () use ($allowHeader) {
            return new Response(405, ['Allow' => $allowHeader], '405 Method Not Allowed');
        };
    }

    /**
     * OnWorkerStart.
     * @param $worker
     * @return void
     */
    public function onWorkerStart($worker)
    {
        static::$worker = $worker;
        Http::requestClass(static::$requestClass);
    }

    /**
     * CollectCallbacks.
     * @param string $key
     * @param array $data
     * @return void
     */
    protected static function collectCallbacks(string $key, array $data)
    {
        static::$callbacks[$key] = $data;
        if (count(static::$callbacks) > 1024) {
            unset(static::$callbacks[key(static::$callbacks)]);
        }
    }

    /**
     * UnsafeUri.
     * @param TcpConnection $connection
     * @param string $path
     * @param $request
     * @return bool
     */
    protected static function unsafeUri(TcpConnection $connection, string $path, $request): bool
    {
        if (!$path || $path[0] !== '/' || static::containsPathTraversal($path)) {
            $callback = static::getFallback('', 400);
            $request->plugin = $request->app = $request->controller = $request->action = '';
            static::send($connection, $callback($request, 400), $request);
            return true;
        }
        return false;
    }

    /**
     * Check if a path contains directory traversal or dangerous sequences.
     * @param string $path
     * @return bool
     */
    protected static function containsPathTraversal(string $path): bool
    {
        return strpos($path, '/../') !== false
            || substr($path, -3) === '/..'
            || strpos($path, "\\") !== false
            || strpos($path, "\0") !== false;
    }

    /**
     * GetFallback.
     * @param string $plugin
     * @param int $status
     * @return Closure
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     * @throws ReflectionException
     */
    protected static function getFallback(string $plugin = '', int $status = 404): Closure
    {
        // When route, controller and action not found, try to use Route::fallback
        return Route::getFallback($plugin, $status) ?: function () {
            throw new PageNotFoundException();
        };
    }

    /**
     * ExceptionResponse.
     * @param Throwable $e
     * @param $request
     * @return Response
     */
    protected static function exceptionResponse(Throwable $e, $request): Response
    {
        try {
            $app = $request->app ?: '';
            $plugin = $request->plugin ?: '';
            $exceptionConfig = static::config($plugin, 'exception');
            $appExceptionConfig = static::config("", 'exception');
            if (!isset($exceptionConfig['']) && isset($appExceptionConfig['@'])) {
                //如果插件没有配置自己的异常处理器并且配置了全局@异常处理器 则使用全局异常处理器
                $defaultException = $appExceptionConfig['@'] ?? ExceptionHandler::class;
            } else {
                $defaultException = $exceptionConfig[''] ?? ExceptionHandler::class;
            }
            $exceptionHandlerClass = $exceptionConfig[$app] ?? $defaultException;

            /** @var ExceptionHandlerInterface $exceptionHandler */
            $exceptionHandler = (static::container($plugin) ?? static::container(''))->make($exceptionHandlerClass, [
                'logger' => static::$logger,
                'debug' => static::config($plugin, 'app.debug')
            ]);
            $exceptionHandler->report($e);
            $response = $exceptionHandler->render($request, $e);
            $response->exception($e);
            return $response;
        } catch (Throwable $e) {
            $response = new Response(500, [], static::config($plugin ?? '', 'app.debug') ? (string)$e : $e->getMessage());
            $response->exception($e);
            return $response;
        }
    }

    /**
     * GetCallback.
     * @param string $plugin
     * @param string $app
     * @param $call
     * @param array $args
     * @param bool $withGlobalMiddleware
     * @param RouteObject|null $route
     * @return callable
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     * @throws ReflectionException
     */
    public static function getCallback(string $plugin, string $app, $call, array $args = [], bool $withGlobalMiddleware = true, ?RouteObject $route = null)
    {
        $isController = is_array($call) && is_string($call[0]);
        $middlewares = Middleware::getMiddleware($plugin, $app, $call, $route, $withGlobalMiddleware);

        $container = static::container($plugin) ?? static::container('');
        foreach ($middlewares as $key => $item) {
            $middleware = $item[0];
            if (is_string($middleware)) {
                $middleware = $container->get($middleware);
            } elseif ($middleware instanceof Closure) {
                $middleware = call_user_func($middleware, $container);
            }
            $middlewares[$key][0] = $middleware;
        }

        $needInject = static::isNeedInject($call, $args);
        $anonymousArgs = array_values($args);
        // Pre-compute return type for Response check optimization in middleware chain
        $alwaysReturnsResponse = false;
        if ($middlewares) {
            try {
                $returnType = static::getReflector($call)->getReturnType();
                $alwaysReturnsResponse = $returnType instanceof \ReflectionNamedType
                    && !$returnType->allowsNull()
                    && is_a($returnType->getName(), Response::class, true);
            } catch (Throwable $e) {
            }
        }
        if ($isController) {
            $controllerReuse = static::config($plugin, 'app.controller_reuse', true);
            if (!$controllerReuse) {
                if ($needInject) {
                    // Pre-compute metadata at closure creation time
                    $reflector = static::getReflector($call);
                    $metadataList = static::getMethodParameterMetadata($reflector);
                    $debug = static::config($plugin, 'app.debug');
                    $call = function ($request) use ($call, $plugin, $args, $container, $metadataList, $debug) {
                        $call[0] = $container->make($call[0]);
                        $inputs = $args ? array_merge($request->all(), $args) : $request->all();
                        $resolvedArgs = array_values(static::resolveMethodDependenciesFromMetadata($container, $request, $inputs, $metadataList, $debug));
                        return $call(...$resolvedArgs);
                    };
                    $needInject = false;
                } else {
                    $call = function ($request, ...$anonymousArgs) use ($call, $plugin, $container) {
                        $call[0] = $container->make($call[0]);
                        return $call($request, ...$anonymousArgs);
                    };
                }
            } else {
                $call[0] = $container->get($call[0]);
            }
        }

        if ($needInject) {
            $call = static::resolveInject($container, $call, $args, static::config($plugin, 'app.debug'));
        }

        if ($middlewares) {
            if ($alwaysReturnsResponse) {
                $innermost = function ($request) use ($call, $anonymousArgs) {
                    try {
                        return $call($request, ...$anonymousArgs);
                    } catch (Throwable $e) {
                        return static::exceptionResponse($e, $request);
                    }
                };
            } else {
                $innermost = function ($request) use ($call, $anonymousArgs) {
                    try {
                        $response = $call($request, ...$anonymousArgs);
                    } catch (Throwable $e) {
                        return static::exceptionResponse($e, $request);
                    }
                    if (!$response instanceof Response) {
                        if (!is_string($response)) {
                            $response = static::stringify($response);
                        }
                        $response = new Response(200, [], $response);
                    }
                    return $response;
                };
            }
            $callback = array_reduce($middlewares, function ($carry, $pipe) {
                return function ($request) use ($carry, $pipe) {
                    try {
                        return $pipe($request, $carry);
                    } catch (Throwable $e) {
                        return static::exceptionResponse($e, $request);
                    }
                };
            }, $innermost);
        } else {
            if (!$anonymousArgs) {
                $callback = $call;
            } else {
                $callback = function ($request) use ($call, $anonymousArgs) {
                    return $call($request, ...$anonymousArgs);
                };
            }
        }
        return $callback;
    }

    /**
     * ResolveInject.
     * @param ContainerInterface $container
     * @param array|Closure $call
     * @param array $args
     * @param bool $debug
     * @return Closure
     * @see Dependency injection through reflection information
     */
    protected static function resolveInject(ContainerInterface $container, $call, array $args, bool $debug): Closure
    {
        // Pre-compute metadata at closure creation time (once), not at execution time (every request)
        $metadataList = static::getMethodParameterMetadata(static::getReflector($call));

        return function (Request $request) use ($container, $call, $args, $metadataList, $debug) {
            $inputs = $args ? array_merge($request->all(), $args) : $request->all();
            $resolvedArgs = array_values(static::resolveMethodDependenciesFromMetadata(
                $container, $request, $inputs, $metadataList, $debug
            ));
            return $call(...$resolvedArgs);
        };
    }

    /**
     * Check whether inject is required.
     * @param $call
     * @param array $args
     * @return bool
     * @throws ReflectionException
     */
    protected static function isNeedInject($call, array &$args): bool
    {
        if (is_array($call) && !method_exists($call[0], $call[1])) {
            return false;
        }
        $reflector = static::getReflector($call);
        $reflectionParameters = $reflector->getParameters();
        if (!$reflectionParameters) {
            return false;
        }
        $firstParameter = current($reflectionParameters);
        unset($reflectionParameters[key($reflectionParameters)]);
        $adaptersList = ['int', 'string', 'bool', 'array', 'object', 'float', 'mixed', 'resource'];
        $keys = [];
        $needInject = false;
        foreach ($reflectionParameters as $parameter) {
            $parameterName = $parameter->name;
            $keys[] = $parameterName;
            if ($parameter->hasType()) {
                $type = $parameter->getType();
                if (!$type instanceof \ReflectionNamedType) {
                    throw new \RuntimeException(
                        sprintf('Union/intersection types are not supported for controller parameter $%s. Use a single type instead.', $parameter->name)
                    );
                }
                $typeName = $type->getName();
                if (!in_array($typeName, $adaptersList)) {
                    $needInject = true;
                    continue;
                }
                if (!array_key_exists($parameterName, $args)) {
                    $needInject = true;
                    continue;
                }
                switch ($typeName) {
                    case 'int':
                    case 'float':
                        if (!is_numeric($args[$parameterName])) {
                            return true;
                        }
                        $args[$parameterName] = $typeName === 'int' ? (int)$args[$parameterName]: (float)$args[$parameterName];
                        break;
                    case 'bool':
                        $args[$parameterName] = (bool)$args[$parameterName];
                        break;
                    case 'array':
                    case 'object':
                        if (!is_array($args[$parameterName])) {
                            return true;
                        }
                        $args[$parameterName] = $typeName === 'array' ? $args[$parameterName] : (object)$args[$parameterName];
                        break;
                    case 'string':
                    case 'mixed':
                    case 'resource':
                        break;
                }
            }
        }
        if (array_keys($args) !== $keys) {
            return true;
        }
        if (!$firstParameter->hasType()) {
            return $firstParameter->getName() !== 'request';
        }
        $firstType = $firstParameter->getType();
        if (!$firstType instanceof \ReflectionNamedType) {
            return true;
        }
        if (!is_a(static::$requestClass, $firstType->getName(), true)) {
            return true;
        }

        return $needInject;
    }

    /**
     * Get reflector.
     * @param $call
     * @return ReflectionFunction|ReflectionMethod
     * @throws ReflectionException
     */
    protected static function getReflector($call)
    {
        $cacheKey = static::getReflectorCacheKey($call);
        if ($cacheKey !== null && isset(static::$reflectorCache[$cacheKey])) {
            return static::$reflectorCache[$cacheKey];
        }

        if ($call instanceof Closure || is_string($call)) {
            $reflector = new ReflectionFunction($call);
        } else {
            $reflector = new ReflectionMethod($call[0], $call[1]);
        }

        if ($cacheKey !== null) {
            static::$reflectorCache[$cacheKey] = $reflector;
            if (count(static::$reflectorCache) > 1024) {
                unset(static::$reflectorCache[key(static::$reflectorCache)]);
            }
        }

        return $reflector;
    }

    /**
     * Get reflector cache key.
     * @param mixed $call
     * @return string|null
     */
    protected static function getReflectorCacheKey($call): ?string
    {
        if (is_string($call)) {
            return 'func:' . $call;
        }
        if (is_array($call) && isset($call[0], $call[1])) {
            $class = is_object($call[0]) ? get_class($call[0]) : $call[0];
            return 'method:' . $class . '::' . $call[1];
        }
        // Closures may be short-lived; avoid caching to prevent key reuse risks.
        return null;
    }

    /**
     * Return dependent parameters
     * @param ContainerInterface $container
     * @param Request $request
     * @param array $inputs
     * @param ReflectionFunctionAbstract $reflector
     * @param bool $debug
     * @return array
     * @throws ReflectionException
     */
    protected static function resolveMethodDependencies(ContainerInterface $container, Request $request, array $inputs, ReflectionFunctionAbstract $reflector, bool $debug): array
    {
        $metadataList = static::getMethodParameterMetadata($reflector);
        return static::resolveMethodDependenciesFromMetadata($container, $request, $inputs, $metadataList, $debug);
    }

    /**
     * Return dependent parameters from pre-computed metadata.
     * @param ContainerInterface $container
     * @param Request $request
     * @param array $inputs
     * @param array $metadataList
     * @param bool $debug
     * @return array
     * @throws ReflectionException
     */
    protected static function resolveMethodDependenciesFromMetadata(ContainerInterface $container, Request $request, array $inputs, array $metadataList, bool $debug): array
    {
        $parameters = [];
        foreach ($metadataList as $metadata) {
            $parameterName = $metadata['name'];
            $typeName = $metadata['type'];

            if (!empty($metadata['isRequest'])) {
                $parameters[$parameterName] = $request;
                continue;
            }

            if (!array_key_exists($parameterName, $inputs)) {
                if (!$metadata['hasDefault']) {
                    if (!$typeName || (!$metadata['isClass'] && !$metadata['isEnum']) || $metadata['isEnum']) {
                        throw (new MissingInputException())->data([
                            'parameter' => $parameterName,
                        ])->debug($debug);
                    }
                } else {
                    $parameters[$parameterName] = $metadata['default'];
                    continue;
                }
            }

            $parameterValue = $inputs[$parameterName] ?? null;

            switch ($typeName) {
                case 'int':
                case 'float':
                    if (!is_numeric($parameterValue)) {
                        throw (new InputTypeException())->data([
                            'parameter' => $parameterName,
                            'exceptType' => $typeName,
                            'actualType' => gettype($parameterValue),
                        ])->debug($debug);
                    }
                    $parameters[$parameterName] = $typeName === 'float' ? (float)$parameterValue :  (int)$parameterValue;
                    break;
                case 'bool':
                    $parameters[$parameterName] = (bool)$parameterValue;
                    break;
                case 'array':
                case 'object':
                    if (!is_array($parameterValue)) {
                        throw (new InputTypeException())->data([
                            'parameter' => $parameterName,
                            'exceptType' => $typeName,
                            'actualType' => gettype($parameterValue),
                        ])->debug($debug);
                    }
                    $parameters[$parameterName] = $typeName === 'object' ? (object)$parameterValue : $parameterValue;
                    break;
                case 'string':
                case 'mixed':
                case 'resource':
                case null:
                    $parameters[$parameterName] = $parameterValue;
                    break;
                default:
                    $subInputs = is_array($parameterValue) ? $parameterValue : [];
                    if (!empty($metadata['isModel'])) {
                        $parameters[$parameterName] = $container->make($typeName, [
                            'attributes' => $subInputs
                        ]);
                        break;
                    }
                    if (!empty($metadata['isThinkModel'])) {
                        $parameters[$parameterName] = $container->make($typeName, [
                            'data' => $subInputs
                        ]);
                        break;
                    }
                    if (!empty($metadata['isEnum'])) {
                        // Use pre-computed enum case mappings (avoids per-request ReflectionEnum)
                        if (isset($metadata['enumCases'][$parameterValue])) {
                            $parameters[$parameterName] = $metadata['enumCases'][$parameterValue];
                            break;
                        }
                        if (!empty($metadata['enumIsBacked']) && isset($metadata['enumBackedValues'][$parameterValue])) {
                            $parameters[$parameterName] = $metadata['enumBackedValues'][$parameterValue];
                            break;
                        }
                        throw (new InputValueException())->data([
                            'parameter' => $parameterName,
                            'enum' => $typeName
                        ])->debug($debug);
                    }
                    if (is_array($subInputs) && !empty($metadata['hasConstructor'])) {
                        $constructorReflector = static::getReflector([$typeName, '__construct']);
                        $parameters[$parameterName] = $container->make($typeName, static::resolveMethodDependencies($container, $request, $subInputs, $constructorReflector, $debug));
                    } else {
                        $parameters[$parameterName] = $container->make($typeName);
                    }
                    break;
            }
        }
        return $parameters;
    }

    /**
     * Get method parameter metadata from cache.
     * @param ReflectionFunctionAbstract $reflector
     * @return array<int, array<string, mixed>>
     * @throws ReflectionException
     */
    protected static function getMethodParameterMetadata(ReflectionFunctionAbstract $reflector): array
    {
        $cacheKey = static::getParameterMetadataCacheKey($reflector);
        if ($cacheKey !== null && isset(static::$parameterMetadataCache[$cacheKey])) {
            return static::$parameterMetadataCache[$cacheKey];
        }

        $metadataList = [];
        foreach ($reflector->getParameters() as $parameter) {
            $type = $parameter->getType();
            if ($type !== null && !$type instanceof \ReflectionNamedType) {
                throw new \RuntimeException(
                    sprintf('Union/intersection types are not supported for controller parameter $%s. Use a single type instead.', $parameter->name)
                );
            }
            $typeName = $type?->getName();
            $hasDefault = $parameter->isDefaultValueAvailable();
            $isEnum = $typeName && enum_exists($typeName);
            $isClass = $typeName && class_exists($typeName);
            $isRequest = $typeName && is_a(static::$requestClass, $typeName, true);
            $isModel = $typeName && is_a($typeName, Model::class, true);
            $isThinkModel = $typeName && is_a($typeName, ThinkModel::class, true);
            $metadata = [
                'name' => $parameter->name,
                'type' => $typeName,
                'hasDefault' => $hasDefault,
                'default' => $hasDefault ? $parameter->getDefaultValue() : null,
                'isRequest' => $isRequest,
                'isEnum' => $isEnum,
                'isClass' => $isClass,
                'isModel' => $isModel,
                'isThinkModel' => $isThinkModel,
            ];
            // Pre-compute enum case mappings to avoid per-request ReflectionEnum
            if ($isEnum) {
                $enumReflection = new ReflectionEnum($typeName);
                $enumCases = [];
                $enumBackedValues = [];
                $isBacked = $enumReflection->isBacked();
                foreach ($enumReflection->getCases() as $case) {
                    $caseValue = $case->getValue();
                    $enumCases[$case->getName()] = $caseValue;
                    if ($isBacked) {
                        $enumBackedValues[$caseValue->value] = $caseValue;
                    }
                }
                $metadata['enumCases'] = $enumCases;
                $metadata['enumBackedValues'] = $enumBackedValues;
                $metadata['enumIsBacked'] = $isBacked;
            }
            // Pre-compute class constructor info to avoid per-request ReflectionClass
            if ($isClass && !$isRequest && !$isEnum && !$isModel && !$isThinkModel) {
                $classRef = new ReflectionClass($typeName);
                $constructor = $classRef->getConstructor();
                $metadata['hasConstructor'] = $constructor !== null;
                if ($constructor) {
                    // Pre-cache constructor reflector for use by getReflector()
                    $constructorKey = 'method:' . $typeName . '::__construct';
                    if (!isset(static::$reflectorCache[$constructorKey])) {
                        static::$reflectorCache[$constructorKey] = $constructor;
                    }
                }
            }
            $metadataList[] = $metadata;
        }

        if ($cacheKey !== null) {
            static::$parameterMetadataCache[$cacheKey] = $metadataList;
            if (count(static::$parameterMetadataCache) > 1024) {
                unset(static::$parameterMetadataCache[key(static::$parameterMetadataCache)]);
            }
        }

        return $metadataList;
    }

    /**
     * Get parameter metadata cache key.
     * @param ReflectionFunctionAbstract $reflector
     * @return string|null
     */
    protected static function getParameterMetadataCacheKey(ReflectionFunctionAbstract $reflector): ?string
    {
        if ($reflector instanceof ReflectionMethod) {
            return 'method:' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName();
        }
        if ($reflector instanceof ReflectionFunction && $reflector->isClosure()) {
            return null;
        }
        if ($reflector instanceof ReflectionFunction) {
            return 'func:' . $reflector->getName();
        }
        return null;
    }

    /**
     * Container.
     * @param string $plugin
     * @return ContainerInterface
     */
    public static function container(string $plugin = '')
    {
        return static::config($plugin, 'container');
    }

    /**
     * Get request.
     * @return Request|\support\Request
     */
    public static function request()
    {
        return Context::get(Request::class);
    }

    /**
     * Get worker.
     * @return Worker
     */
    public static function worker(): ?Worker
    {
        return static::$worker;
    }

    /**
     * Find Route.
     * @param TcpConnection $connection
     * @param string $path
     * @param string $key
     * @param $request
     * @param $status
     * @return bool
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     * @throws ReflectionException|Throwable
     */
    protected static function findRoute(TcpConnection $connection, string $path, string $key, $request, &$status): bool
    {
        $routeInfo = Route::dispatch($request->method(), $path);
        if ($routeInfo[0] === Dispatcher::FOUND) {
            $status = 200;
            $routeInfo[0] = 'route';
            $callback = $routeInfo[1]['callback'];
            $route = clone $routeInfo[1]['route'];
            $app = $controller = $action = '';
            $args = !empty($routeInfo[2]) ? $routeInfo[2] : [];
            if ($args) {
                $route->setParams($args);
            }
            $args = array_merge($route->param(), $args);
            if (is_array($callback)) {
                $controller = $callback[0];
                $plugin = static::getPluginByClass($controller);
                $app = static::getAppByController($controller);
                $action = static::getRealMethod($controller, $callback[1]) ?? '';
            } else {
                $plugin = static::getPluginByPath($path);
            }
            $callback = static::getCallback($plugin, $app, $callback, $args, true, $route);
            static::collectCallbacks($key, [$callback, $plugin, $app, $controller ?: '', $action, $route]);
            [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
            static::send($connection, $callback($request), $request);
            return true;
        }
        $status = $routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED ? 405 : 404;
        return false;
    }

    /**
     * Find File.
     * @param TcpConnection $connection
     * @param string $path
     * @param string $key
     * @param $request
     * @return bool
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     * @throws ReflectionException
     */
    protected static function findFile(TcpConnection $connection, string $path, string $key, $request): bool
    {
        if (preg_match('/%[0-9a-f]{2}/i', $path)) {
            $path = urldecode($path);
            if (static::unsafeUri($connection, $path, $request)) {
                return true;
            }
        }

        $pathExplodes = explode('/', trim($path, '/'));
        $plugin = '';
        if (isset($pathExplodes[1]) && $pathExplodes[0] === 'app') {
            $plugin = $pathExplodes[1];
            $publicDir = static::config($plugin, 'app.public_path') ?: BASE_PATH . "/plugin/$pathExplodes[1]/public";
            $path = substr($path, strlen("/app/$pathExplodes[1]/"));
        } else {
            $publicDir = static::$publicPath;
        }
        $file = "$publicDir/$path";
        if (!is_file($file)) {
            return false;
        }

        if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
            if (!static::config($plugin, 'app.support_php_files', false)) {
                return false;
            }
            static::collectCallbacks($key, [function () use ($file) {
                return static::execPhpFile($file);
            }, $plugin, '', '', '', null]);
            [, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
            static::send($connection, static::execPhpFile($file), $request);
            return true;
        }

        if (!static::config($plugin, 'static.enable', false)) {
            return false;
        }

        static::collectCallbacks($key, [static::getCallback($plugin, '__static__', function ($request) use ($file, $plugin) {
            clearstatcache(true, $file);
            if (!is_file($file)) {
                $callback = static::getFallback($plugin);
                return $callback($request);
            }
            return (new Response())->file($file);
        }, [], false), '', '', '', '', null]);
        [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key];
        static::send($connection, $callback($request), $request);
        return true;
    }

    /**
     * Send.
     * @param TcpConnection|mixed $connection
     * @param mixed|Response $response
     * @param Request|mixed $request
     * @return void
     */
    protected static function send($connection, $response, $request)
    {
        Context::destroy();
        // Remove the reference of request to session.
        unset($request->context['session']);
        $keepAlive = $request->header('connection');
        if ($keepAlive === null) {
            if ($request->protocolVersion() === '1.1') {
                $connection->send($response);
                return;
            }
        } elseif (\strcasecmp($keepAlive, 'keep-alive') === 0) {
            $connection->send($response);
            return;
        }
        if ($response instanceof Response && $response->getHeader('Transfer-Encoding') === 'chunked') {
            $connection->send($response);
            return;
        }
        $connection->close($response);
    }

    /**
     * ParseControllerAction.
     * @param string $path
     * @return array|false
     * @throws ReflectionException
     */
    protected static function parseControllerAction(string $path)
    {
        $path = str_replace(['-', '//'], ['', '/'], $path);
        if (static::containsPathTraversal($path)) {
            return false;
        }
        static $cache = [];
        if (isset($cache[$path])) {
            return $cache[$path];
        }
        $pathExplode = explode('/', trim($path, '/'));
        $isPlugin = isset($pathExplode[1]) && $pathExplode[0] === 'app';
        $configPrefix = $isPlugin ? "plugin.$pathExplode[1]." : '';
        $pathPrefix = $isPlugin ? "/app/$pathExplode[1]" : '';
        $classPrefix = $isPlugin ? "plugin\\$pathExplode[1]" : '';
        $suffix = Config::get("{$configPrefix}app.controller_suffix", '');
        $relativePath = trim(substr($path, strlen($pathPrefix)), '/');
        $pathExplode = $relativePath ? explode('/', $relativePath) : [];

        $action = 'index';
        if (!$controllerAction = static::guessControllerAction($pathExplode, $action, $suffix, $classPrefix)) {
            if (count($pathExplode) <= 1) {
                return false;
            }
            $action = end($pathExplode);
            unset($pathExplode[count($pathExplode) - 1]);
            $controllerAction = static::guessControllerAction($pathExplode, $action, $suffix, $classPrefix);
        }
        if ($controllerAction && !isset($path[256])) {
            $cache[$path] = $controllerAction;
            if (count($cache) > 1024) {
                unset($cache[key($cache)]);
            }
        }
        return $controllerAction;
    }

    /**
     * GuessControllerAction.
     * @param $pathExplode
     * @param $action
     * @param $suffix
     * @param $classPrefix
     * @return array|false
     * @throws ReflectionException
     */
    protected static function guessControllerAction($pathExplode, $action, $suffix, $classPrefix)
    {
        $map[] = trim("$classPrefix\\app\\controller\\" . implode('\\', $pathExplode), '\\');
        foreach ($pathExplode as $index => $section) {
            $tmp = $pathExplode;
            array_splice($tmp, $index, 1, [$section, 'controller']);
            $map[] = trim("$classPrefix\\" . implode('\\', array_merge(['app'], $tmp)), '\\');
        }
        foreach ($map as $item) {
            $map[] = $item . '\\index';
        }
        foreach ($map as $controllerClass) {
            // Remove xx\xx\controller
            if (substr($controllerClass, -11) === '\\controller') {
                continue;
            }
            $controllerClass .= $suffix;
            if ($controllerAction = static::getControllerAction($controllerClass, $action)) {
                return $controllerAction;
            }
        }
        return false;
    }

    /**
     * GetControllerAction.
     * @param string $controllerClass
     * @param string $action
     * @return array|false
     * @throws ReflectionException
     */
    protected static function getControllerAction(string $controllerClass, string $action)
    {
        // Disable calling magic methods
        if (strpos($action, '__') === 0) {
            return false;
        }
        if (($controllerClass = static::getController($controllerClass)) && ($action = static::getAction($controllerClass, $action))) {
            return [
                'plugin' => static::getPluginByClass($controllerClass),
                'app' => static::getAppByController($controllerClass),
                'controller' => $controllerClass,
                'action' => $action
            ];
        }
        return false;
    }

    /**
     * GetController.
     * @param string $controllerClass
     * @return string|false
     * @throws ReflectionException
     */
    protected static function getController(string $controllerClass)
    {
        if (class_exists($controllerClass)) {
            return (new ReflectionClass($controllerClass))->name;
        }
        $explodes = explode('\\', strtolower(ltrim($controllerClass, '\\')));
        $basePath = $explodes[0] === 'plugin' ? BASE_PATH . '/plugin' : static::$appPath;
        unset($explodes[0]);
        $fileName = array_pop($explodes) . '.php';
        $found = true;
        foreach ($explodes as $pathSection) {
            if (!$found) {
                break;
            }
            $dirs = Util::scanDir($basePath, false);
            $found = false;
            foreach ($dirs as $name) {
                $path = "$basePath/$name";

                if (is_dir($path) && strtolower($name) === $pathSection) {
                    $basePath = $path;
                    $found = true;
                    break;
                }
            }
        }
        if (!$found) {
            return false;
        }
        foreach (scandir($basePath) ?: [] as $name) {
            if (strtolower($name) === $fileName) {
                require_once "$basePath/$name";
                if (class_exists($controllerClass, false)) {
                    return (new ReflectionClass($controllerClass))->name;
                }
            }
        }
        return false;
    }

    /**
     * GetAction.
     * @param string $controllerClass
     * @param string $action
     * @return string|false
     */
    protected static function getAction(string $controllerClass, string $action)
    {
        $methods = get_class_methods($controllerClass);
        $lowerAction = strtolower($action);
        $found = false;
        foreach ($methods as $candidate) {
            if (strtolower($candidate) === $lowerAction) {
                $action = $candidate;
                $found = true;
                break;
            }
        }
        if ($found) {
            return $action;
        }
        // Action is not public method
        if (method_exists($controllerClass, $action)) {
            return false;
        }
        if (method_exists($controllerClass, '__call')) {
            return $action;
        }
        return false;
    }

    /**
     * GetPluginByClass.
     * @param string $controllerClass
     * @return mixed|string
     */
    public static function getPluginByClass(string $controllerClass)
    {
        $controllerClass = trim($controllerClass, '\\');
        $tmp = explode('\\', $controllerClass, 3);
        if ($tmp[0] !== 'plugin') {
            return '';
        }
        return $tmp[1] ?? '';
    }

    /**
     * GetPluginByPath.
     * @param string $path
     * @return mixed|string
     */
    public static function getPluginByPath(string $path)
    {
        $path = trim($path, '/');
        $tmp = explode('/', $path, 3);
        if ($tmp[0] !== 'app') {
            return '';
        }
        $plugin = $tmp[1] ?? '';
        if ($plugin && !static::config('', "plugin.$plugin.app")) {
            return '';
        }
        return $plugin;
    }

    /**
     * GetAppByController.
     * @param string $controllerClass
     * @return mixed|string
     */
    protected static function getAppByController(string $controllerClass)
    {
        $controllerClass = trim($controllerClass, '\\');
        $tmp = explode('\\', $controllerClass, 5);
        $pos = $tmp[0] === 'plugin' ? 3 : 1;
        if (!isset($tmp[$pos])) {
            return '';
        }
        return strtolower($tmp[$pos]) === 'controller' ? '' : $tmp[$pos];
    }

    /**
     * ExecPhpFile.
     * @param string $file
     * @return false|string
     */
    public static function execPhpFile(string $file)
    {
        ob_start();
        // Try to include php file.
        try {
            include $file;
        } catch (Throwable $e) {
            ob_get_clean();
            throw $e;
        }
        return ob_get_clean();
    }

    /**
     * GetRealMethod.
     * @param string $class
     * @param string $method
     * @return string
     */
    protected static function getRealMethod(string $class, string $method): string
    {
        $method = strtolower($method);
        $methods = get_class_methods($class);
        foreach ($methods as $candidate) {
            if (strtolower($candidate) === $method) {
                return $candidate;
            }
        }
        return $method;
    }

    /**
     * Config.
     * @param string $plugin
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    protected static function config(string $plugin, string $key, mixed $default = null)
    {
        return Config::get($plugin ? "plugin.$plugin.$key" : $key, $default);
    }


    /**
     * @param mixed $data
     * @return string
     */
    protected static function stringify($data): string
    {
        $type = gettype($data);
        switch ($type) {
            case 'boolean':
                return  $data ? 'true' : 'false';
            case 'NULL':
                return 'NULL';
            case 'array':
                return 'Array';
            case 'object':
                if (!method_exists($data, '__toString')) {
                    return 'Object';
                }
            default:
                return (string)$data;
        }
    }
}


================================================
FILE: src/Bootstrap.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman;

use Workerman\Worker;

interface Bootstrap
{
    /**
     * onWorkerStart
     *
     * @param Worker|null $worker
     * @return mixed
     */
    public static function start(?Worker $worker);
}


================================================
FILE: src/Config.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman;

use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use function array_replace_recursive;
use function array_reverse;
use function count;
use function explode;
use function in_array;
use function is_array;
use function is_dir;
use function is_file;
use function key;
use function str_replace;

class Config
{

    /**
     * @var array
     */
    protected static $config = [];

    /**
     * @var string
     */
    protected static $configPath = '';

    /**
     * @var bool
     */
    protected static $loaded = false;

    /**
     * @var array Flat cache for repeated get() lookups.
     */
    protected static $flatCache = [];

    /**
     * Load.
     * @param string $configPath
     * @param array $excludeFile
     * @param string|null $key
     * @return void
     */
    public static function load(string $configPath, array $excludeFile = [], ?string $key = null)
    {
        static::$configPath = $configPath;
        static::$flatCache = [];
        if (!$configPath) {
            return;
        }
        static::$loaded = false;
        $config = static::loadFromDir($configPath, $excludeFile);
        if (!$config) {
            static::$loaded = true;
            return;
        }
        if ($key !== null) {
            foreach (array_reverse(explode('.', $key)) as $k) {
                $config = [$k => $config];
            }
        }
        static::$config = array_replace_recursive(static::$config, $config);
        static::formatConfig();
        static::$loaded = true;
    }

    /**
     * This deprecated method will certainly be removed in the future.
     * @param string $configPath
     * @param array $excludeFile
     * @return void
     * @deprecated
     */
    public static function reload(string $configPath, array $excludeFile = [])
    {
        static::load($configPath, $excludeFile);
    }

    /**
     * Clear.
     * @return void
     */
    public static function clear()
    {
        static::$config = [];
        static::$flatCache = [];
    }

    /**
     * FormatConfig.
     * @return void
     */
    protected static function formatConfig()
    {
        $config = static::$config;
        // Merge log config
        foreach ($config['plugin'] ?? [] as $firm => $projects) {
            if (isset($projects['app'])) {
                foreach ($projects['log'] ?? [] as $key => $item) {
                    $config['log']["plugin.$firm.$key"] = $item;
                }
            }
            foreach ($projects as $name => $project) {
                if (!is_array($project)) {
                    continue;
                }
                foreach ($project['log'] ?? [] as $key => $item) {
                    $config['log']["plugin.$firm.$name.$key"] = $item;
                }
            }
        }
        // Merge database config
        foreach ($config['plugin'] ?? [] as $firm => $projects) {
            if (isset($projects['app'])) {
                foreach ($projects['database']['connections'] ?? [] as $key => $connection) {
                    $config['database']['connections']["plugin.$firm.$key"] = $connection;
                }
            }
            foreach ($projects as $name => $project) {
                if (!is_array($project)) {
                    continue;
                }
                foreach ($project['database']['connections'] ?? [] as $key => $connection) {
                    $config['database']['connections']["plugin.$firm.$name.$key"] = $connection;
                }
            }
        }
        if (!empty($config['database']['connections'])) {
            $config['database']['default'] = $config['database']['default'] ?? key($config['database']['connections']);
        }
        // Merge thinkorm config
        foreach ($config['plugin'] ?? [] as $firm => $projects) {
            if (isset($projects['app'])) {
                foreach ($projects['thinkorm']['connections'] ?? [] as $key => $connection) {
                    $config['thinkorm']['connections']["plugin.$firm.$key"] = $connection;
                }
                foreach ($projects['think-orm']['connections'] ?? [] as $key => $connection) {
                    $config['think-orm']['connections']["plugin.$firm.$key"] = $connection;
                }
            }
            foreach ($projects as $name => $project) {
                if (!is_array($project)) {
                    continue;
                }
                foreach ($project['thinkorm']['connections'] ?? [] as $key => $connection) {
                    $config['thinkorm']['connections']["plugin.$firm.$name.$key"] = $connection;
                }
                foreach ($project['think-orm']['connections'] ?? [] as $key => $connection) {
                    $config['think-orm']['connections']["plugin.$firm.$name.$key"] = $connection;
                }
            }
        }
        if (!empty($config['thinkorm']['connections'])) {
            $config['thinkorm']['default'] = $config['thinkorm']['default'] ?? key($config['thinkorm']['connections']);
        }
        if (!empty($config['think-orm']['connections'])) {
            $config['think-orm']['default'] = $config['think-orm']['default'] ?? key($config['think-orm']['connections']);
        }
        // Merge redis config
        foreach ($config['plugin'] ?? [] as $firm => $projects) {
            if (isset($projects['app'])) {
                foreach ($projects['redis'] ?? [] as $key => $connection) {
                    $config['redis']["plugin.$firm.$key"] = $connection;
                }
            }
            foreach ($projects as $name => $project) {
                if (!is_array($project)) {
                    continue;
                }
                foreach ($project['redis'] ?? [] as $key => $connection) {
                    $config['redis']["plugin.$firm.$name.$key"] = $connection;
                }
            }
        }
        static::$config = $config;
    }

    /**
     * LoadFromDir.
     * @param string $configPath
     * @param array $excludeFile
     * @return array
     */
    public static function loadFromDir(string $configPath, array $excludeFile = []): array
    {
        $allConfig = [];
        $dirIterator = new RecursiveDirectoryIterator($configPath, FilesystemIterator::FOLLOW_SYMLINKS);
        $iterator = new RecursiveIteratorIterator($dirIterator);
        foreach ($iterator as $file) {
            /** var SplFileInfo $file */
            if (is_dir($file) || $file->getExtension() != 'php' || in_array($file->getBaseName('.php'), $excludeFile)) {
                continue;
            }
            $appConfigFile = $file->getPath() . '/app.php';
            if (!is_file($appConfigFile)) {
                continue;
            }
            $relativePath = str_replace($configPath . DIRECTORY_SEPARATOR, '', substr($file, 0, -4));
            $explode = array_reverse(explode(DIRECTORY_SEPARATOR, $relativePath));
            if (count($explode) >= 2) {
                $appConfig = include $appConfigFile;
                if (empty($appConfig['enable'])) {
                    continue;
                }
            }
            $config = include $file;
            foreach ($explode as $section) {
                $tmp = [];
                $tmp[$section] = $config;
                $config = $tmp;
            }
            $allConfig = array_replace_recursive($allConfig, $config);
        }
        return $allConfig;
    }

    /**
     * Get.
     * @param string|null $key
     * @param mixed $default
     * @return mixed
     */
    public static function get(?string $key = null, mixed $default = null)
    {
        if ($key === null) {
            return static::$config;
        }
        if (isset(static::$flatCache[$key])) {
            return static::$flatCache[$key];
        }
        $keyArray = explode('.', $key);
        $value = static::$config;
        $found = true;
        foreach ($keyArray as $index) {
            if (!isset($value[$index])) {
                if (static::$loaded) {
                    return $default;
                }
                $found = false;
                break;
            }
            $value = $value[$index];
        }
        if ($found) {
            if (static::$loaded) {
                static::$flatCache[$key] = $value;
                if (count(static::$flatCache) > 1024) {
                    unset(static::$flatCache[key(static::$flatCache)]);
                }
            }
            return $value;
        }
        return static::read($key, $default);
    }

    /**
     * Read.
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    protected static function read(string $key, mixed $default = null)
    {
        $path = static::$configPath;
        if ($path === '') {
            return $default;
        }
        $keys = $keyArray = explode('.', $key);
        foreach ($keyArray as $index => $section) {
            unset($keys[$index]);
            if (is_file($file = "$path/$section.php")) {
                $config = include $file;
                return static::find($keys, $config, $default);
            }
            if (!is_dir($path = "$path/$section")) {
                return $default;
            }
        }
        return $default;
    }

    /**
     * Find.
     * @param array $keyArray
     * @param mixed $stack
     * @param mixed $default
     * @return array|mixed
     */
    protected static function find(array $keyArray, $stack, $default)
    {
        if (!is_array($stack)) {
            return $default;
        }
        $value = $stack;
        foreach ($keyArray as $index) {
            if (!isset($value[$index])) {
                return $default;
            }
            $value = $value[$index];
        }
        return $value;
    }

}


================================================
FILE: src/Container.php
================================================
<?php

namespace Webman;

use Psr\Container\ContainerInterface;
use Webman\Exception\NotFoundException;
use function array_key_exists;
use function class_exists;

/**
 * Class Container
 * @package Webman
 */
class Container implements ContainerInterface
{

    /**
     * @var array
     */
    protected $instances = [];
    /**
     * @var array
     */
    protected $definitions = [];

    /**
     * Get.
     * @param string $name
     * @return mixed
     * @throws NotFoundException
     */
    public function get(string $name)
    {
        if (!isset($this->instances[$name])) {
            if (isset($this->definitions[$name])) {
                $this->instances[$name] = call_user_func($this->definitions[$name], $this);
            } else {
                if (!class_exists($name)) {
                    throw new NotFoundException("Class '$name' not found");
                }
                $this->instances[$name] = new $name();
            }
        }
        return $this->instances[$name];
    }

    /**
     * Has.
     * @param string $name
     * @return bool
     */
    public function has(string $name): bool
    {
        return array_key_exists($name, $this->instances)
            || array_key_exists($name, $this->definitions);
    }

    /**
     * Make.
     * @param string $name
     * @param array $constructor
     * @return mixed
     * @throws NotFoundException
     */
    public function make(string $name, array $constructor = [])
    {
        if (!class_exists($name)) {
            throw new NotFoundException("Class '$name' not found");
        }
        return new $name(... array_values($constructor));
    }

    /**
     * AddDefinitions.
     * @param array $definitions
     * @return $this
     */
    public function addDefinitions(array $definitions): Container
    {
        $this->definitions = array_merge($this->definitions, $definitions);
        return $this;
    }

}


================================================
FILE: src/Context.php
================================================
<?php

namespace Webman;

use Workerman\Coroutine\Context as WorkermanContext;
use Workerman\Coroutine\Utils\DestructionWatcher;
use Closure;

/**
 * Class Context
 * @package Webman
 */
class Context extends WorkermanContext
{
    public static function onDestroy(Closure $closure): void
    {
        $obj = static::get('context.onDestroy');
        if (!$obj) {
            $obj = new \stdClass();
            static::set('context.onDestroy', $obj);
        }
        DestructionWatcher::watch($obj, $closure);
    }
}

================================================
FILE: src/Exception/BusinessException.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Exception;

use RuntimeException;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
use function json_encode;

/**
 * Class BusinessException
 * @package support\exception
 */
class BusinessException extends RuntimeException
{

    /**
     * @var array
     */
    protected $data = [];

    /**
     * @var bool
     */
    protected $debug = false;

    /**
     * Render an exception into an HTTP response.
     * @param Request $request
     * @return Response|null
     */
    public function render(Request $request): ?Response
    {
        if ($request->expectsJson()) {
            $code = $this->getCode();
            $json = ['code' => $code ?: 500, 'msg' => $this->getMessage(), 'data' => $this->data];
            return new Response(200, ['Content-Type' => 'application/json'],
                json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        return new Response(200, [], $this->getMessage());
    }

    /**
     * Set data.
     * @param array|null $data
     * @return array|$this
     */
    public function data(?array $data = null): array|static
    {
        if ($data === null) {
            return $this->data;
        }
        $this->data = $data;
        return $this;
    }

    /**
     * Set debug.
     * @param bool|null $value
     * @return $this|bool
     */
    public function debug(?bool $value = null): bool|static
    {
        if ($value === null) {
            return $this->debug;
        }
        $this->debug = $value;
        return $this;
    }

    /**
     * Get data.
     * @return array
     */
    public function getData(): array
    {
        return $this->data;
    }

    /**
     * Translate message.
     * @param string $message
     * @param array $parameters
     * @param string|null $domain
     * @param string|null $locale
     * @return string
     */
    protected function trans(string $message, array $parameters = [], ?string $domain = null, ?string $locale = null): string
    {
        $args = [];
        foreach ($parameters as $key => $parameter) {
            $args[":$key"] = $parameter;
        }
        try {
            $message = trans($message, $args, $domain, $locale);
        } catch (Throwable $e) {
        }
        foreach ($parameters as $key => $value) {
            $message = str_replace(":$key", $value, $message);
        }
        return $message;
    }

}


================================================
FILE: src/Exception/ExceptionHandler.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Exception;

use Psr\Log\LoggerInterface;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
use function json_encode;
use function nl2br;
use function trim;

/**
 * Class Handler
 * @package support\exception
 */
class ExceptionHandler implements ExceptionHandlerInterface
{
    /**
     * @var LoggerInterface
     */
    protected $logger = null;

    /**
     * @var bool
     */
    protected $debug = false;

    /**
     * @var array
     */
    public $dontReport = [];

    /**
     * ExceptionHandler constructor.
     * @param $logger
     * @param $debug
     */
    public function __construct($logger, $debug)
    {
        $this->logger = $logger;
        $this->debug = $debug;
    }

    /**
     * @param Throwable $exception
     * @return void
     */
    public function report(Throwable $exception)
    {
        if ($this->shouldntReport($exception)) {
            return;
        }
        $logs = '';
        if ($request = \request()) {
            $logs = $request->getRealIp() . ' ' . $request->method() . ' ' . trim($request->fullUrl(), '/');
        }
        $this->logger->error($logs . PHP_EOL . $exception);
    }

    /**
     * @param Request $request
     * @param Throwable $exception
     * @return Response
     */
    public function render(Request $request, Throwable $exception): Response
    {
        if (method_exists($exception, 'render') && ($response = $exception->render($request))) {
            return $response;
        }
        $code = $exception->getCode();
        if ($request->expectsJson()) {
            $json = ['code' => $code ?: 500, 'msg' => $this->debug ? $exception->getMessage() : 'Server internal error'];
            $this->debug && $json['traces'] = (string)$exception;
            return new Response(200, ['Content-Type' => 'application/json'],
                json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        $error = $this->debug ? nl2br((string)$exception) : 'Server internal error';
        return new Response(500, [], $error);
    }

    /**
     * @param Throwable $e
     * @return bool
     */
    protected function shouldntReport(Throwable $e): bool
    {
        foreach ($this->dontReport as $type) {
            if ($e instanceof $type) {
                return true;
            }
        }
        return false;
    }

    /**
     * Compatible $this->_debug
     *
     * @param string $name
     * @return bool|null
     */
    public function __get(string $name)
    {
        if ($name === '_debug') {
            return $this->debug;
        }
        return null;
    }
}


================================================
FILE: src/Exception/ExceptionHandlerInterface.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Exception;

use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;

interface ExceptionHandlerInterface
{
    /**
     * @param Throwable $exception
     * @return mixed
     */
    public function report(Throwable $exception);

    /**
     * @param Request $request
     * @param Throwable $exception
     * @return Response
     */
    public function render(Request $request, Throwable $exception): Response;
}

================================================
FILE: src/Exception/FileException.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Exception;

use RuntimeException;

/**
 * Class FileException
 * @package Webman\Exception
 */
class FileException extends RuntimeException
{
}

================================================
FILE: src/Exception/NotFoundException.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Exception;

use Psr\Container\NotFoundExceptionInterface;

/**
 * Class NotFoundException
 * @package Webman\Exception
 */
class NotFoundException extends \Exception implements NotFoundExceptionInterface
{
}


================================================
FILE: src/File.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman;

use SplFileInfo;
use Webman\Exception\FileException;
use function chmod;
use function is_dir;
use function mkdir;
use function pathinfo;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function strip_tags;
use function umask;

class File extends SplFileInfo
{

    /**
     * Move.
     * @param string $destination
     * @return File
     */
    public function move(string $destination): File
    {
        set_error_handler(function ($type, $msg) use (&$error) {
            $error = $msg;
        });
        $path = pathinfo($destination, PATHINFO_DIRNAME);
        if (!is_dir($path) && !mkdir($path, 0777, true)) {
            restore_error_handler();
            throw new FileException(sprintf('Unable to create the "%s" directory (%s)', $path, strip_tags($error)));
        }
        if (!rename($this->getPathname(), $destination)) {
            restore_error_handler();
            throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $destination, strip_tags($error)));
        }
        restore_error_handler();
        @chmod($destination, 0666 & ~umask());
        return new self($destination);
    }

}

================================================
FILE: src/Finder/ControllerFinder.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Finder;

use InvalidArgumentException;
use Webman\Config;

use function is_array;
use function is_dir;
use function preg_match;
use function preg_quote;
use function scandir;
use function str_starts_with;

/**
 * ControllerFinder
 *
 * Discover controller files in main app and/or plugins.
 *
 * Scope examples:
 * - null        : main app only
 * - '*'         : main app + all enabled plugins
 * - 'plugin.*'  : all enabled plugins
 * - 'plugin.xxx': single plugin (strict: throws when plugin directory/config missing)
 */
class ControllerFinder
{
    /**
     * Find controller files by scope.
     *
     * @param string|null $scope
     * @return FileInfo[]
     */
    public static function files(?string $scope = null): array
    {
        $roots = static::resolveRoots($scope);
        if (!$roots) {
            return [];
        }

        $resultsByPath = [];
        foreach ($roots as $root) {
            $dir = $root['dir'];
            $suffix = $root['suffix'] ?? '';
            $controllerFiles = static::findControllerFiles($dir, $suffix);
            foreach ($controllerFiles as $file) {
                $resultsByPath[$file->getPathname()] = $file;
            }
        }

        return array_values($resultsByPath);
    }

    /**
     * Resolve search roots by scope.
     *
     * @param string|null $scope
     * @return array<int, array{dir: string, suffix: string}>
     */
    protected static function resolveRoots(?string $scope): array
    {
        if ($scope === null) {
            return static::mainAppRoots();
        }

        if ($scope === '*') {
            return array_merge(static::mainAppRoots(), static::allPluginRoots());
        }

        if ($scope === 'plugin.*') {
            return static::allPluginRoots();
        }

        if (str_starts_with($scope, 'plugin.')) {
            $plugin = substr($scope, strlen('plugin.'));
            if ($plugin === '' || $plugin === '*') {
                throw new InvalidArgumentException("Invalid controller scope: $scope");
            }
            return static::singlePluginRoots($plugin);
        }

        throw new InvalidArgumentException("Invalid controller scope: $scope");
    }

    /**
     * Main app roots.
     *
     * @return array<int, array{dir: string, suffix: string}>
     */
    protected static function mainAppRoots(): array
    {
        $roots = [];
        $appRoot = app_path();
        if (is_dir($appRoot)) {
            $roots[] = [
                'dir' => $appRoot,
                'suffix' => (string)Config::get('app.controller_suffix', ''),
            ];
        }
        return $roots;
    }

    /**
     * Roots for all enabled plugins.
     *
     * Rule (A): if plugin app config is missing/empty, skip it silently.
     *
     * @return array<int, array{dir: string, suffix: string}>
     */
    protected static function allPluginRoots(): array
    {
        $roots = [];
        $pluginBase = base_path('plugin');
        if (!is_dir($pluginBase)) {
            return [];
        }

        foreach (scandir($pluginBase) ?: [] as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }
            if (!static::isValidIdentifier($entry)) {
                continue;
            }

            $pluginDir = $pluginBase . DIRECTORY_SEPARATOR . $entry;
            if (!is_dir($pluginDir)) {
                continue;
            }

            // Only load enabled plugins (same semantics as Route::loadAnnotationRoutes()).
            $pluginAppConfig = Config::get("plugin.$entry.app");
            if (!$pluginAppConfig) {
                continue;
            }

            $pluginAppDir = $pluginDir . DIRECTORY_SEPARATOR . 'app';
            if (!is_dir($pluginAppDir)) {
                continue;
            }

            $roots[] = [
                'dir' => $pluginAppDir,
                'suffix' => is_array($pluginAppConfig)
                    ? (string)($pluginAppConfig['controller_suffix'] ?? '')
                    : (string)Config::get("plugin.$entry.app.controller_suffix", ''),
            ];
        }

        return $roots;
    }

    /**
     * Roots for a single plugin (strict).
     *
     * @param string $plugin
     * @return array<int, array{dir: string, suffix: string}>
     */
    protected static function singlePluginRoots(string $plugin): array
    {
        if (!static::isValidIdentifier($plugin)) {
            throw new InvalidArgumentException("Invalid plugin identifier: $plugin");
        }

        $pluginBase = base_path('plugin');
        $pluginDir = $pluginBase . DIRECTORY_SEPARATOR . $plugin;
        if (!is_dir($pluginDir)) {
            throw new InvalidArgumentException("Plugin directory not found: $plugin");
        }

        $pluginAppConfig = Config::get("plugin.$plugin.app");
        if (!$pluginAppConfig) {
            throw new InvalidArgumentException("Plugin app config not found or empty: plugin.$plugin.app");
        }

        $pluginAppDir = $pluginDir . DIRECTORY_SEPARATOR . 'app';
        if (!is_dir($pluginAppDir)) {
            throw new InvalidArgumentException("Plugin app directory not found: plugin/$plugin/app");
        }

        return [[
            'dir' => $pluginAppDir,
            'suffix' => is_array($pluginAppConfig)
                ? (string)($pluginAppConfig['controller_suffix'] ?? '')
                : (string)Config::get("plugin.$plugin.app.controller_suffix", ''),
        ]];
    }

    /**
     * Find controller files.
     *
     * @param string $rootDir
     * @param string $controllerSuffix
     * @return FileInfo[]
     */
    protected static function findControllerFiles(string $rootDir, string $controllerSuffix = ''): array
    {
        $controllerPathRegex = $controllerSuffix !== ''
            ? ('/(^|[\/\\\\])controller[\/\\\\].*' . preg_quote($controllerSuffix, '/') . '\.php$/i')
            : '/(^|[\/\\\\])controller[\/\\\\].+\.php$/i';

        $finder = Finder::in($rootDir)
            ->files()
            ->path($controllerPathRegex)
            ->hasAttributes(true)
            ->typeIn(['class'])
            ->psr4(true);

        return $finder->find();
    }

    /**
     * Is valid identifier (plugin name).
     *
     * @param string $name
     * @return bool
     */
    protected static function isValidIdentifier(string $name): bool
    {
        return $name !== '' && (bool)preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name);
    }
}



================================================
FILE: src/Finder/FileInfo.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Finder;

use Webman\File;

/**
 * Class FileInfo
 * @package Webman\Finder
 */
class FileInfo extends File
{
    /**
     * @var array PHP file meta info (hasAttributes, type, class, psr4, etc.)
     */
    protected array $meta = [];

    /**
     * @var string Root directory this file belongs to
     */
    protected string $rootDir = '';

    // Root namespace is intentionally not stored.
    /**
     * Constructor.
     * @param string $path
     * @param array $meta
     * @param string $rootDir
     */
    public function __construct(string $path, array $meta = [], string $rootDir = '')
    {
        parent::__construct($path);
        $this->meta = $meta;
        $this->rootDir = $rootDir;
    }

    /**
     * Get PHP file meta info.
     * @return array
     */
    public function meta(): array
    {
        return $this->meta;
    }

    /**
     * Get declared class (FQCN) from meta.
     * Example: "App\\Controller\\IndexController"
     *
     * @return string|null
     */
    public function class(): ?string
    {
        $class = $this->meta['class'] ?? null;
        if (!is_string($class) || $class === '') {
            return null;
        }
        return ltrim($class, '\\');
    }

    /**
     * Get declared short class name (without namespace).
     * Example: "IndexController"
     *
     * @return string|null
     */
    public function className(): ?string
    {
        $fqcn = $this->class();
        if ($fqcn === null) {
            return null;
        }
        $pos = strrpos($fqcn, '\\');
        return $pos === false ? $fqcn : substr($fqcn, $pos + 1);
    }

    /**
     * Get declared namespace.
     * Example: "App\\Controller"
     *
     * @return string|null
     */
    public function namespace(): ?string
    {
        $fqcn = $this->class();
        if ($fqcn === null) {
            return null;
        }
        $pos = strrpos($fqcn, '\\');
        return $pos === false ? null : substr($fqcn, 0, $pos);
    }

    /**
     * Set meta info.
     * @param array $meta
     * @return $this
     */
    public function setMeta(array $meta): static
    {
        $this->meta = $meta;
        return $this;
    }

    /**
     * Get root directory.
     * @return string
     */
    public function rootDir(): string
    {
        return $this->rootDir;
    }

    /**
     * Get relative pathname from root directory.
     * @return string
     */
    public function relativePathname(): string
    {
        if ($this->rootDir === '') {
            return $this->getPathname();
        }
        $rootDir = rtrim(str_replace('\\', '/', $this->rootDir), '/');
        $pathname = str_replace('\\', '/', $this->getPathname());
        $rootLen = strlen($rootDir);
        if (strncasecmp($pathname, $rootDir, $rootLen) === 0) {
            return ltrim(substr($pathname, $rootLen), '/');
        }
        return $this->getPathname();
    }
}


================================================
FILE: src/Finder/Finder.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Finder;

use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RecursiveCallbackFilterIterator;

/**
 * Class Finder
 * File finder with PHP file caching support.
 * @package Webman\Finder
 */
class Finder
{
    /**
     * @var string[] Root directories
     */
    protected array $roots = [];

    /**
     * @var bool Only match files (not directories)
     */
    protected bool $onlyFiles = false;

    /**
     * @var array File name patterns (glob or regex)
     */
    protected array $names = [];

    /**
     * @var array Path patterns (regex)
     */
    protected array $paths = [];

    /**
     * @var array Directories to exclude
     */
    protected array $excludeDirs = ['vendor', 'runtime', '.git', 'storage', 'tests', 'node_modules'];

    /**
     * @var bool Enable PHP meta analysis
     */
    protected bool $phpMetaEnabled = false;

    /**
     * @var bool|null Filter by hasAttributes
     */
    protected ?bool $filterHasAttributes = null;

    /**
     * @var array|null Filter by type
     */
    protected ?array $filterTypes = null;

    /**
     * @var bool|null Filter by psr4
     */
    protected ?bool $filterPsr4 = null;

    /**
     * @var array<string, array> PHP file cache: [path => meta, ...]
     */
    protected static array $phpCache = [];

    /**
     * @var array<string, bool> Cache dirty flags by root
     */
    protected static array $cacheDirty = [];

    /**
     * @var string Cache directory
     */
    protected static string $cacheDir = '';

    /**
     * Create a new Finder instance with root directories.
     * @param string|array $dirs
     * @return static
     */
    public static function in(string|array $dirs): static
    {
        $instance = new static();
        foreach ((array)$dirs as $dir) {
            if (is_dir($dir)) {
                $instance->roots[] = static::normalizePath($dir);
            }
        }
        return $instance;
    }

    /**
     * Create a new Finder instance.
     * Use in() to set search directories.
     * @return static
     */
    public static function create(): static
    {
        return new static();
    }

    /**
     * Set cache directory.
     * @param string $dir
     * @return void
     */
    public static function setCacheDir(string $dir): void
    {
        static::$cacheDir = rtrim($dir, '/\\');
    }

    /**
     * Get cache directory.
     * @return string
     */
    protected static function getCacheDir(): string
    {
        if (static::$cacheDir === '') {
            static::$cacheDir = rtrim(runtime_path('cache/framework/finder'), '/\\');
            if (!is_dir(static::$cacheDir)) {
                @mkdir(static::$cacheDir, 0755, true);
            }
        }
        return static::$cacheDir;
    }

    /**
     * Only match files.
     * @return $this
     */
    public function files(): static
    {
        $this->onlyFiles = true;
        return $this;
    }

    /**
     * Filter by file name pattern (glob or regex).
     * @param string|array $patterns
     * @return $this
     */
    public function name(string|array $patterns): static
    {
        $this->names = array_merge($this->names, (array)$patterns);
        return $this;
    }

    /**
     * Filter by path pattern (regex).
     * @param string|array $patterns
     * @return $this
     */
    public function path(string|array $patterns): static
    {
        $this->paths = array_merge($this->paths, (array)$patterns);
        return $this;
    }

    /**
     * Exclude directories.
     * @param string|array $dirs
     * @return $this
     */
    public function exclude(string|array $dirs): static
    {
        $this->excludeDirs = array_merge($this->excludeDirs, (array)$dirs);
        return $this;
    }

    /**
     * Set exclude directories (replace default).
     * @param array $dirs
     * @return $this
     */
    public function excludeDirs(array $dirs): static
    {
        $this->excludeDirs = $dirs;
        return $this;
    }

    /**
     * Enable PHP meta analysis and caching.
     * @return $this
     */
    public function withPhpMeta(): static
    {
        $this->phpMetaEnabled = true;
        return $this;
    }

    /**
     * Whether any PHP meta filters are requested.
     * @return bool
     */
    protected function phpFiltersRequested(): bool
    {
        return $this->filterHasAttributes !== null
            || $this->filterTypes !== null
            || $this->filterPsr4 !== null;
    }

    /**
     * Filter by hasAttributes.
     * @param bool $value
     * @return $this
     */
    public function hasAttributes(bool $value): static
    {
        // PHP-specific filter: enable PHP meta automatically.
        $this->phpMetaEnabled = true;
        $this->filterHasAttributes = $value;
        return $this;
    }

    /**
     * Filter by type (class, interface, trait, enum, non_class).
     * @param array $types
     * @return $this
     */
    public function typeIn(array $types): static
    {
        // PHP-specific filter: enable PHP meta automatically.
        $this->phpMetaEnabled = true;
        $this->filterTypes = $types;
        return $this;
    }

    /**
     * Filter by PSR-4 compliance.
     * @param bool $value
     * @return $this
     */
    public function psr4(bool $value): static
    {
        // PHP-specific filter: enable PHP meta automatically.
        $this->phpMetaEnabled = true;
        $this->filterPsr4 = $value;
        return $this;
    }

    /**
     * Find files and return FileInfo array.
     * @return FileInfo[]
     */
    public function find(): array
    {
        $results = [];
        $phpFiltersRequested = $this->phpFiltersRequested();
        // phpMetaEnabled can be turned on explicitly (withPhpMeta) or implicitly (by PHP filters).
        $phpMetaEnabled = $this->phpMetaEnabled || $phpFiltersRequested;

        foreach ($this->roots as $rootDir) {

            // Load cache for this root
            if ($phpMetaEnabled) {
                $this->loadCache($rootDir);
            }

            // Scan directory
            $files = $this->scanDirectory($rootDir);

            // Apply filters
            foreach ($files as $filePath) {
                // Apply name filter
                if (!$this->matchesName($filePath)) {
                    continue;
                }

                // Apply path filter
                if (!$this->matchesPath($filePath, $rootDir)) {
                    continue;
                }

                // Get or compute PHP meta
                $meta = [];
                if ($phpMetaEnabled) {
                    // If any PHP filters were requested, skip non-PHP files.
                    if ($phpFiltersRequested && !$this->isPhpFile($filePath)) {
                        continue;
                    }
                    if ($this->isPhpFile($filePath)) {
                        $meta = $this->getPhpMeta($filePath, $rootDir);

                        // Apply PHP meta filters
                        if ($phpFiltersRequested && !$this->matchesPhpFilters($meta, $filePath, $rootDir)) {
                            continue;
                        }
                    }
                }

                $results[] = new FileInfo($filePath, $meta, $rootDir);
            }

            // Save cache if dirty
            if ($phpMetaEnabled) {
                $this->saveCache($rootDir);
            }
        }

        return $results;
    }

    /**
     * Find files and return paths array.
     * @return string[]
     */
    public function findPaths(): array
    {
        return array_map(fn(FileInfo $f) => $f->getPathname(), $this->find());
    }

    /**
     * Scan directory recursively.
     * @param string $dir
     * @return array
     */
    protected function scanDirectory(string $dir): array
    {
        $files = [];
        $excludeSet = array_flip($this->excludeDirs);

        try {
            $directoryIterator = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
            $filterIterator = new RecursiveCallbackFilterIterator(
                $directoryIterator,
                function (\SplFileInfo $current) use ($excludeSet) {
                    if ($current->isDir()) {
                        return !isset($excludeSet[$current->getBasename()]);
                    }
                    return true;
                }
            );
            $iterator = new RecursiveIteratorIterator($filterIterator, RecursiveIteratorIterator::SELF_FIRST);

            foreach ($iterator as $item) {
                /** @var \SplFileInfo $item */
                $basename = $item->getBasename();

                // Skip excluded directories
                if ($item->isDir()) {
                    continue;
                }

                // Skip if only files mode and not a file
                if ($this->onlyFiles && !$item->isFile()) {
                    continue;
                }

                $files[] = static::normalizePath($item->getPathname());
            }
        } catch (\Throwable $e) {
            // Ignore unreadable directories
        }

        return $files;
    }

    /**
     * Check if file matches name patterns.
     * @param string $filePath
     * @return bool
     */
    protected function matchesName(string $filePath): bool
    {
        if (empty($this->names)) {
            return true;
        }

        $basename = basename($filePath);
        foreach ($this->names as $pattern) {
            if ($this->matchPattern($basename, $pattern)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if file matches path patterns.
     * @param string $filePath
     * @param string $rootDir
     * @return bool
     */
    protected function matchesPath(string $filePath, string $rootDir): bool
    {
        if (empty($this->paths)) {
            return true;
        }

        // Use relative path for matching
        $relativePath = $this->getRelativePath($filePath, $rootDir);

        foreach ($this->paths as $pattern) {
            if ($this->isRegex($pattern) && preg_match($pattern, $relativePath)) {
                return true;
            }
            if (!$this->isRegex($pattern) && stripos($relativePath, $pattern) !== false) {
                return true;
            }
        }
        return false;
    }

    /**
     * Match a pattern (glob or regex).
     * @param string $value
     * @param string $pattern
     * @return bool
     */
    protected function matchPattern(string $value, string $pattern): bool
    {
        // Regex pattern
        if ($this->isRegex($pattern)) {
            return (bool)preg_match($pattern, $value);
        }

        // Glob pattern
        return fnmatch($pattern, $value, FNM_CASEFOLD);
    }

    /**
     * Check if pattern is a regex.
     * @param string $pattern
     * @return bool
     */
    protected function isRegex(string $pattern): bool
    {
        if ($pattern === '') {
            return false;
        }
        $delimiter = $pattern[0];
        if (!in_array($delimiter, ['/', '#', '~', '%'], true)) {
            return false;
        }
        return (bool)preg_match('/^' . preg_quote($delimiter, '/') . '.*' . preg_quote($delimiter, '/') . '[imsxuADU]*$/', $pattern);
    }

    /**
     * Check if file is a PHP file.
     * @param string $filePath
     * @return bool
     */
    protected function isPhpFile(string $filePath): bool
    {
        return strtolower(pathinfo($filePath, PATHINFO_EXTENSION)) === 'php';
    }

    /**
     * Get PHP file meta, using cache when possible.
     * @param string $filePath
     * @param string $rootDir
     * @return array
     */
    protected function getPhpMeta(string $filePath, string $rootDir): array
    {
        $cacheKey = $this->getCacheKey($rootDir);
        $mtime = @filemtime($filePath);

        // Check cache
        if (isset(static::$phpCache[$cacheKey][$filePath])) {
            $cached = static::$phpCache[$cacheKey][$filePath];
            if (isset($cached['mtime']) && $cached['mtime'] === $mtime) {
                return $cached;
            }
        }

        // Compute new meta
        $meta = $this->computePhpMeta($filePath, $mtime);

        // Update cache
        static::$phpCache[$cacheKey][$filePath] = $meta;
        static::$cacheDirty[$cacheKey] = true;

        return $meta;
    }

    /**
     * Ensure psr4 is computed and cached in meta.
     * Only computes once per (file, mtime) cache entry.
     * @param string $filePath
     * @param string $rootDir
     * @param array $meta
     * @return array Updated meta
     */
    protected function ensurePsr4Cached(string $filePath, string $rootDir, array $meta): array
    {
        if (array_key_exists('psr4', $meta)) {
            return $meta;
        }

        $psr4 = $this->checkPsr4($filePath, $rootDir, $meta['class'] ?? null);
        $meta['psr4'] = $psr4;

        $cacheKey = $this->getCacheKey($rootDir);
        static::$phpCache[$cacheKey][$filePath] = $meta;
        static::$cacheDirty[$cacheKey] = true;

        return $meta;
    }

    /**
     * Compute PHP file meta info.
     * @param string $filePath
     * @param int|false $mtime
     * @return array
     */
    protected function computePhpMeta(string $filePath, int|false $mtime): array
    {
        $meta = [
            'mtime' => $mtime ?: 0,
            'hasAttributes' => false,
            'class' => null,
        ];

        $code = @file_get_contents($filePath);
        if ($code === false || $code === '') {
            $meta['type'] = 'non_class';
            return $meta;
        }

        // Fast check for attributes
        $meta['hasAttributes'] = str_contains($code, '#[');

        // Parse to get type and declared class
        $parseResult = $this->parsePhpFile($code);
        $meta['type'] = $parseResult['type'];
        $meta['class'] = $parseResult['class'];

        return $meta;
    }

    /**
     * Parse PHP file to extract type and declared class.
     * @param string $code
     * @return array{type: string, class: string|null}
     */
    protected function parsePhpFile(string $code): array
    {
        $result = [
            'type' => 'non_class',
            'class' => null,
        ];

        try {
            $tokens = token_get_all($code);
        } catch (\Throwable $e) {
            return $result;
        }

        $namespace = '';
        $count = count($tokens);
        $prevSignificant = null;

        for ($i = 0; $i < $count; $i++) {
            $token = $tokens[$i];
            $id = is_array($token) ? $token[0] : null;

            // Extract namespace
            if ($id === T_NAMESPACE) {
                $ns = '';
                for ($j = $i + 1; $j < $count; $j++) {
                    $t = $tokens[$j];
                    if (is_array($t)) {
                        if ($t[0] === T_STRING || $t[0] === T_NS_SEPARATOR) {
                            $ns .= $t[1];
                            continue;
                        }
                        if (defined('T_NAME_QUALIFIED') && $t[0] === T_NAME_QUALIFIED) {
                            $ns .= $t[1];
                            continue;
                        }
                        if ($t[0] === T_WHITESPACE) {
                            continue;
                        }
                    } else {
                        if ($t === ';' || $t === '{') {
                            break;
                        }
                    }
                }
                $namespace = trim($ns, '\\');
                continue;
            }

            // Detect class/interface/trait/enum
            if ($id === T_CLASS || $id === T_INTERFACE || $id === T_TRAIT || (defined('T_ENUM') && $id === T_ENUM)) {
                // Skip ::class usage
                if ($prevSignificant === T_DOUBLE_COLON) {
                    $prevSignificant = null;
                    continue;
                }
                // Skip anonymous class
                if ($id === T_CLASS && $prevSignificant === T_NEW) {
                    continue;
                }

                // Determine type
                $type = match ($id) {
                    T_CLASS => 'class',
                    T_INTERFACE => 'interface',
                    T_TRAIT => 'trait',
                    default => defined('T_ENUM') && $id === T_ENUM ? 'enum' : 'class',
                };

                // Extract class name
                for ($j = $i + 1; $j < $count; $j++) {
                    $t = $tokens[$j];
                    if (!is_array($t)) {
                        continue;
                    }
                    if ($t[0] === T_WHITESPACE) {
                        continue;
                    }
                    if ($t[0] === T_STRING) {
                        $className = $t[1];
                        $result['type'] = $type;
                        $result['class'] = $namespace !== '' ? ($namespace . '\\' . $className) : $className;
                        return $result; // Return first declaration only
                    }
                    break;
                }
            }

            // Track previous significant token
            if (is_array($token)) {
                if ($id !== T_WHITESPACE && $id !== T_COMMENT && $id !== T_DOC_COMMENT) {
                    $prevSignificant = $id;
                }
            } else {
                if (trim($token) !== '') {
                    $prevSignificant = $token;
                }
            }
        }

        return $result;
    }

    /**
     * Check if meta matches PHP filters.
     * @param array $meta
     * @param string $filePath
     * @param string $rootDir
     * @return bool
     */
    protected function matchesPhpFilters(array $meta, string $filePath, string $rootDir): bool
    {
        // Filter by hasAttributes
        if ($this->filterHasAttributes !== null && $meta['hasAttributes'] !== $this->filterHasAttributes) {
            return false;
        }

        // Filter by type
        if ($this->filterTypes !== null && !in_array($meta['type'], $this->filterTypes, true)) {
            return false;
        }

        // Filter by PSR-4
        if ($this->filterPsr4 !== null) {
            $meta = $this->ensurePsr4Cached($filePath, $rootDir, $meta);
            $psr4Ok = $meta['psr4'];
            if ($psr4Ok !== $this->filterPsr4) {
                return false;
            }
        }

        return true;
    }

    /**
     * Check if file complies with PSR-4.
     * @param string $filePath
     * @param string $rootDir
     * @param string|null $declaredClass
     * @return bool
     */
    protected function checkPsr4(string $filePath, string $rootDir, ?string $declaredClass): bool
    {
        if ($declaredClass === null) {
            return false;
        }

        // Namespace-agnostic check:
        // declared class must end with the PSR-4 relative class path derived from file path.
        $relativePath = $this->getRelativePath($filePath, $rootDir);
        if ($relativePath === '' || !str_ends_with($relativePath, '.php')) {
            return false;
        }
        $relativeClassPath = substr($relativePath, 0, -4);
        $relativeClassPath = str_replace('/', '\\', $relativeClassPath);
        if (!$this->isValidPsr4ClassPath($relativeClassPath)) {
            return false;
        }

        $declaredClass = ltrim($declaredClass, '\\');
        $suffix = '\\' . $relativeClassPath;
        return $declaredClass === $relativeClassPath || str_ends_with($declaredClass, $suffix);
    }

    /**
     * Derive expected class name from file path (PSR-4 style).
     * @param string $filePath
     * @param string $rootDir
     * @param string $rootNamespace
     * @return string|null
     */
    protected function classFromFile(string $filePath, string $rootDir, string $rootNamespace): ?string
    {
        $rootDir = rtrim(static::normalizePath($rootDir), '/');
        $filePath = static::normalizePath($filePath);

        $rootLen = strlen($rootDir);
        if (strncasecmp($filePath, $rootDir, $rootLen) !== 0) {
            return null;
        }

        $relative = ltrim(substr($filePath, $rootLen), '/');
        if ($relative === '' || !str_ends_with($relative, '.php')) {
            return null;
        }

        $relative = substr($relative, 0, -4); // Remove .php
        $relative = str_replace('/', '\\', $relative);

        if (!$this->isValidPsr4ClassPath($relative)) {
            return null;
        }

        return rtrim($rootNamespace, '\\') . '\\' . $relative;
    }

    /**
     * Check if relative class path is valid PSR-4.
     * @param string $relativeClassPath
     * @return bool
     */
    protected function isValidPsr4ClassPath(string $relativeClassPath): bool
    {
        return (bool)preg_match('/^[A-Za-z_][A-Za-z0-9_]*(\\\\[A-Za-z_][A-Za-z0-9_]*)*$/', $relativeClassPath);
    }

    /**
     * Get relative path from root directory.
     * @param string $filePath
     * @param string $rootDir
     * @return string
     */
    protected function getRelativePath(string $filePath, string $rootDir): string
    {
        $rootDir = rtrim(static::normalizePath($rootDir), '/');
        $filePath = static::normalizePath($filePath);
        $rootLen = strlen($rootDir);

        if (strncasecmp($filePath, $rootDir, $rootLen) === 0) {
            return ltrim(substr($filePath, $rootLen), '/');
        }

        return $filePath;
    }

    /**
     * Get cache key for a root directory.
     * @param string $rootDir
     * @return string
     */
    protected function getCacheKey(string $rootDir): string
    {
        return hash('sha256', $rootDir);
    }

    /**
     * Get cache file path for a root directory.
     * @param string $rootDir
     * @return string
     */
    protected function getCacheFile(string $rootDir): string
    {
        $cacheKey = $this->getCacheKey($rootDir);
        return static::getCacheDir() . DIRECTORY_SEPARATOR . "php_files_{$cacheKey}.php";
    }

    /**
     * Load cache for a root directory.
     * @param string $rootDir
     * @return void
     */
    protected function loadCache(string $rootDir): void
    {
        $cacheKey = $this->getCacheKey($rootDir);
        if (isset(static::$phpCache[$cacheKey])) {
            return; // Already loaded
        }

        $cacheFile = $this->getCacheFile($rootDir);
        if (is_file($cacheFile)) {
            try {
                $data = include $cacheFile;
                if (is_array($data)) {
                    static::$phpCache[$cacheKey] = $data;
                    return;
                }
            } catch (\Throwable $e) {
                // Ignore corrupted cache
            }
        }

        static::$phpCache[$cacheKey] = [];
    }

    /**
     * Save cache for a root directory.
     * @param string $rootDir
     * @return void
     */
    protected function saveCache(string $rootDir): void
    {
        $cacheKey = $this->getCacheKey($rootDir);
        if (empty(static::$cacheDirty[$cacheKey])) {
            return;
        }

        $cacheFile = $this->getCacheFile($rootDir);
        $cacheDir = dirname($cacheFile);

        if (!is_dir($cacheDir)) {
            @mkdir($cacheDir, 0755, true);
        }

        $data = static::$phpCache[$cacheKey] ?? [];

        // Clean up deleted files
        foreach ($data as $path => $meta) {
            if (!is_file($path)) {
                unset($data[$path]);
            }
        }

        static::$phpCache[$cacheKey] = $data;

        $content = "<?php\nreturn " . var_export($data, true) . ";\n";
        $suffix = (string)getmypid();
        try {
            $suffix .= '.' . bin2hex(random_bytes(6));
        } catch (\Throwable $e) {
            $suffix .= '.' . uniqid('', true);
        }
        $tempFile = $cacheFile . '.tmp.' . $suffix;

        // No locks: write temp file then atomic rename.
        if (@file_put_contents($tempFile, $content) !== false) {
            // On Windows, rename() fails if target exists. Use a backup to swap.
            if (!@rename($tempFile, $cacheFile)) {
                $backupFile = $cacheFile . '.bak.' . $suffix;
                // Best-effort: move old cache away, then move new cache in.
                @rename($cacheFile, $backupFile);
                if (@rename($tempFile, $cacheFile)) {
                    @unlink($backupFile);
                } else {
                    // Restore old cache if possible.
                    @rename($backupFile, $cacheFile);
                    @unlink($tempFile);
                }
            }
        }

        static::$cacheDirty[$cacheKey] = false;
    }

    /**
     * Clear all caches.
     * @return void
     */
    public static function clearCache(): void
    {
        static::$phpCache = [];
        static::$cacheDirty = [];
    }

    /**
     * Normalize path separators.
     * @param string $path
     * @return string
     */
    protected static function normalizePath(string $path): string
    {
        $realPath = realpath($path);
        if ($realPath !== false) {
            return str_replace('\\', '/', $realPath);
        }
        return str_replace('\\', '/', $path);
    }
}


================================================
FILE: src/Http/Request.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Http;

use Webman\Route\Route;
use function current;
use function filter_var;
use function ip2long;
use function is_array;
use function strpos;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_NO_PRIV_RANGE;
use const FILTER_FLAG_NO_RES_RANGE;
use const FILTER_VALIDATE_IP;

/**
 * Class Request
 * @package Webman\Http
 */
class Request extends \Workerman\Protocols\Http\Request
{
    /**
     * @var string
     */
    public $plugin = null;

    /**
     * @var string
     */
    public $app = null;

    /**
     * @var string
     */
    public $controller = null;

    /**
     * @var string
     */
    public $action = null;

    /**
     * @var Route
     */
    public $route = null;

    /**
     * @var bool
     */
    protected $isDirty = false;

    /**
     * @return mixed|null
     */
    public function all()
    {
        return $this->get() + $this->post();
    }

    /**
     * Input
     * @param string $name
     * @param mixed $default
     * @return mixed
     */
    public function input(string $name, mixed $default = null)
    {
        return $this->get($name, $this->post($name, $default));
    }

    /**
     * Only
     * @param array $keys
     * @return array
     */
    public function only(array $keys): array
    {
        $all = $this->all();
        $result = [];
        foreach ($keys as $key) {
            if (isset($all[$key])) {
                $result[$key] = $all[$key];
            }
        }
        return $result;
    }

    /**
     * Except
     * @param array $keys
     * @return mixed|null
     */
    public function except(array $keys)
    {
        $all = $this->all();
        foreach ($keys as $key) {
            unset($all[$key]);
        }
        return $all;
    }

    /**
     * File
     * @param string|null $name
     * @return UploadFile|UploadFile[]|null
     */
    public function file(?string $name = null): array|null|UploadFile
    {
        $files = parent::file($name);
        if (null === $files) {
            return $name === null ? [] : null;
        }
        if ($name !== null) {
            // Multi files
            if (is_array(current($files))) {
                return $this->parseFiles($files);
            }
            return $this->parseFile($files);
        }
        $uploadFiles = [];
        foreach ($files as $name => $file) {
            // Multi files
            if (is_array(current($file))) {
                $uploadFiles[$name] = $this->parseFiles($file);
            } else {
                $uploadFiles[$name] = $this->parseFile($file);
            }
        }
        return $uploadFiles;
    }

    /**
     * ParseFile
     * @param array $file
     * @return UploadFile
     */
    protected function parseFile(array $file): UploadFile
    {
        return new UploadFile($file['tmp_name'], $file['name'], $file['type'], $file['error']);
    }

    /**
     * ParseFiles
     * @param array $files
     * @return array
     */
    protected function parseFiles(array $files): array
    {
        $uploadFiles = [];
        foreach ($files as $key => $file) {
            if (is_array(current($file))) {
                $uploadFiles[$key] = $this->parseFiles($file);
            } else {
                $uploadFiles[$key] = $this->parseFile($file);
            }
        }
        return $uploadFiles;
    }

    /**
     * GetRemoteIp
     * @return string
     */
    public function getRemoteIp(): string
    {
        return $this->connection ? $this->connection->getRemoteIp() : '0.0.0.0';
    }

    /**
     * GetRemotePort
     * @return int
     */
    public function getRemotePort(): int
    {
        return $this->connection ? $this->connection->getRemotePort() : 0;
    }

    /**
     * GetLocalIp
     * @return string
     */
    public function getLocalIp(): string
    {
        return $this->connection ? $this->connection->getLocalIp() : '0.0.0.0';
    }

    /**
     * GetLocalPort
     * @return int
     */
    public function getLocalPort(): int
    {
        return $this->connection ? $this->connection->getLocalPort() : 0;
    }

    /**
     * GetRealIp
     * @param bool $safeMode
     * @return string
     */
    public function getRealIp(bool $safeMode = true): string
    {
        $remoteIp = $this->getRemoteIp();
        if ($safeMode && !static::isIntranetIp($remoteIp)) {
            return $remoteIp;
        }
        $ip = $this->header('x-forwarded-for')
           ?? $this->header('x-real-ip')
           ?? $this->header('client-ip')
           ?? $this->header('x-client-ip')
           ?? $this->header('via')
           ?? $remoteIp;
        if (is_string($ip)) {
            $ip = current(explode(',', $ip));
        }
        return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : $remoteIp;
    }

    /**
     * Url
     * @return string
     */
    public function url(): string
    {
        return '//' . $this->host() . $this->path();
    }

    /**
     * FullUrl
     * @return string
     */
    public function fullUrl(): string
    {
        return '//' . $this->host() . $this->uri();
    }

    /**
     * IsAjax
     * @return bool
     */
    public function isAjax(): bool
    {
        return $this->header('X-Requested-With') === 'XMLHttpRequest';
    }

    /**
     * IsGet
     * @return bool
     */
    public function isGet(): bool
    {
        return $this->method() === 'GET';
    }


    /**
     * IsPost
     * @return bool
     */
    public function isPost(): bool
    {
        return $this->method() === 'POST';
    }


    /**
     * IsPjax
     * @return bool
     */
    public function isPjax(): bool
    {
        return (bool)$this->header('X-PJAX');
    }

    /**
     * ExpectsJson
     * @return bool
     */
    public function expectsJson(): bool
    {
        return ($this->isAjax() && !$this->isPjax()) || $this->acceptJson();
    }

    /**
     * AcceptJson
     * @return bool
     */
    public function acceptJson(): bool
    {
        return false !== strpos($this->header('accept', ''), 'json');
    }

    /**
     * IsIntranetIp
     * @param string $ip
     * @return bool
     */
    public static function isIntranetIp(string $ip): bool
    {
        // Not validate ip .
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }
        // Is intranet ip ? For IPv4, the result of false may not be accurate, so we need to check it manually later .
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return true;
        }
        // Manual check only for IPv4 .
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            return false;
        }
        // Manual check .
        $reservedIps = [
            1681915904 => 1686110207, // 100.64.0.0 -  100.127.255.255
            3221225472 => 3221225727, // 192.0.0.0 - 192.0.0.255
            3221225984 => 3221226239, // 192.0.2.0 - 192.0.2.255
            3227017984 => 3227018239, // 192.88.99.0 - 192.88.99.255
            3323068416 => 3323199487, // 198.18.0.0 - 198.19.255.255
            3325256704 => 3325256959, // 198.51.100.0 - 198.51.100.255
            3405803776 => 3405804031, // 203.0.113.0 - 203.0.113.255
            3758096384 => 4026531839, // 224.0.0.0 - 239.255.255.255
        ];
        $ipLong = ip2long($ip);
        foreach ($reservedIps as $ipStart => $ipEnd) {
            if (($ipLong >= $ipStart) && ($ipLong <= $ipEnd)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Set get.
     * @param array|string $input
     * @param mixed $value
     * @return Request
     */
    public function setGet(array|string $input, mixed $value = null): Request
    {
        $this->isDirty = true;
        $input = is_array($input) ? $input : array_merge($this->get(), [$input => $value]);
        if (isset($this->data)) {
            $this->data['get'] = $input;
        } else {
            $this->_data['get'] = $input;
        }
        return $this;
    }

    /**
     * Set post.
     * @param array|string $input
     * @param mixed $value
     * @return Request
     */
    public function setPost(array|string $input, mixed $value = null): Request
    {
        $this->isDirty = true;
        $input = is_array($input) ? $input : array_merge($this->post(), [$input => $value]);
        if (isset($this->data)) {
            $this->data['post'] = $input;
        } else {
            $this->_data['post'] = $input;
        }
        return $this;
    }

    /**
     * Set header.
     * @param array|string $input
     * @param mixed $value
     * @return Request
     */
    public function setHeader(array|string $input, mixed $value = null): Request
    {
        $this->isDirty = true;
        $input = is_array($input) ? $input : array_merge($this->header(), [$input => $value]);
        if (isset($this->data)) {
            $this->data['headers'] = $input;
        } else {
            $this->_data['headers'] = $input;
        }
        return $this;
    }

    /**
     * Destroy
     */
    public function destroy(): void
    {
        if ($this->isDirty) {
            unset($this->data['get'], $this->data['post'], $this->data['headers']);
        }
        parent::destroy();
    }

}


================================================
FILE: src/Http/Response.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Http;

use Throwable;
use Webman\App;
use function filemtime;
use function gmdate;

/**
 * Class Response
 * @package Webman\Http
 */
class Response extends \Workerman\Protocols\Http\Response
{
    /**
     * @var Throwable
     */
    protected $exception = null;

    /**
     * File
     * @param string $file
     * @return $this
     */
    public function file(string $file): Response
    {
        if ($this->notModifiedSince($file)) {
            return $this->withStatus(304);
        }
        return $this->withFile($file);
    }

    /**
     * Download
     * @param string $file
     * @param string $downloadName
     * @return $this
     */
    public function download(string $file, string $downloadName = ''): Response
    {
        $this->withFile($file);
        if ($downloadName) {
            // Sanitize to prevent header injection
            $downloadName = str_replace(['"', "\r", "\n", "\0"], '', $downloadName);
            $this->header('Content-Disposition', "attachment; filename=\"$downloadName\"");
        }
        return $this;
    }

    /**
     * NotModifiedSince
     * @param string $file
     * @return bool
     */
    protected function notModifiedSince(string $file): bool
    {
        $ifModifiedSince = App::request()->header('if-modified-since');
        if ($ifModifiedSince === null || !is_file($file) || !($mtime = filemtime($file))) {
            return false;
        }
        return $ifModifiedSince === gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
    }

    /**
     * Exception
     * @param Throwable|null $exception
     * @return Throwable|null
     */
    public function exception(?Throwable $exception = null): ?Throwable
    {
        if ($exception) {
            $this->exception = $exception;
        }
        return $this->exception;
    }
}


================================================
FILE: src/Http/UploadFile.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Http;

use Webman\File;
use function pathinfo;

/**
 * Class UploadFile
 * @package Webman\Http
 */
class UploadFile extends File
{
    /**
     * @var string
     */
    protected $uploadName = null;

    /**
     * @var string
     */
    protected $uploadMimeType = null;

    /**
     * @var int
     */
    protected $uploadErrorCode = null;

    /**
     * UploadFile constructor.
     *
     * @param string $fileName
     * @param string $uploadName
     * @param string $uploadMimeType
     * @param int $uploadErrorCode
     */
    public function __construct(string $fileName, string $uploadName, string $uploadMimeType, int $uploadErrorCode)
    {
        $this->uploadName = $uploadName;
        $this->uploadMimeType = $uploadMimeType;
        $this->uploadErrorCode = $uploadErrorCode;
        parent::__construct($fileName);
    }

    /**
     * GetUploadName
     * @return string
     */
    public function getUploadName(): ?string
    {
        return $this->uploadName;
    }

    /**
     * GetUploadMimeType
     * @return string
     */
    public function getUploadMimeType(): ?string
    {
        return $this->uploadMimeType;
    }

    /**
     * GetUploadExtension
     * @return string
     */
    public function getUploadExtension(): string
    {
        return pathinfo($this->uploadName, PATHINFO_EXTENSION);
    }

    /**
     * GetUploadErrorCode
     * @return int
     */
    public function getUploadErrorCode(): ?int
    {
        return $this->uploadErrorCode;
    }

    /**
     * IsValid
     * @return bool
     */
    public function isValid(): bool
    {
        return $this->uploadErrorCode === UPLOAD_ERR_OK;
    }

    /**
     * GetUploadMineType
     * @return string
     * @deprecated
     */
    public function getUploadMineType(): ?string
    {
        return $this->uploadMimeType;
    }
}


================================================
FILE: src/Install.php
================================================
<?php

namespace Webman;

class Install
{
    const WEBMAN_PLUGIN = true;

    /**
     * @var array
     */
    protected static $pathRelation = [
        'start.php' => 'start.php',
        'windows.php' => 'windows.php',
    ];

    /**
     * Install
     * @return void
     */
    public static function install()
    {
        static::installByRelation();
    }

    /**
     * Uninstall
     * @return void
     */
    public static function uninstall()
    {

    }

    /**
     * InstallByRelation
     * @return void
     */
    public static function installByRelation()
    {
        foreach (static::$pathRelation as $source => $dest) {
            $parentDir = base_path(dirname($dest));
            if (!is_dir($parentDir)) {
                mkdir($parentDir, 0777, true);
            }
            $sourceFile = __DIR__ . "/$source";
            copy_dir($sourceFile, base_path($dest), true);
            echo "Create $dest\r\n";
            if (is_file($sourceFile)) {
                @unlink($sourceFile);
            }
        }
        if (is_file($file = base_path('support/helpers.php'))) {
            file_put_contents($file, "<?php\n// This file is generated by Webman, please don't modify it.\n");
            echo "Clear helpers.php\r\n";
        }
    }

}


================================================
FILE: src/Middleware.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman;


use Closure;
use ReflectionAttribute;
use support\annotation\Middleware as MiddlewareAttribute;
use Webman\Route\Route;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;
use function array_merge;
use function array_reverse;
use function is_array;
use function method_exists;

class Middleware
{

    /**
     * @var array
     */
    protected static $instances = [];

    /**
     * @var array Cache for controller middleware resolved via reflection/attributes.
     */
    protected static $controllerMiddlewareCache = [];

    /**
     * @param mixed $allMiddlewares
     * @param string $plugin
     * @return void
     */
    public static function load($allMiddlewares, string $plugin = '')
    {
        if (!is_array($allMiddlewares)) {
            return;
        }
        foreach ($allMiddlewares as $appName => $middlewares) {
            if (!is_array($middlewares)) {
                throw new RuntimeException('Bad middleware config');
            }
            $pluginKey = $plugin;
            $appKey = $appName;
            if ($appKey === '@') {
                $pluginKey = '';
            } elseif (strpos($appKey, 'plugin.') !== false) {
                $explode = explode('.', $appKey, 4);
                $pluginKey = $explode[1];
                $appKey = $explode[2] ?? '';
            }
            foreach ($middlewares as $className) {
                if (method_exists($className, 'process')) {
                    static::$instances[$pluginKey][$appKey][] = [$className, 'process'];
                } else {
                    // @todo Log
                    echo "middleware $className::process not exsits\n";
                }
            }
        }
    }

    /**
     * @param string $plugin
     * @param string $appName
     * @param string|array|Closure $controller
     * @param Route|null $route
     * @param bool $withGlobalMiddleware
     * @return array
     */
    public static function getMiddleware(string $plugin, string $appName, string|array|Closure $controller, Route|null $route, bool $withGlobalMiddleware = true): array
    {
        $isController = is_array($controller) && is_string($controller[0]);
        $globalMiddleware = $withGlobalMiddleware ? static::$instances['']['@'] ?? [] : [];
        $appGlobalMiddleware = $withGlobalMiddleware && isset(static::$instances[$plugin]['']) ? static::$instances[$plugin][''] : [];
        $middlewares = $routeMiddlewares = [];
        // Route middleware
        if ($route) {
            foreach (array_reverse($route->getMiddleware()) as $className) {
                $routeMiddlewares[] = [$className, 'process'];
            }
        }
        if ($isController && $controller[0] && class_exists($controller[0])) {
            $cacheKey = $controller[0] . '::' . $controller[1];
            if (isset(static::$controllerMiddlewareCache[$cacheKey])) {
                $cached = static::$controllerMiddlewareCache[$cacheKey];
                $middlewares = array_merge($cached['before_route'], $routeMiddlewares, $cached['after_route']);
            } else {
                $beforeRoute = [];
                $afterRoute = [];
                // Controller middleware annotation
                $reflectionClass = new ReflectionClass($controller[0]);
                self::prepareAttributeMiddlewares($beforeRoute, $reflectionClass);
                // Controller middleware property
                if ($reflectionClass->hasProperty('middleware')) {
                    $defaultProperties = $reflectionClass->getDefaultProperties();
                    $middlewaresClasses = $defaultProperties['middleware'];
                    foreach ((array)$middlewaresClasses as $className) {
                        $beforeRoute[] = [$className, 'process'];
                    }
                }
                // Method middleware annotation (route must be between controller and method)
                if ($reflectionClass->hasMethod($controller[1])) {
                    self::prepareAttributeMiddlewares($afterRoute, $reflectionClass->getMethod($controller[1]));
                }
                $middlewares = array_merge($beforeRoute, $routeMiddlewares, $afterRoute);
                static::$controllerMiddlewareCache[$cacheKey] = ['before_route' => $beforeRoute, 'after_route' => $afterRoute];
                if (count(static::$controllerMiddlewareCache) > 1024) {
                    unset(static::$controllerMiddlewareCache[key(static::$controllerMiddlewareCache)]);
                }
            }
        } else {
            // Route middleware
            $middlewares = array_merge($middlewares, $routeMiddlewares);
        }
        if ($appName === '') {
            return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $middlewares));
        }
        $appMiddleware = static::$instances[$plugin][$appName] ?? [];
        return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $appMiddleware, $middlewares));
    }

    /**
     * @param array $middlewares
     * @param ReflectionClass|ReflectionMethod $reflection
     * @return void
     */
    private static function prepareAttributeMiddlewares(array &$middlewares, ReflectionClass|ReflectionMethod $reflection): void
    {
        if ($reflection instanceof ReflectionClass && $parent_ref = $reflection->getParentClass()) {
            self::prepareAttributeMiddlewares($middlewares, $parent_ref);
        }
        $middlewareAttributes = $reflection->getAttributes(MiddlewareAttribute::class, ReflectionAttribute::IS_INSTANCEOF);
        foreach ($middlewareAttributes as $middlewareAttribute) {
            $middlewareAttributeInstance = $middlewareAttribute->newInstance();
            $middlewares = array_merge($middlewares, $middlewareAttributeInstance->getMiddlewares());
        }
    }

    /**
     * @return void
     * @deprecated
     */
    public static function container($_)
    {

    }
}


================================================
FILE: src/MiddlewareInterface.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman;

use Webman\Http\Request;
use Webman\Http\Response;

interface MiddlewareInterface
{
    /**
     * Process an incoming server request.
     *
     * Processes an incoming server request in order to produce a response.
     * If unable to produce the response itself, it may delegate to the provided
     * request handler to do so.
     */
    public function process(Request $request, callable $handler): Response;
}


================================================
FILE: src/Route/Route.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Route;

use Webman\Route as Router;
use function array_merge;
use function count;
use function preg_replace_callback;
use function str_replace;

/**
 * Class Route
 * @package Webman
 */
class Route
{
    /**
     * @var string|null
     */
    protected $name = null;

    /**
     * @var array
     */
    protected $methods = [];

    /**
     * @var string
     */
    protected $path = '';

    /**
     * @var callable
     */
    protected $callback = null;

    /**
     * @var array
     */
    protected $middlewares = [];

    /**
     * @var array
     */
    protected $params = [];

    /**
     * Route constructor.
     * @param array $methods
     * @param string $path
     * @param callable $callback
     */
    public function __construct($methods, string $path, $callback)
    {
        $this->methods = (array)$methods;
        $this->path = $path;
        $this->callback = $callback;
    }

    /**
     * Get name.
     * @return string|null
     */
    public function getName(): ?string
    {
        return $this->name ?? null;
    }

    /**
     * Name.
     * @param string $name
     * @return $this
     */
    public function name(string $name): Route
    {
        $this->name = $name;
        Router::setByName($name, $this);
        return $this;
    }

    /**
     * Middleware.
     * @param mixed $middleware
     * @return $this|array
     */
    public function middleware(mixed $middleware = null)
    {
        if ($middleware === null) {
            return $this->middlewares;
        }
        $this->middlewares = array_merge($this->middlewares, is_array($middleware) ? array_reverse($middleware) : [$middleware]);
        return $this;
    }

    /**
     * GetPath.
     * @return string
     */
    public function getPath(): string
    {
        return $this->path;
    }

    /**
     * GetMethods.
     * @return array
     */
    public function getMethods(): array
    {
        return $this->methods;
    }

    /**
     * GetCallback.
     * @return callable|null
     */
    public function getCallback()
    {
        return $this->callback;
    }

    /**
     * GetMiddleware.
     * @return array
     */
    public function getMiddleware(): array
    {
        return $this->middlewares;
    }

    /**
     * Param.
     * @param string|null $name
     * @param mixed $default
     * @return mixed
     */
    public function param(?string $name = null, mixed $default = null)
    {
        if ($name === null) {
            return $this->params;
        }
        return $this->params[$name] ?? $default;
    }

    /**
     * SetParams.
     * @param array $params
     * @return $this
     */
    public function setParams(array $params): Route
    {
        $this->params = array_merge($this->params, $params);
        return $this;
    }

    /**
     * Url.
     * @param array $parameters
     * @return string
     */
    public function url(array $parameters = []): string
    {
        if (empty($parameters)) {
            return $this->path;
        }
        $path = str_replace(['[', ']'], '', $this->path);
        $path = preg_replace_callback('/\{(.*?)(?:\:[^\}]*?)*?\}/', function ($matches) use (&$parameters) {
            if (!$parameters) {
                return $matches[0];
            }
            if (isset($parameters[$matches[1]])) {
                $value = $parameters[$matches[1]];
                unset($parameters[$matches[1]]);
                return $value;
            }
            $key = key($parameters);
            if (is_int($key)) {
                $value = $parameters[$key];
                unset($parameters[$key]);
                return $value;
            }
            return $matches[0];
        }, $path);
        return count($parameters) > 0 ? $path . '?' . http_build_query($parameters) : $path;
    }

}


================================================
FILE: src/Route.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman;

use FastRoute\Dispatcher\GroupCountBased;
use FastRoute\RouteCollector;
use FilesystemIterator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use RuntimeException;
use support\annotation\Middleware as MiddlewareAttribute;
use support\annotation\route\DisableDefaultRoute;
use support\annotation\route\Route as RouteAttribute;
use support\annotation\route\RouteGroup as RouteGroupAttribute;
use Webman\Finder\FileInfo;
use Webman\Finder\ControllerFinder;
use Webman\Route\Route as RouteObject;
use function array_diff;
use function array_values;
use function class_exists;
use function explode;
use function FastRoute\simpleDispatcher;
use function in_array;
use function is_array;
use function is_callable;
use function is_file;
use function is_scalar;
use function is_string;
use function json_encode;
use function method_exists;
use function strpos;

/**
 * Class Route
 * @package Webman
 */
class Route
{
    /**
     * @var Route
     */
    protected static $instance = null;

    /**
     * @var GroupCountBased
     */
    protected static $dispatcher = null;

    /**
     * @var RouteCollector
     */
    protected static $collector = null;

    /**
     * @var RouteObject[]
     */
    protected static $fallbackRoutes = [];

    /**
     * @var array
     */
    protected static $fallback = [];

    /**
     * @var array
     */
    protected static $nameList = [];

    /**
     * @var string
     */
    protected static $groupPrefix = '';

    /**
     * @var bool
     */
    protected static $disabledDefaultRoutes = [];

    /**
     * @var array
     */
    protected static $disabledDefaultRouteControllers = [];

    /**
     * @var array
     */
    protected static $disabledDefaultRouteActions = [];

    /**
     * @var RouteObject[]
     */
    protected static $allRoutes = [];

    /**
     * Index for conflict detection: ["METHOD path" => "callback string"]
     * @var array<string, string>
     */
    protected static array $methodPathIndex = [];

    /**
     * @var string|null
     */
    protected static ?string $registeringSource = null;

    /**
     * @var RouteObject[]
     */
    protected $routes = [];

    /**
     * @var Route[]
     */
    protected $children = [];

    /**
     * Add GET route.
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    public static function get(string $path, $callback): RouteObject
    {
        return static::addRoute('GET', $path, $callback);
    }

    /**
     * Add POST route.
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    public static function post(string $path, $callback): RouteObject
    {
        return static::addRoute('POST', $path, $callback);
    }

    /**
     * Add PUT route.
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    public static function put(string $path, $callback): RouteObject
    {
        return static::addRoute('PUT', $path, $callback);
    }

    /**
     * Add PATCH route.
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    public static function patch(string $path, $callback): RouteObject
    {
        return static::addRoute('PATCH', $path, $callback);
    }

    /**
     * Add DELETE route.
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    public static function delete(string $path, $callback): RouteObject
    {
        return static::addRoute('DELETE', $path, $callback);
    }

    /**
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    public static function head(string $path, $callback): RouteObject
    {
        return static::addRoute('HEAD', $path, $callback);
    }

    /**
     * Add HEAD route.
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    public static function options(string $path, $callback): RouteObject
    {
        return static::addRoute('OPTIONS', $path, $callback);
    }

    /**
     * Add OPTIONS route.
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    public static function any(string $path, $callback): RouteObject
    {
        return static::addRoute(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], $path, $callback);
    }

    /**
     * Add route.
     * @param $method
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    public static function add($method, string $path, $callback): RouteObject
    {
        return static::addRoute($method, $path, $callback);
    }

    /**
     * Add group.
     * @param string|callable $path
     * @param callable|null $callback
     * @return static
     */
    public static function group($path, ?callable $callback = null): Route
    {
        if ($callback === null) {
            $callback = $path;
            $path = '';
        }
        $previousGroupPrefix = static::$groupPrefix;
        static::$groupPrefix = $previousGroupPrefix . $path;
        $previousInstance = static::$instance;
        $instance = static::$instance = new static;
        static::$collector->addGroup($path, $callback);
        static::$groupPrefix = $previousGroupPrefix;
        static::$instance = $previousInstance;
        if ($previousInstance) {
            $previousInstance->addChild($instance);
        }
        return $instance;
    }

    /**
     * Add resource.
     * @param string $name
     * @param string $controller
     * @param array $options
     * @return void
     */
    public static function resource(string $name, string $controller, array $options = [])
    {
        $name = trim($name, '/');
        if (is_array($options) && !empty($options)) {
            $diffOptions = array_diff($options, ['index', 'create', 'store', 'update', 'show', 'edit', 'destroy', 'recovery']);
            if (!empty($diffOptions)) {
                foreach ($diffOptions as $action) {
                    static::any("/$name/{$action}[/{id}]", [$controller, $action])->name("$name.{$action}");
                }
            }
            // 注册路由 由于顺序不同会导致路由无效 因此不适用循环注册
            if (in_array('index', $options)) static::get("/$name", [$controller, 'index'])->name("$name.index");
            if (in_array('create', $options)) static::get("/$name/create", [$controller, 'create'])->name("$name.create");
            if (in_array('store', $options)) static::post("/$name", [$controller, 'store'])->name("$name.store");
            if (in_array('update', $options)) static::put("/$name/{id}", [$controller, 'update'])->name("$name.update");
            if (in_array('patch', $options)) static::patch("/$name/{id}", [$controller, 'patch'])->name("$name.patch");
            if (in_array('show', $options)) static::get("/$name/{id}", [$controller, 'show'])->name("$name.show");
            if (in_array('edit', $options)) static::get("/$name/{id}/edit", [$controller, 'edit'])->name("$name.edit");
            if (in_array('destroy', $options)) static::delete("/$name/{id}", [$controller, 'destroy'])->name("$name.destroy");
            if (in_array('recovery', $options)) static::put("/$name/{id}/recovery", [$controller, 'recovery'])->name("$name.recovery");
        } else {
            //为空时自动注册所有常用路由
            if (method_exists($controller, 'index')) static::get("/$name", [$controller, 'index'])->name("$name.index");
            if (method_exists($controller, 'create')) static::get("/$name/create", [$controller, 'create'])->name("$name.create");
            if (method_exists($controller, 'store')) static::post("/$name", [$controller, 'store'])->name("$name.store");
            if (method_exists($controller, 'update')) static::put("/$name/{id}", [$controller, 'update'])->name("$name.update");
            if (method_exists($controller, 'patch')) static::patch("/$name/{id}", [$controller, 'patch'])->name("$name.patch");
            if (method_exists($controller, 'show')) static::get("/$name/{id}", [$controller, 'show'])->name("$name.show");
            if (method_exists($controller, 'edit')) static::get("/$name/{id}/edit", [$controller, 'edit'])->name("$name.edit");
            if (method_exists($controller, 'destroy')) static::delete("/$name/{id}", [$controller, 'destroy'])->name("$name.destroy");
            if (method_exists($controller, 'recovery')) static::put("/$name/{id}/recovery", [$controller, 'recovery'])->name("$name.recovery");
        }
    }

    /**
     * Get routes.
     * @return RouteObject[]
     */
    public static function getRoutes(): array
    {
        return static::$allRoutes;
    }

    /**
     * Disable default route.
     * @param array|string $plugin
     * @param string|null $app
     * @return bool
     */
    public static function disableDefaultRoute(array|string $plugin = '', ?string $app = null): bool
    {
        // Is [controller action]
        if (is_array($plugin)) {
            $controllerAction = $plugin;
            if (!isset($controllerAction[0]) || !is_string($controllerAction[0]) ||
                !isset($controllerAction[1]) || !is_string($controllerAction[1])) {
                return false;
            }
            $controller = $controllerAction[0];
            $action = $controllerAction[1];
            static::$disabledDefaultRouteActions[$controller][$action] = $action;
            return true;
        }
        // Is plugin
        if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) {
            if (!isset(static::$disabledDefaultRoutes[$plugin])) {
                static::$disabledDefaultRoutes[$plugin] = [];
            }
            $app = $app ?? '*';
            static::$disabledDefaultRoutes[$plugin][$app] = $app;
            return true;
        }
        // Is controller
        if (is_string($plugin) && class_exists($plugin)) {
            static::$disabledDefaultRouteControllers[$plugin] = $plugin;
            return true;
        }
        return false;
    }

    /**
     * Is default route disabled.
     * @param array|string $plugin
     * @param string|null $app
     * @return bool
     */
    public static function isDefaultRouteDisabled(array|string $plugin = '', ?string $app = null): bool
    {
        // Is [controller action]
        if (is_array($plugin)) {
            if (!isset($plugin[0]) || !is_string($plugin[0]) ||
                !isset($plugin[1]) || !is_string($plugin[1])) {
                return false;
            }
            return isset(static::$disabledDefaultRouteActions[$plugin[0]][$plugin[1]]) || static::isDefaultRouteDisabledByAnnotation($plugin[0], $plugin[1]);
        }
        // Is plugin
        if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) {
            $app = $app ?? '*';
            return isset(static::$disabledDefaultRoutes[$plugin]['*']) || isset(static::$disabledDefaultRoutes[$plugin][$app]);
        }
        // Is controller
        if (is_string($plugin) && class_exists($plugin)) {
            return isset(static::$disabledDefaultRouteControllers[$plugin]);
        }
        return false;
    }

    /**
     * Is default route disabled by annotation.
     * @param string $controller
     * @param string|null $action
     * @return bool
     */
    protected static function isDefaultRouteDisabledByAnnotation(string $controller, ?string $action = null): bool
    {
        if (class_exists($controller)) {
            $reflectionClass = new ReflectionClass($controller);
            if (static::isRefHasDefaultRouteDisabledAnnotation($reflectionClass)) {
                return true;
            }
            if ($action && $reflectionClass->hasMethod($action)) {
                $reflectionMethod = $reflectionClass->getMethod($action);
                if ($reflectionMethod->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Is reflection class has default route disabled annotation.
     * @param ReflectionClass $reflectionClass
     * @return bool
     */
    protected static function isRefHasDefaultRouteDisabledAnnotation(ReflectionClass $reflectionClass): bool
    {
        $has = $reflectionClass->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF);
        if ($has) {
            return true;
        }
        if (method_exists($reflectionClass, 'getParentClass')) {
            $parent = $reflectionClass->getParentClass();
            if ($parent) {
                return static::isRefHasDefaultRouteDisabledAnnotation($parent);
            }
        }
        return false;
    }

    /**
     * Add middleware.
     * @param $middleware
     * @return $this
     */
    public function middleware($middleware): Route
    {
        foreach ($this->routes as $route) {
            $route->middleware($middleware);
        }
        foreach ($this->getChildren() as $child) {
            $child->middleware($middleware);
        }
        return $this;
    }

    /**
     * Collect route.
     * @param RouteObject $route
     */
    public function collect(RouteObject $route)
    {
        $this->routes[] = $route;
    }

    /**
     * Set by name.
     * @param string $name
     * @param RouteObject $instance
     */
    public static function setByName(string $name, RouteObject $instance)
    {
        static::$nameList[$name] = $instance;
    }

    /**
     * Get by name.
     * @param string $name
     * @return null|RouteObject
     */
    public static function getByName(string $name): ?RouteObject
    {
        return static::$nameList[$name] ?? null;
    }

    /**
     * Add child.
     * @param Route $route
     * @return void
     */
    public function addChild(Route $route)
    {
        $this->children[] = $route;
    }

    /**
     * Get children.
     * @return Route[]
     */
    public function getChildren()
    {
        return $this->children;
    }

    /**
     * Dispatch.
     * @param string $method
     * @param string $path
     * @return array
     */
    public static function dispatch(string $method, string $path): array
    {
        return static::$dispatcher->dispatch($method, $path);
    }

    /**
     * Convert to callable.
     * @param string $path
     * @param callable|mixed $callback
     * @return callable|false|string[]
     */
    public static function convertToCallable(string $path, $callback)
    {
        if (is_string($callback) && strpos($callback, '@')) {
            $callback = explode('@', $callback, 2);
        }

        if (!is_array($callback)) {
            if (!is_callable($callback)) {
                $callStr = is_scalar($callback) ? $callback : 'Closure';
                echo "Route $path $callStr is not callable\n";
                return false;
            }
        } else {
            $callback = array_values($callback);
            if (!isset($callback[1]) || !class_exists($callback[0]) || !method_exists($callback[0], $callback[1])) {
                echo "Route $path " . json_encode($callback) . " is not callable\n";
                return false;
            }
        }

        return $callback;
    }

    /**
     * Add route.
     * @param array|string $methods
     * @param string $path
     * @param callable|mixed $callback
     * @return RouteObject
     */
    protected static function addRoute($methods, string $path, $callback): RouteObject
    {
        $fullPath = static::$groupPrefix . $path;
        foreach ((array)$methods as $method) {
            $method = strtoupper((string)$method);
            $key = $method . ' ' . $fullPath;
            if (isset(static::$methodPathIndex[$key])) {
                $old = static::$methodPathIndex[$key];
                $new = static::callbackToString($callback);
                $source = static::$registeringSource ? (' from ' . static::$registeringSource) : '';
                throw new RuntimeException("Route conflict: [$key] already registered as $old, cannot register $new$source");
            }
            static::$methodPathIndex[$key] = static::callbackToString($callback);
        }

        $route = new RouteObject($methods, static::$groupPrefix . $path, $callback);
        static::$allRoutes[] = $route;

        if ($callback = static::convertToCallable($path, $callback)) {
            static::$collector->addRoute($methods, $path, ['callback' => $callback, 'route' => $route]);
        }
        if (static::$instance) {
            static::$instance->collect($route);
        }
        return $route;
    }

    /**
     * Load.
     * @param mixed $paths
     * @return void
     */
    public static function load($paths)
    {
        if (!is_array($paths)) {
            return;
        }
        static::$dispatcher = null;
        static::$collector = null;
        static::$fallbackRoutes = [];
        static::$fallback = [];
        static::$nameList = [];
        static::$disabledDefaultRoutes = [];
        static::$disabledDefaultRouteControllers = [];
        static::$disabledDefaultRouteActions = [];
        static::$allRoutes = [];
        static::$methodPathIndex = [];
        static::$registeringSource = null;

        static::$dispatcher = simpleDispatcher(function (RouteCollector $route) use ($paths) {
            Route::setCollector($route);
            foreach ($paths as $configPath) {
                $routeConfigFile = $configPath . '/route.php';
                if (is_file($routeConfigFile)) {
                    require_once $routeConfigFile;
                }
                if (!is_dir($pluginConfigPath = $configPath . '/plugin')) {
                    continue;
                }
                $dirIterator = new RecursiveDirectoryIterator($pluginConfigPath, FilesystemIterator::FOLLOW_SYMLINKS);
                $iterator = new RecursiveIteratorIterator($dirIterator);
                foreach ($iterator as $file) {
                    if ($file->getBaseName('.php') !== 'route') {
                        continue;
                    }
                    $appConfigFile = pathinfo($file, PATHINFO_DIRNAME) . '/app.php';
                    if (!is_file($appConfigFile)) {
                        continue;
                    }
                    $appConfig = include $appConfigFile;
                    if (empty($appConfig['enable'])) {
                        continue;
                    }
                    require_once $file;
                }
            }
            static::loadAnnotationRoutes();
        });
    }

    /**
     * SetCollector.
     * @param RouteCollector $route
     * @return void
     */
    public static function setCollector(RouteCollector $route)
    {
        static::$collector = $route;
    }

    /**
     * Fallback.
     * @param callable|mixed $callback
     * @param string $plugin
     * @return RouteObject
     */
    public static function fallback(callable $callback, string $plugin = '')
    {
        $route = new RouteObject([], '', $callback);
        static::$fallbackRoutes[$plugin] = $route;
        return $route;
    }

    /**
     * GetFallBack.
     * @param string $plugin
     * @param int $status
     * @return callable|null
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     * @throws ReflectionException
     */
    public static function getFallback(string $plugin = '', int $status = 404)
    {
        if (!isset(static::$fallback[$plugin])) {
            $callback = null;
            $route = static::$fallbackRoutes[$plugin] ?? null;
            static::$fallback[$plugin] = $route ? App::getCallback($plugin, 'NOT_FOUND', $route->getCallback(), ['status' => $status], false, $route) : null;
        }
        return static::$fallback[$plugin];
    }

    /**
     * Load annotation routes.
     * @return void
     */
    protected static function loadAnnotationRoutes(): void
    {
        $controllerFiles = ControllerFinder::files('*');
        if (!$controllerFiles) {
            return;
        }
        $routes = static::buildAnnotationRouteDefinitions($controllerFiles);
        static::registerAnnotationRouteDefinitions($routes);

    }

    /**
     * Build annotation route definitions.
     * @param FileInfo[] $controllerFiles
     * @return array<int,array{methods: string[], path: string, callback: array{0:string,1:string}, name: ?string, middlewares: array}>
     */
    protected static function buildAnnotationRouteDefinitions(array $controllerFiles): array
    {
        $definitions = [];

        foreach ($controllerFiles as $foundFile) {
            $meta = $foundFile->meta();
            $controllerClass = $meta['class'] ?? null;
            if (!$controllerClass) {
                continue;
            }

            $file = $foundFile->getPathname();
            if (!class_exists($controllerClass)) {
                require_once $file;
            }
            if (!class_exists($controllerClass)) {
                continue;
            }

            $ref = new ReflectionClass($controllerClass);
            if ($ref->isAbstract() || $ref->isInterface()) {
                continue;
            }

            $prefix = '';
            $groupAttrs = $ref->getAttributes(RouteGroupAttribute::class, ReflectionAttribute::IS_INSTANCEOF);
            if ($groupAttrs) {
                /** @var RouteGroupAttribute $group */
                $group = $groupAttrs[0]->newInstance();
                $prefix = static::normalizeRoutePrefix($group->prefix);
            }

            foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
                if ($method->isConstructor() || $method->isDestructor()) {
                    continue;
                }
                if ($method->getDeclaringClass()->getName() !== $controllerClass) {
                    continue;
                }

                $routeAttrs = $method->getAttributes(RouteAttribute::class, ReflectionAttribute::IS_INSTANCEOF);
                if (!$routeAttrs) {
                    continue;
                }

                foreach ($routeAttrs as $routeAttr) {
                    /** @var RouteAttribute $route */
                    $route = $routeAttr->newInstance();
                    if ($route->path === null) {
                        // Null path means "method restriction only" for default route, do not register.
                        continue;
                    }
                    $source = $controllerClass . '::' . $method->getName();
                    $path = static::normalizeRoutePath($route->path, $source);
                    $fullPath = $prefix . $path;
                    if ($fullPath === '') {
                        throw new RuntimeException("Annotation route resolves to empty path: #[Get('')] requires a #[RouteGroup] prefix ($source)");
                    }

                    $methods = [];
                    foreach ($route->methods as $m) {
                        $methods[] = strtoupper((string)$m);
                    }

                    $definitions[] = [
                        'methods' => $methods,
                        'path' => $fullPath,
                        'callback' => [$controllerClass, $method->getName()],
                        'name' => $route->name,
                    ];
                }
            }
        }

        return $definitions;
    }

    /**
     * Collect middlewares from attributes.
     * @param array<ReflectionAttribute> $attributes
     * @return array
     */
    protected static function collectMiddlewaresFromAttributes(array $attributes): array
    {
        $middlewares = [];
        foreach ($attributes as $attribute) {
            /** @var MiddlewareAttribute $instance */
            $instance = $attribute->newInstance();
            foreach ($instance->getMiddlewares() as $middleware) {
                if (is_string($middleware)) {
                    $middlewares[] = $middleware;
                    continue;
                }
                if (is_array($middleware) && isset($middleware[0]) && is_string($middleware[0])) {
                    $middlewares[] = $middleware[0];
                }
            }
        }
        return $middlewares;
    }

    /**
     * Register annotation route definitions.
     * @param array $definitions
     * @return void
     */
    protected static function registerAnnotationRouteDefinitions(array $definitions): void
    {
        foreach ($definitions as $definition) {
            static::$registeringSource = 'annotation ' . $definition['callback'][0] . '::' . $definition['callback'][1];
            $route = static::add($definition['methods'], $definition['path'], $definition['callback']);
            if (!empty($definition['name'])) {
                $route->name($definition['name']);
            }
            if (!empty($definition['middlewares'])) {
                $route->middleware($definition['middlewares']);
            }
            static::$registeringSource = null;
        }
    }

    /**
     * Normalize route prefix.
     * @param string $prefix
     * @return string
     */
    protected static function normalizeRoutePrefix(string $prefix): string
    {
        $prefix = trim($prefix);
        if ($prefix === '') {
            return '';
        }
        if ($prefix[0] !== '/') {
            $prefix = '/' . $prefix;
        }
        return rtrim($prefix, '/');
    }

    /**
     * Normalize route path.
     * Empty string is allowed (means "use group prefix only").
     * Non-empty path must start with '/'.
     * @param string $path
     * @param string $source
     * @return string
     */
    protected static function normalizeRoutePath(string $path, string $source): string
    {
        $path = trim($path);
        if ($path === '') {
            return '';
        }
        if ($path[0] !== '/') {
            throw new RuntimeException("Annotation route path must start with '/': '$path' ($source)");
        }
        return $path;
    }

    /**
     * Callback to string.
     * @param mixed $callback
     * @return string
     */
    protected static function callbackToString(mixed $callback): string
    {
        if (is_array($callback)) {
            $callback = array_values($callback);
            $class = $callback[0] ?? '';
            $method = $callback[1] ?? '';
            return $class && $method ? ($class . '::' . $method) : json_encode($callback);
        }
        if ($callback instanceof \Closure) {
            return 'Closure';
        }
        if (is_string($callback)) {
            return $callback;
        }
        return get_debug_type($callback);
    }

    /**
     * @return void
     * @deprecated
     */
    public static function container()
    {

    }

}


================================================
FILE: src/Session/FileSessionHandler.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Session;

use Workerman\Protocols\Http\Session\FileSessionHandler as FileHandler;

/**
 * Class FileSessionHandler
 * @package Webman
 */
class FileSessionHandler extends FileHandler
{

}


================================================
FILE: src/Session/RedisClusterSessionHandler.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Session;

use Workerman\Protocols\Http\Session\RedisClusterSessionHandler as RedisClusterHandler;

class RedisClusterSessionHandler extends RedisClusterHandler
{

}


================================================
FILE: src/Session/RedisSessionHandler.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman\Session;

use Workerman\Protocols\Http\Session\RedisSessionHandler as RedisHandler;

/**
 * Class FileSessionHandler
 * @package Webman
 */
class RedisSessionHandler extends RedisHandler
{

}


================================================
FILE: src/Util.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman;

use function array_diff;
use function array_map;
use function scandir;

/**
 * Class Util
 * @package Webman
 */
class Util
{
    /**
     * ScanDir.
     * @param string $basePath
     * @param bool $withBasePath
     * @return array
     */
    public static function scanDir(string $basePath, bool $withBasePath = true): array
    {
        if (!is_dir($basePath)) {
            return [];
        }
        $paths = array_diff(scandir($basePath), array('.', '..')) ?: [];
        return $withBasePath ? array_map(static function ($path) use ($basePath) {
            return $basePath . DIRECTORY_SEPARATOR . $path;
        }, $paths) : $paths;
    }

}


================================================
FILE: src/View.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Webman;

interface View
{
    /**
     * Render.
     * @param string $template
     * @param array $vars
     * @param string|null $app
     * @return string
     */
    public static function render(string $template, array $vars, ?string $app = null): string;
}


================================================
FILE: src/start.php
================================================
#!/usr/bin/env php
<?php
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
support\App::run();


================================================
FILE: src/support/App.php
================================================
<?php

namespace support;

use Dotenv\Dotenv;
use RuntimeException;
use Throwable;
use Webman\Config;
use Webman\Util;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
use function base_path;
use function call_user_func;
use function is_dir;
use function opcache_get_status;
use function opcache_invalidate;
use const DIRECTORY_SEPARATOR;

class App
{
    /**
     * Run.
     * @return void
     * @throws Throwable
     */
    public static function run()
    {
        ini_set('display_errors', 'on');
        error_reporting(E_ALL);

        if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {
            if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {
                Dotenv::createUnsafeImmutable(run_path())->load();
            } else {
                Dotenv::createMutable(run_path())->load();
            }
        }

        if (!$appConfigFile = config_path('app.php')) {
            throw new RuntimeException('Config file not found: app.php');
        }
        $appConfig = require $appConfigFile;
        if ($timezone = $appConfig['default_timezone'] ?? '') {
            date_default_timezone_set($timezone);
        }

        static::loadAllConfig(['route', 'container']);

        if (!is_phar() && DIRECTORY_SEPARATOR === '\\' && empty(config('server.listen'))) {
            echo "Please run 'php windows.php' on windows system." . PHP_EOL;
            exit;
        }

        $errorReporting = config('app.error_reporting');
        if (isset($errorReporting)) {
            error_reporting($errorReporting);
        }

        $runtimeLogsPath = runtime_path() . DIRECTORY_SEPARATOR . 'logs';
        if (!file_exists($runtimeLogsPath) || !is_dir($runtimeLogsPath)) {
            if (!mkdir($runtimeLogsPath, 0777, true)) {
                throw new RuntimeException("Failed to create runtime logs directory. Please check the permission.");
            }
        }

        $runtimeViewsPath = runtime_path() . DIRECTORY_SEPARATOR . 'views';
        if (!file_exists($runtimeViewsPath) || !is_dir($runtimeViewsPath)) {
            if (!mkdir($runtimeViewsPath, 0777, true)) {
                throw new RuntimeException("Failed to create runtime views directory. Please check the permission.");
            }
        }

        Worker::$onMasterReload = function () {
            if (function_exists('opcache_get_status')) {
                if ($status = opcache_get_status()) {
                    if (isset($status['scripts']) && $scripts = $status['scripts']) {
                        foreach (array_keys($scripts) as $file) {
                            opcache_invalidate($file, true);
                        }
                    }
                }
            }
        };

        $config = config('server');
        Worker::$pidFile = $config['pid_file'];
        Worker::$stdoutFile = $config['stdout_file'];
        Worker::$logFile = $config['log_file'];
        Worker::$eventLoopClass = $config['event_loop'] ?? '';
        TcpConnection::$defaultMaxPackageSize = $config['max_package_size'] ?? 10 * 1024 * 1024;
        if (property_exists(Worker::class, 'statusFile')) {
            Worker::$statusFile = $config['status_file'] ?? '';
        }
        if (property_exists(Worker::class, 'stopTimeout')) {
            Worker::$stopTimeout = $config['stop_timeout'] ?? 2;
        }

        if ($config['listen'] ?? false) {
            $worker = new Worker($config['listen'], $config['context'] ?? []);
            $propertyMap = [
                'name',
                'count',
                'user',
                'group',
                'reusePort',
                'transport',
                'protocol'
            ];
            foreach ($propertyMap as $property) {
                if (isset($config[$property])) {
                    $worker->$property = $config[$property];
                }
            }

            $worker->onWorkerStart = function ($worker) {
                require_once base_path() . '/support/bootstrap.php';
                $app = new \Webman\App(config('app.request_class', Request::class), Log::channel('default'), app_path(), public_path());
                $worker->onMessage = [$app, 'onMessage'];
                call_user_func([$app, 'onWorkerStart'], $worker);
            };
        }

        $windowsWithoutServerListen = is_phar() && DIRECTORY_SEPARATOR === '\\' && empty($config['listen']);
        $process = config('process', []);
        if ($windowsWithoutServerListen && $process) {
            $processName = isset($process['webman']) ? 'webman' : key($process);
            worker_start($processName, $process[$processName]);
        } else if (DIRECTORY_SEPARATOR === '/') {
            foreach (config('process', []) as $processName => $config) {
                worker_start($processName, $config);
            }
            foreach (config('plugin', []) as $firm => $projects) {
                foreach ($projects as $name => $project) {
                    if (!is_array($project)) {
                        continue;
                    }
                    foreach ($project['process'] ?? [] as $processName => $config) {
                        worker_start("plugin.$firm.$name.$processName", $config);
                    }
                }
                foreach ($projects['process'] ?? [] as $processName => $config) {
                    worker_start("plugin.$firm.$processName", $config);
                }
            }
        }

        Worker::runAll();
    }

    /**
     * LoadAllConfig.
     * @param array $excludes
     * @return void
     */
    public static function loadAllConfig(array $excludes = [])
    {
        Config::load(config_path(), $excludes);
        $directory = base_path() . '/plugin';
        foreach (Util::scanDir($directory, false) as $name) {
            $dir = "$directory/$name/config";
            if (is_dir($dir)) {
                Config::load($dir, $excludes, "plugin.$name");
            }
        }
    }

}


================================================
FILE: src/support/Container.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace support;

use Webman\Config;

/**
 * Class Container
 * @package support
 * @method static mixed get($name)
 * @method static mixed make($name, array $parameters)
 * @method static bool has($name)
 */
class Container
{
    /**
     * Instance
     * @param string $plugin
     * @return array|mixed|void|null
     */
    public static function instance(string $plugin = '')
    {
        return Config::get($plugin ? "plugin.$plugin.container" : 'container');
    }

    /**
     * @param string $name
     * @param array $arguments
     * @return mixed
     */
    public static function __callStatic(string $name, array $arguments)
    {
        return static::instance()->{$name}(... $arguments);
    }
}


================================================
FILE: src/support/Context.php
================================================
<?php

/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace support;

/**
 * Class Context
 * @package Webman
 */
class Context extends \Webman\Context
{

}


================================================
FILE: src/support/Log.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace support;

use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\FormattableHandlerInterface;
use Monolog\Handler\HandlerInterface;
use Monolog\Logger;
use function array_values;
use function config;
use function is_array;

/**
 * Class Log
 * @package support
 *
 * @method static void log($level, $message, array $context = [])
 * @method static void debug($message, array $context = [])
 * @method static void info($message, array $context = [])
 * @method static void notice($message, array $context = [])
 * @method static void warning($message, array $context = [])
 * @method static void error($message, array $context = [])
 * @method static void critical($message, array $context = [])
 * @method static void alert($message, array $context = [])
 * @method static void emergency($message, array $context = [])
 */
class Log
{
    /**
     * @var array
     */
    protected static $instance = [];

    /**
     * Channel.
     * @param string $name
     * @return Logger
     */
    public static function channel(string $name = 'default'): Logger
    {
        if (!isset(static::$instance[$name])) {
            $config = config('log', [])[$name];
            $handlers = self::handlers($config);
            $processors = self::processors($config);
            $logger = new Logger($name, $handlers, $processors);
            if (method_exists($logger, 'useLoggingLoopDetection')) {
                $logger->useLoggingLoopDetection(false);
            }
            static::$instance[$name] = $logger;
        }
        return static::$instance[$name];
    }

    /**
     * Handlers.
     * @param array $config
     * @return array
     */
    protected static function handlers(array $config): array
    {
        $handlerConfigs = $config['handlers'] ?? [[]];
        $handlers = [];
        foreach ($handlerConfigs as $value) {
            $class = $value['class'] ?? [];
            $constructor = $value['constructor'] ?? [];

            $formatterConfig = $value['formatter'] ?? [];

            $class && $handlers[] = self::handler($class, $constructor, $formatterConfig);
        }

        return $handlers;
    }

    /**
     * Handler.
     * @param string $class
     * @param array $constructor
     * @param array $formatterConfig
     * @return HandlerInterface
     */
    protected static function handler(string $class, array $constructor, array $formatterConfig): HandlerInterface
    {
        /** @var HandlerInterface $handler */
        $handler = new $class(... array_values($constructor));

        if ($handler instanceof FormattableHandlerInterface && $formatterConfig) {
            $formatterClass = $formatterConfig['class'];
            $formatterConstructor = $formatterConfig['constructor'];

            /** @var FormatterInterface $formatter */
            $formatter = new $formatterClass(... array_values($formatterConstructor));

            $handler->setFormatter($formatter);
        }

        return $handler;
    }

    /**
     * Processors.
     * @param array $config
     * @return array
     */
    protected static function processors(array $config): array
    {
        $result = [];
        if (!isset($config['processors']) && isset($config['processor'])) {
            $config['processors'] = [$config['processor']];
        }

        foreach ($config['processors'] ?? [] as $value) {
            if (is_array($value) && isset($value['class'])) {
                $value = new $value['class'](... array_values($value['constructor'] ?? []));
            }
            $result[] = $value;
        }

        return $result;
    }

    /**
     * @param string $name
     * @param array $arguments
     * @return mixed
     */
    public static function __callStatic(string $name, array $arguments)
    {
        return static::channel()->{$name}(... $arguments);
    }
}


================================================
FILE: src/support/Plugin.php
================================================
<?php

namespace support;

use function defined;
use function is_callable;
use function is_file;
use function method_exists;

class Plugin
{
    /**
     * Install.
     * @param mixed $event
     * @return void
     */
    public static function install($event)
    {
        static::findHelper();
        $psr4 = static::getPsr4($event);
        foreach ($psr4 as $namespace => $path) {
            $pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
            if (!defined($pluginConst)) {
                continue;
            }
            $installFunction = "\\{$namespace}Install::install";
            if (is_callable($installFunction)) {
                $installFunction(true);
            }
        }
    }

    /**
     * Update.
     * @param mixed $event
     * @return void
     */
    public static function update($event)
    {
        static::findHelper();
        $psr4 = static::getPsr4($event);
        foreach ($psr4 as $namespace => $path) {
            $pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
            if (!defined($pluginConst)) {
                continue;
            }
            $updateFunction = "\\{$namespace}Install::update";
            if (is_callable($updateFunction)) {
                $updateFunction();
                continue;
            }
            $installFunction = "\\{$namespace}Install::install";
            if (is_callable($installFunction)) {
                $installFunction(false);
            }
        }
    }

    /**
     * Uninstall.
     * @param mixed $event
     * @return void
     */
    public static function uninstall($event)
    {
        static::findHelper();
        $psr4 = static::getPsr4($event);
        foreach ($psr4 as $namespace => $path) {
            $pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
            if (!defined($pluginConst)) {
                continue;
            }
            $uninstallFunction = "\\{$namespace}Install::uninstall";
            if (is_callable($uninstallFunction)) {
                $uninstallFunction();
            }
        }
    }

    /**
     * Get psr-4 info
     *
     * @param mixed $event
     * @return array
     */
    protected static function getPsr4($event)
    {
        $operation = $event->getOperation();
        $autoload = method_exists($operation, 'getPackage') ? $operation->getPackage()->getAutoload() : $operation->getTargetPackage()->getAutoload();
        return $autoload['psr-4'] ?? [];
    }

    /**
     * FindHelper.
     * @return void
     */
    protected static function findHelper()
    {
        // Plugin.php in webman
        require_once __DIR__ . '/helpers.php';
    }
}


================================================
FILE: src/support/Request.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace support;

/**
 * Class Request
 * @package support
 */
class Request extends \Webman\Http\Request
{

}

================================================
FILE: src/support/Response.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace support;

/**
 * Class Response
 * @package support
 */
class Response extends \Webman\Http\Response
{

}

================================================
FILE: src/support/Translation.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace support;

use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use Symfony\Component\Translation\Translator;
use Webman\Exception\NotFoundException;
use function basename;
use function config;
use function get_realpath;
use function pathinfo;
use function request;
use function substr;

/**
 * Class Translation
 * @package support
 * @method static string trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
 * @method static void setLocale(string $locale)
 * @method static string getLocale()
 */
class Translation
{

    /**
     * @var Translator[]
     */
    protected static $instance = [];

    /**
     * Instance.
     * @param string $plugin
     * @param array|null $config
     * @return Translator
     * @throws NotFoundException
     */
    public static function instance(string $plugin = '', ?array $config = null): Translator
    {
        if (!isset(static::$instance[$plugin])) {
            $config = $config ?? config($plugin ? "plugin.$plugin.translation" : 'translation', []);
            $paths = (array)($config['path'] ?? []);

            static::$instance[$plugin] = $translator = new Translator($config['locale']);
            $translator->setFallbackLocales($config['fallback_locale']);

            $classes = $config['loader'] ?? [
                'Symfony\Component\Translation\Loader\PhpFileLoader' => [
                    'extension' => '.php',
                    'format' => 'phpfile'
                ],
                'Symfony\Component\Translation\Loader\PoFileLoader' => [
                    'extension' => '.po',
                    'format' => 'pofile'
                ]
            ];
            foreach ($paths as $path) {
                // Phar support. Compatible with the 'realpath' function in the phar file.
                if (!$translationsPath = get_realpath($path)) {
                    continue;
                }

                foreach ($classes as $class => $opts) {
                    $translator->addLoader($opts['format'], new $class);
                    $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($translationsPath, FilesystemIterator::SKIP_DOTS));
                    $files = new RegexIterator($iterator, '/^.+' . preg_quote($opts['extension']) . '$/i', RegexIterator::GET_MATCH);
                    foreach ($files as $file) {
                        $file = $file[0];
                        $domain = basename($file, $opts['extension']);
                        $dirName = pathinfo($file, PATHINFO_DIRNAME);
                        $locale = substr(strrchr($dirName, DIRECTORY_SEPARATOR), 1);
                        if ($domain && $locale) {
                            $translator->addResource($opts['format'], $file, $locale, $domain);
                        }
                    }
                }
            }
        }
        return static::$instance[$plugin];
    }

    /**
     * @param string $name
     * @param array $arguments
     * @return mixed
     * @throws NotFoundException
     */
    public static function __callStatic(string $name, array $arguments)
    {
        $request = request();
        $plugin = $request->plugin ?? '';
        return static::instance($plugin)->{$name}(... $arguments);
    }
}


================================================
FILE: src/support/View.php
================================================
<?php
/**
 * This file is part of webman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace support;

use function config;
use function request;

class View
{
    /**
     * Assign.
     * @param mixed $name
     * @param mixed $value
     * @return void
     */
    public static function assign($name, mixed $value = null)
    {
        $request = request();
        $plugin = $request->plugin ?? '';
        $handler = config($plugin ? "plugin.$plugin.view.handler" : 'view.handler');
        $handler::assign($name, $value);
    }
}

================================================
FILE: src/support/annotation/DisableDefaultRoute.php
================================================
<?php

namespace support\annotation;

use Attribute;

/**
 * @deprecated Use support\annotation\route\DisableDefaultRoute instead.
 */
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute extends \support\annotation\route\DisableDefaultRoute
{
}


================================================
FILE: src/support/annotation/Middleware.php
================================================
<?php

namespace support\annotation;

use Attribute;

/**
 * Attach middlewares to routes/controllers/functions via attributes.
 *
 * Example:
 *   #[Middleware(AuthMiddleware::class, RateLimitMiddleware::class)]
 */
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class Middleware
{
    /**
     * @var array
     */
    protected array $middlewares = [];

    /**
     * @param mixed ...$middlewares Middleware class names.
     */
    public function __construct(...$middlewares)
    {
        $this->middlewares = $middlewares;
    }

    /**
     * Convert to webman middleware callable format: [MiddlewareClass, 'process'].
     * @return array
     */
    public function getMiddlewares(): array
    {
        $middlewares = [];
        foreach ($this->middlewares as $middleware) {
            $middlewares[] = [$middleware, 'process'];
        }
        return $middlewares;
    }
}

================================================
FILE: src/support/annotation/route/Any.php
================================================
<?php

namespace support\annotation\route;

use Attribute;

/**
 * Shortcut for #[Route(methods: ['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS'], ...)].
 */
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Any extends Route
{
    /**
     * @param string|null $path Route path. Null means default-route method restriction only.
     * @param string|null $name Route name
     */
    public function __construct(?string $path = null, ?string $name = null)
    {
        parent::__construct($path, ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], $name);
    }
}



================================================
FILE: src/support/annotation/route/Delete.php
================================================
<?php

namespace support\annotation\route;

use Attribute;

/**
 * Shortcut for #[Route(methods: 'DELETE', ...)].
 */
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Delete extends Route
{
    /**
     * @param string|null $path Route path. Null means default-route method restriction only.
     * @param string|null $name Route name
     */
    public function __construct(?string $path = null, ?string $name = null)
    {
        parent::__construct($path, 'DELETE', $name);
    }
}



================================================
FILE: src/support/annotation/route/DisableDefaultRoute.php
================================================
<?php

namespace support\annotation\route;

use Attribute;

/**
 * Disable webman's default route mapping for a controller or action.
 */
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute
{
}

================================================
FILE: src/support/annotation/route/Get.php
================================================
<?php

namespace support\annotation\route;

use Attribute;

/**
 * Shortcut for #[Route(methods: 'GET', ...)].
 */
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Get extends Route
{
    /**
     * @param string|null $path Route path. Null means default-route method restriction only.
     * @param string|null $name Route name
     */
    public function __construct(?string $path = null, ?string $name = null)
    {
        parent::__construct($path, 'GET', $name);
    }
}



================================================
FILE: src/support/annotation/route/Head.php
================================================
<?php

namespace support\annotation\route;

use Attribute;

/**
 * Shortcut for #[Route(methods: 'HEAD', ...)].
 */
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Head extends Route
{
    /**
     * @param string|null $path Route path. Null means default-route method restriction only.
     * @param string|null $name Route name
     */
    public function __construct(?string $path = null, ?string $name = null)
    {
        parent::__construct($path, 'HEAD', $name);
    }
}



================================================
FILE: src/support/annotation/route/Options.php
================================================
<?php

namespace support\annotation\route;

use Attribute;

/**
 * Shortcut for #[Route(methods: 'OPTIONS', ...)].
 */
#[Attribute(Attribute::
Download .txt
gitextract_e73a02zd/

├── .gitignore
├── README.md
├── composer.json
└── src/
    ├── App.php
    ├── Bootstrap.php
    ├── Config.php
    ├── Container.php
    ├── Context.php
    ├── Exception/
    │   ├── BusinessException.php
    │   ├── ExceptionHandler.php
    │   ├── ExceptionHandlerInterface.php
    │   ├── FileException.php
    │   └── NotFoundException.php
    ├── File.php
    ├── Finder/
    │   ├── ControllerFinder.php
    │   ├── FileInfo.php
    │   └── Finder.php
    ├── Http/
    │   ├── Request.php
    │   ├── Response.php
    │   └── UploadFile.php
    ├── Install.php
    ├── Middleware.php
    ├── MiddlewareInterface.php
    ├── Route/
    │   └── Route.php
    ├── Route.php
    ├── Session/
    │   ├── FileSessionHandler.php
    │   ├── RedisClusterSessionHandler.php
    │   └── RedisSessionHandler.php
    ├── Util.php
    ├── View.php
    ├── start.php
    ├── support/
    │   ├── App.php
    │   ├── Container.php
    │   ├── Context.php
    │   ├── Log.php
    │   ├── Plugin.php
    │   ├── Request.php
    │   ├── Response.php
    │   ├── Translation.php
    │   ├── View.php
    │   ├── annotation/
    │   │   ├── DisableDefaultRoute.php
    │   │   ├── Middleware.php
    │   │   └── route/
    │   │       ├── Any.php
    │   │       ├── Delete.php
    │   │       ├── DisableDefaultRoute.php
    │   │       ├── Get.php
    │   │       ├── Head.php
    │   │       ├── Options.php
    │   │       ├── Patch.php
    │   │       ├── Post.php
    │   │       ├── Put.php
    │   │       ├── Route.php
    │   │       └── RouteGroup.php
    │   ├── bootstrap/
    │   │   └── Session.php
    │   ├── bootstrap.php
    │   ├── exception/
    │   │   ├── BusinessException.php
    │   │   ├── Handler.php
    │   │   ├── InputTypeException.php
    │   │   ├── InputValueException.php
    │   │   ├── MissingInputException.php
    │   │   ├── NotFoundException.php
    │   │   └── PageNotFoundException.php
    │   ├── helpers.php
    │   └── view/
    │       ├── Blade.php
    │       ├── Raw.php
    │       ├── ThinkPHP.php
    │       └── Twig.php
    └── windows.php
Download .txt
SYMBOL INDEX (352 symbols across 63 files)

FILE: src/App.php
  class App (line 86) | class App
    method __construct (line 136) | public function __construct(string $requestClass, LoggerInterface $log...
    method onMessage (line 151) | public function onMessage($connection, $request)
    method defaultRouteMethodNotAllowedResponse (line 214) | protected static function defaultRouteMethodNotAllowedResponse(string ...
    method onWorkerStart (line 263) | public function onWorkerStart($worker)
    method collectCallbacks (line 275) | protected static function collectCallbacks(string $key, array $data)
    method unsafeUri (line 290) | protected static function unsafeUri(TcpConnection $connection, string ...
    method containsPathTraversal (line 306) | protected static function containsPathTraversal(string $path): bool
    method getFallback (line 323) | protected static function getFallback(string $plugin = '', int $status...
    method exceptionResponse (line 337) | protected static function exceptionResponse(Throwable $e, $request): R...
    method getCallback (line 381) | public static function getCallback(string $plugin, string $app, $call,...
    method resolveInject (line 495) | protected static function resolveInject(ContainerInterface $container,...
    method isNeedInject (line 516) | protected static function isNeedInject($call, array &$args): bool
    method getReflector (line 598) | protected static function getReflector($call)
    method getReflectorCacheKey (line 626) | protected static function getReflectorCacheKey($call): ?string
    method resolveMethodDependencies (line 649) | protected static function resolveMethodDependencies(ContainerInterface...
    method resolveMethodDependenciesFromMetadata (line 665) | protected static function resolveMethodDependenciesFromMetadata(Contai...
    method getMethodParameterMetadata (line 771) | protected static function getMethodParameterMetadata(ReflectionFunctio...
    method getParameterMetadataCacheKey (line 852) | protected static function getParameterMetadataCacheKey(ReflectionFunct...
    method container (line 871) | public static function container(string $plugin = '')
    method request (line 880) | public static function request()
    method worker (line 889) | public static function worker(): ?Worker
    method findRoute (line 906) | protected static function findRoute(TcpConnection $connection, string ...
    method findFile (line 949) | protected static function findFile(TcpConnection $connection, string $...
    method send (line 1008) | protected static function send($connection, $response, $request)
    method parseControllerAction (line 1036) | protected static function parseControllerAction(string $path)
    method guessControllerAction (line 1082) | protected static function guessControllerAction($pathExplode, $action,...
    method getControllerAction (line 1113) | protected static function getControllerAction(string $controllerClass,...
    method getController (line 1136) | protected static function getController(string $controllerClass)
    method getAction (line 1182) | protected static function getAction(string $controllerClass, string $a...
    method getPluginByClass (line 1212) | public static function getPluginByClass(string $controllerClass)
    method getPluginByPath (line 1227) | public static function getPluginByPath(string $path)
    method getAppByController (line 1246) | protected static function getAppByController(string $controllerClass)
    method execPhpFile (line 1262) | public static function execPhpFile(string $file)
    method getRealMethod (line 1281) | protected static function getRealMethod(string $class, string $method)...
    method config (line 1300) | protected static function config(string $plugin, string $key, mixed $d...
    method stringify (line 1310) | protected static function stringify($data): string

FILE: src/Bootstrap.php
  type Bootstrap (line 19) | interface Bootstrap
    method start (line 27) | public static function start(?Worker $worker);

FILE: src/Config.php
  class Config (line 31) | class Config
    method load (line 61) | public static function load(string $configPath, array $excludeFile = [...
    method reload (line 91) | public static function reload(string $configPath, array $excludeFile =...
    method clear (line 100) | public static function clear()
    method formatConfig (line 110) | protected static function formatConfig()
    method loadFromDir (line 201) | public static function loadFromDir(string $configPath, array $excludeF...
    method get (line 240) | public static function get(?string $key = null, mixed $default = null)
    method read (line 279) | protected static function read(string $key, mixed $default = null)
    method find (line 306) | protected static function find(array $keyArray, $stack, $default)

FILE: src/Container.php
  class Container (line 14) | class Container implements ContainerInterface
    method get (line 32) | public function get(string $name)
    method has (line 52) | public function has(string $name): bool
    method make (line 65) | public function make(string $name, array $constructor = [])
    method addDefinitions (line 78) | public function addDefinitions(array $definitions): Container

FILE: src/Context.php
  class Context (line 13) | class Context extends WorkermanContext
    method onDestroy (line 15) | public static function onDestroy(Closure $closure): void

FILE: src/Exception/BusinessException.php
  class BusinessException (line 27) | class BusinessException extends RuntimeException
    method render (line 45) | public function render(Request $request): ?Response
    method data (line 61) | public function data(?array $data = null): array|static
    method debug (line 75) | public function debug(?bool $value = null): bool|static
    method getData (line 88) | public function getData(): array
    method trans (line 101) | protected function trans(string $message, array $parameters = [], ?str...

FILE: src/Exception/ExceptionHandler.php
  class ExceptionHandler (line 29) | class ExceptionHandler implements ExceptionHandlerInterface
    method __construct (line 51) | public function __construct($logger, $debug)
    method report (line 61) | public function report(Throwable $exception)
    method render (line 78) | public function render(Request $request, Throwable $exception): Response
    method shouldntReport (line 98) | protected function shouldntReport(Throwable $e): bool
    method __get (line 114) | public function __get(string $name)

FILE: src/Exception/ExceptionHandlerInterface.php
  type ExceptionHandlerInterface (line 21) | interface ExceptionHandlerInterface
    method report (line 27) | public function report(Throwable $exception);
    method render (line 34) | public function render(Request $request, Throwable $exception): Response;

FILE: src/Exception/FileException.php
  class FileException (line 23) | class FileException extends RuntimeException

FILE: src/Exception/NotFoundException.php
  class NotFoundException (line 23) | class NotFoundException extends \Exception implements NotFoundExceptionI...

FILE: src/File.php
  class File (line 29) | class File extends SplFileInfo
    method move (line 37) | public function move(string $destination): File

FILE: src/Finder/ControllerFinder.php
  class ControllerFinder (line 38) | class ControllerFinder
    method files (line 46) | public static function files(?string $scope = null): array
    method resolveRoots (line 72) | protected static function resolveRoots(?string $scope): array
    method mainAppRoots (line 102) | protected static function mainAppRoots(): array
    method allPluginRoots (line 122) | protected static function allPluginRoots(): array
    method singlePluginRoots (line 171) | protected static function singlePluginRoots(string $plugin): array
    method findControllerFiles (line 208) | protected static function findControllerFiles(string $rootDir, string ...
    method isValidIdentifier (line 230) | protected static function isValidIdentifier(string $name): bool

FILE: src/Finder/FileInfo.php
  class FileInfo (line 23) | class FileInfo extends File
    method __construct (line 42) | public function __construct(string $path, array $meta = [], string $ro...
    method meta (line 53) | public function meta(): array
    method class (line 64) | public function class(): ?string
    method className (line 79) | public function className(): ?string
    method namespace (line 95) | public function namespace(): ?string
    method setMeta (line 110) | public function setMeta(array $meta): static
    method rootDir (line 120) | public function rootDir(): string
    method relativePathname (line 129) | public function relativePathname(): string

FILE: src/Finder/Finder.php
  class Finder (line 27) | class Finder
    method in (line 94) | public static function in(string|array $dirs): static
    method create (line 110) | public static function create(): static
    method setCacheDir (line 120) | public static function setCacheDir(string $dir): void
    method getCacheDir (line 129) | protected static function getCacheDir(): string
    method files (line 144) | public function files(): static
    method name (line 155) | public function name(string|array $patterns): static
    method path (line 166) | public function path(string|array $patterns): static
    method exclude (line 177) | public function exclude(string|array $dirs): static
    method excludeDirs (line 188) | public function excludeDirs(array $dirs): static
    method withPhpMeta (line 198) | public function withPhpMeta(): static
    method phpFiltersRequested (line 208) | protected function phpFiltersRequested(): bool
    method hasAttributes (line 220) | public function hasAttributes(bool $value): static
    method typeIn (line 233) | public function typeIn(array $types): static
    method psr4 (line 246) | public function psr4(bool $value): static
    method find (line 258) | public function find(): array
    method findPaths (line 320) | public function findPaths(): array
    method scanDirectory (line 330) | protected function scanDirectory(string $dir): array
    method matchesName (line 376) | protected function matchesName(string $filePath): bool
    method matchesPath (line 397) | protected function matchesPath(string $filePath, string $rootDir): bool
    method matchPattern (line 423) | protected function matchPattern(string $value, string $pattern): bool
    method isRegex (line 439) | protected function isRegex(string $pattern): bool
    method isPhpFile (line 456) | protected function isPhpFile(string $filePath): bool
    method getPhpMeta (line 467) | protected function getPhpMeta(string $filePath, string $rootDir): array
    method ensurePsr4Cached (line 498) | protected function ensurePsr4Cached(string $filePath, string $rootDir,...
    method computePhpMeta (line 520) | protected function computePhpMeta(string $filePath, int|false $mtime):...
    method parsePhpFile (line 550) | protected function parsePhpFile(string $code): array
    method matchesPhpFilters (line 659) | protected function matchesPhpFilters(array $meta, string $filePath, st...
    method checkPsr4 (line 690) | protected function checkPsr4(string $filePath, string $rootDir, ?strin...
    method classFromFile (line 720) | protected function classFromFile(string $filePath, string $rootDir, st...
    method isValidPsr4ClassPath (line 750) | protected function isValidPsr4ClassPath(string $relativeClassPath): bool
    method getRelativePath (line 761) | protected function getRelativePath(string $filePath, string $rootDir):...
    method getCacheKey (line 779) | protected function getCacheKey(string $rootDir): string
    method getCacheFile (line 789) | protected function getCacheFile(string $rootDir): string
    method loadCache (line 800) | protected function loadCache(string $rootDir): void
    method saveCache (line 828) | protected function saveCache(string $rootDir): void
    method clearCache (line 886) | public static function clearCache(): void
    method normalizePath (line 897) | protected static function normalizePath(string $path): string

FILE: src/Http/Request.php
  class Request (line 32) | class Request extends \Workerman\Protocols\Http\Request
    method all (line 67) | public function all()
    method input (line 78) | public function input(string $name, mixed $default = null)
    method only (line 88) | public function only(array $keys): array
    method except (line 105) | public function except(array $keys)
    method file (line 119) | public function file(?string $name = null): array|null|UploadFile
    method parseFile (line 149) | protected function parseFile(array $file): UploadFile
    method parseFiles (line 159) | protected function parseFiles(array $files): array
    method getRemoteIp (line 176) | public function getRemoteIp(): string
    method getRemotePort (line 185) | public function getRemotePort(): int
    method getLocalIp (line 194) | public function getLocalIp(): string
    method getLocalPort (line 203) | public function getLocalPort(): int
    method getRealIp (line 213) | public function getRealIp(bool $safeMode = true): string
    method url (line 235) | public function url(): string
    method fullUrl (line 244) | public function fullUrl(): string
    method isAjax (line 253) | public function isAjax(): bool
    method isGet (line 262) | public function isGet(): bool
    method isPost (line 272) | public function isPost(): bool
    method isPjax (line 282) | public function isPjax(): bool
    method expectsJson (line 291) | public function expectsJson(): bool
    method acceptJson (line 300) | public function acceptJson(): bool
    method isIntranetIp (line 310) | public static function isIntranetIp(string $ip): bool
    method setGet (line 350) | public function setGet(array|string $input, mixed $value = null): Request
    method setPost (line 368) | public function setPost(array|string $input, mixed $value = null): Req...
    method setHeader (line 386) | public function setHeader(array|string $input, mixed $value = null): R...
    method destroy (line 401) | public function destroy(): void

FILE: src/Http/Response.php
  class Response (line 26) | class Response extends \Workerman\Protocols\Http\Response
    method file (line 38) | public function file(string $file): Response
    method download (line 52) | public function download(string $file, string $downloadName = ''): Res...
    method notModifiedSince (line 68) | protected function notModifiedSince(string $file): bool
    method exception (line 82) | public function exception(?Throwable $exception = null): ?Throwable

FILE: src/Http/UploadFile.php
  class UploadFile (line 24) | class UploadFile extends File
    method __construct (line 49) | public function __construct(string $fileName, string $uploadName, stri...
    method getUploadName (line 61) | public function getUploadName(): ?string
    method getUploadMimeType (line 70) | public function getUploadMimeType(): ?string
    method getUploadExtension (line 79) | public function getUploadExtension(): string
    method getUploadErrorCode (line 88) | public function getUploadErrorCode(): ?int
    method isValid (line 97) | public function isValid(): bool
    method getUploadMineType (line 107) | public function getUploadMineType(): ?string

FILE: src/Install.php
  class Install (line 5) | class Install
    method install (line 21) | public static function install()
    method uninstall (line 30) | public static function uninstall()
    method installByRelation (line 39) | public static function installByRelation()

FILE: src/Middleware.php
  class Middleware (line 30) | class Middleware
    method load (line 48) | public static function load($allMiddlewares, string $plugin = '')
    method getMiddleware (line 85) | public static function getMiddleware(string $plugin, string $appName, ...
    method prepareAttributeMiddlewares (line 142) | private static function prepareAttributeMiddlewares(array &$middleware...
    method container (line 158) | public static function container($_)

FILE: src/MiddlewareInterface.php
  type MiddlewareInterface (line 20) | interface MiddlewareInterface
    method process (line 29) | public function process(Request $request, callable $handler): Response;

FILE: src/Route.php
  class Route (line 55) | class Route
    method get (line 139) | public static function get(string $path, $callback): RouteObject
    method post (line 150) | public static function post(string $path, $callback): RouteObject
    method put (line 161) | public static function put(string $path, $callback): RouteObject
    method patch (line 172) | public static function patch(string $path, $callback): RouteObject
    method delete (line 183) | public static function delete(string $path, $callback): RouteObject
    method head (line 193) | public static function head(string $path, $callback): RouteObject
    method options (line 204) | public static function options(string $path, $callback): RouteObject
    method any (line 215) | public static function any(string $path, $callback): RouteObject
    method add (line 227) | public static function add($method, string $path, $callback): RouteObject
    method group (line 238) | public static function group($path, ?callable $callback = null): Route
    method resource (line 264) | public static function resource(string $name, string $controller, arra...
    method getRoutes (line 302) | public static function getRoutes(): array
    method disableDefaultRoute (line 313) | public static function disableDefaultRoute(array|string $plugin = '', ...
    method isDefaultRouteDisabled (line 350) | public static function isDefaultRouteDisabled(array|string $plugin = '...
    method isDefaultRouteDisabledByAnnotation (line 378) | protected static function isDefaultRouteDisabledByAnnotation(string $c...
    method isRefHasDefaultRouteDisabledAnnotation (line 400) | protected static function isRefHasDefaultRouteDisabledAnnotation(Refle...
    method middleware (line 420) | public function middleware($middleware): Route
    method collect (line 435) | public function collect(RouteObject $route)
    method setByName (line 445) | public static function setByName(string $name, RouteObject $instance)
    method getByName (line 455) | public static function getByName(string $name): ?RouteObject
    method addChild (line 465) | public function addChild(Route $route)
    method getChildren (line 474) | public function getChildren()
    method dispatch (line 485) | public static function dispatch(string $method, string $path): array
    method convertToCallable (line 496) | public static function convertToCallable(string $path, $callback)
    method addRoute (line 526) | protected static function addRoute($methods, string $path, $callback):...
    method load (line 558) | public static function load($paths)
    method setCollector (line 611) | public static function setCollector(RouteCollector $route)
    method fallback (line 622) | public static function fallback(callable $callback, string $plugin = '')
    method getFallback (line 638) | public static function getFallback(string $plugin = '', int $status = ...
    method loadAnnotationRoutes (line 652) | protected static function loadAnnotationRoutes(): void
    method buildAnnotationRouteDefinitions (line 668) | protected static function buildAnnotationRouteDefinitions(array $contr...
    method collectMiddlewaresFromAttributes (line 750) | protected static function collectMiddlewaresFromAttributes(array $attr...
    method registerAnnotationRouteDefinitions (line 774) | protected static function registerAnnotationRouteDefinitions(array $de...
    method normalizeRoutePrefix (line 794) | protected static function normalizeRoutePrefix(string $prefix): string
    method normalizeRoutePath (line 814) | protected static function normalizeRoutePath(string $path, string $sou...
    method callbackToString (line 831) | protected static function callbackToString(mixed $callback): string
    method container (line 852) | public static function container()

FILE: src/Route/Route.php
  class Route (line 27) | class Route
    method __construct (line 65) | public function __construct($methods, string $path, $callback)
    method getName (line 76) | public function getName(): ?string
    method name (line 86) | public function name(string $name): Route
    method middleware (line 98) | public function middleware(mixed $middleware = null)
    method getPath (line 111) | public function getPath(): string
    method getMethods (line 120) | public function getMethods(): array
    method getCallback (line 129) | public function getCallback()
    method getMiddleware (line 138) | public function getMiddleware(): array
    method param (line 149) | public function param(?string $name = null, mixed $default = null)
    method setParams (line 162) | public function setParams(array $params): Route
    method url (line 173) | public function url(array $parameters = []): string

FILE: src/Session/FileSessionHandler.php
  class FileSessionHandler (line 23) | class FileSessionHandler extends FileHandler

FILE: src/Session/RedisClusterSessionHandler.php
  class RedisClusterSessionHandler (line 19) | class RedisClusterSessionHandler extends RedisClusterHandler

FILE: src/Session/RedisSessionHandler.php
  class RedisSessionHandler (line 23) | class RedisSessionHandler extends RedisHandler

FILE: src/Util.php
  class Util (line 25) | class Util
    method scanDir (line 33) | public static function scanDir(string $basePath, bool $withBasePath = ...

FILE: src/View.php
  type View (line 17) | interface View
    method render (line 26) | public static function render(string $template, array $vars, ?string $...

FILE: src/support/App.php
  class App (line 19) | class App
    method run (line 26) | public static function run()
    method loadAllConfig (line 155) | public static function loadAllConfig(array $excludes = [])

FILE: src/support/Container.php
  class Container (line 26) | class Container
    method instance (line 33) | public static function instance(string $plugin = '')
    method __callStatic (line 43) | public static function __callStatic(string $name, array $arguments)

FILE: src/support/Context.php
  class Context (line 22) | class Context extends \Webman\Context

FILE: src/support/Log.php
  class Log (line 39) | class Log
    method channel (line 51) | public static function channel(string $name = 'default'): Logger
    method handlers (line 71) | protected static function handlers(array $config): array
    method handler (line 94) | protected static function handler(string $class, array $constructor, a...
    method processors (line 117) | protected static function processors(array $config): array
    method __callStatic (line 139) | public static function __callStatic(string $name, array $arguments)

FILE: src/support/Plugin.php
  class Plugin (line 10) | class Plugin
    method install (line 17) | public static function install($event)
    method update (line 38) | public static function update($event)
    method uninstall (line 64) | public static function uninstall($event)
    method getPsr4 (line 86) | protected static function getPsr4($event)
    method findHelper (line 97) | protected static function findHelper()

FILE: src/support/Request.php
  class Request (line 21) | class Request extends \Webman\Http\Request

FILE: src/support/Response.php
  class Response (line 21) | class Response extends \Webman\Http\Response

FILE: src/support/Translation.php
  class Translation (line 37) | class Translation
    method instance (line 52) | public static function instance(string $plugin = '', ?array $config = ...
    method __callStatic (line 102) | public static function __callStatic(string $name, array $arguments)

FILE: src/support/View.php
  class View (line 20) | class View
    method assign (line 28) | public static function assign($name, mixed $value = null)

FILE: src/support/annotation/DisableDefaultRoute.php
  class DisableDefaultRoute (line 10) | #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]

FILE: src/support/annotation/Middleware.php
  class Middleware (line 13) | #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribu...
    method __construct (line 24) | public function __construct(...$middlewares)
    method getMiddlewares (line 33) | public function getMiddlewares(): array

FILE: src/support/annotation/route/Any.php
  class Any (line 10) | #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
    method __construct (line 17) | public function __construct(?string $path = null, ?string $name = null)

FILE: src/support/annotation/route/Delete.php
  class Delete (line 10) | #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
    method __construct (line 17) | public function __construct(?string $path = null, ?string $name = null)

FILE: src/support/annotation/route/DisableDefaultRoute.php
  class DisableDefaultRoute (line 10) | #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]

FILE: src/support/annotation/route/Get.php
  class Get (line 10) | #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
    method __construct (line 17) | public function __construct(?string $path = null, ?string $name = null)

FILE: src/support/annotation/route/Head.php
  class Head (line 10) | #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
    method __construct (line 17) | public function __construct(?string $path = null, ?string $name = null)

FILE: src/support/annotation/route/Options.php
  class Options (line 10) | #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
    method __construct (line 17) | public function __construct(?string $path = null, ?string $name = null)

FILE: src/support/annotation/route/Patch.php
  class Patch (line 10) | #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
    method __construct (line 17) | public function __construct(?string $path = null, ?string $name = null)

FILE: src/support/annotation/route/Post.php
  class Post (line 10) | #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
    method __construct (line 17) | public function __construct(?string $path = null, ?string $name = null)

FILE: src/support/annotation/route/Put.php
  class Put (line 10) | #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
    method __construct (line 17) | public function __construct(?string $path = null, ?string $name = null)

FILE: src/support/annotation/route/Route.php
  class Route (line 10) | #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
    method __construct (line 33) | public function __construct(?string $path = null, array|string $method...

FILE: src/support/annotation/route/RouteGroup.php
  class RouteGroup (line 10) | #[Attribute(Attribute::TARGET_CLASS)]
    method __construct (line 21) | public function __construct(string $prefix = '')

FILE: src/support/bootstrap/Session.php
  class Session (line 28) | class Session implements Bootstrap
    method start (line 35) | public static function start(?Worker $worker)

FILE: src/support/exception/BusinessException.php
  class BusinessException (line 21) | class BusinessException extends \Webman\Exception\BusinessException

FILE: src/support/exception/Handler.php
  class Handler (line 27) | class Handler extends ExceptionHandler
    method report (line 33) | public function report(Throwable $exception)
    method render (line 38) | public function render(Request $request, Throwable $exception): Response

FILE: src/support/exception/InputTypeException.php
  class InputTypeException (line 7) | class InputTypeException extends PageNotFoundException
    method __construct (line 21) | public function __construct(string $message = 'Input :parameter must b...

FILE: src/support/exception/InputValueException.php
  class InputValueException (line 7) | class InputValueException extends PageNotFoundException
    method __construct (line 21) | public function __construct(string $message = 'Input :parameter is inv...

FILE: src/support/exception/MissingInputException.php
  class MissingInputException (line 9) | class MissingInputException extends PageNotFoundException
    method __construct (line 22) | public function __construct(string $message = 'Missing input parameter...
    method render (line 32) | public function render(Request $request): ?Response

FILE: src/support/exception/NotFoundException.php
  class NotFoundException (line 17) | class NotFoundException extends BusinessException

FILE: src/support/exception/PageNotFoundException.php
  class PageNotFoundException (line 21) | class PageNotFoundException extends NotFoundException
    method __construct (line 35) | public function __construct(string $message = '404 Not Found', int $co...
    method render (line 45) | public function render(Request $request): ?Response
    method html (line 64) | protected function html(string $message): string

FILE: src/support/helpers.php
  function run_path (line 58) | function run_path(string $path = ''): string
  function base_path (line 74) | function base_path($path = ''): string
  function app_path (line 89) | function app_path(string $path = ''): string
  function public_path (line 102) | function public_path(string $path = '', ?string $plugin = null): string
  function config_path (line 127) | function config_path(string $path = ''): string
  function runtime_path (line 139) | function runtime_path(string $path = ''): string
  function path_combine (line 156) | function path_combine(string $front, string $back): string
  function response (line 170) | function response(string $body = '', int $status = 200, array $headers =...
  function json (line 183) | function json($data, int $options = JSON_UNESCAPED_UNICODE | JSON_UNESCA...
  function xml (line 195) | function xml($xml): Response
  function jsonp (line 211) | function jsonp($data, string $callbackName = 'callback'): Response
  function redirect (line 228) | function redirect(string $location, int $status = 302, array $headers = ...
  function view (line 247) | function view(mixed $template = null, array $vars = [], ?string $app = n...
  function raw_view (line 265) | function raw_view(mixed $template = null, array $vars = [], ?string $app...
  function blade_view (line 280) | function blade_view(mixed $template = null, array $vars = [], ?string $a...
  function think_view (line 295) | function think_view(mixed $template = null, array $vars = [], ?string $a...
  function twig_view (line 310) | function twig_view(mixed $template = null, array $vars = [], ?string $ap...
  function request (line 321) | function request()
  function config (line 334) | function config(?string $key = null, mixed $default = null)
  function route (line 347) | function route(string $name, ...$parameters): string
  function session (line 374) | function session(array|string|null $key = null, mixed $default = null): ...
  function trans (line 408) | function trans(string $id, array $parameters = [], ?string $domain = nul...
  function locale (line 421) | function locale(?string $locale = null): string
  function not_found (line 436) | function not_found(): Response
  function copy_dir (line 450) | function copy_dir(string $source, string $dest, bool $overwrite = false)
  function remove_dir (line 474) | function remove_dir(string $dir): bool
  function worker_bind (line 493) | function worker_bind($worker, $class)
  function worker_start (line 524) | function worker_start($processName, $config)
  function get_realpath (line 570) | function get_realpath(string $filePath): string
  function is_phar (line 585) | function is_phar(): bool
  function template_inputs (line 600) | function template_inputs(mixed $template, array $vars, ?string $app, ?st...
  function cpu_count (line 639) | function cpu_count(): int
  function input (line 668) | function input(?string $param = null, mixed $default = null): mixed

FILE: src/support/view/Blade.php
  class Blade (line 32) | class Blade implements View
    method assign (line 39) | public static function assign(string|array $name, mixed $value = null)...
    method render (line 53) | public static function render(string $template, array $vars, ?string $...

FILE: src/support/view/Raw.php
  class Raw (line 34) | class Raw implements View
    method assign (line 41) | public static function assign(string|array $name, mixed $value = null)...
    method render (line 55) | public static function render(string $template, array $vars, ?string $...

FILE: src/support/view/ThinkPHP.php
  class ThinkPHP (line 33) | class ThinkPHP implements View
    method assign (line 40) | public static function assign(string|array $name, mixed $value = null)...
    method render (line 54) | public static function render(string $template, array $vars, ?string $...

FILE: src/support/view/Twig.php
  class Twig (line 34) | class Twig implements View
    method assign (line 41) | public static function assign(string|array $name, mixed $value = null)...
    method render (line 55) | public static function render(string $template, array $vars, ?string $...

FILE: src/windows.php
  function write_process_file (line 64) | function write_process_file($runtimeProcessPath, $processName, $firm): s...
  function popen_processes (line 114) | function popen_processes($processFiles)
Condensed preview — 68 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (258K chars).
[
  {
    "path": ".gitignore",
    "chars": 41,
    "preview": "composer.lock\nvendor\nvendor/\n.idea\n.idea/"
  },
  {
    "path": "README.md",
    "chars": 219,
    "preview": "# webman-framework\nNote: This repository is the core code of the webman framework. If you want to build an application u"
  },
  {
    "path": "composer.json",
    "chars": 1358,
    "preview": "{\n  \"name\": \"workerman/webman-framework\",\n  \"type\": \"library\",\n  \"keywords\": [\n    \"high performance\",\n    \"http service"
  },
  {
    "path": "src/App.php",
    "chars": 48825,
    "preview": "<?php\n\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license inform"
  },
  {
    "path": "src/Bootstrap.php",
    "chars": 652,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Config.php",
    "chars": 10312,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Container.php",
    "chars": 1933,
    "preview": "<?php\n\nnamespace Webman;\n\nuse Psr\\Container\\ContainerInterface;\nuse Webman\\Exception\\NotFoundException;\nuse function arr"
  },
  {
    "path": "src/Context.php",
    "chars": 521,
    "preview": "<?php\n\nnamespace Webman;\n\nuse Workerman\\Coroutine\\Context as WorkermanContext;\nuse Workerman\\Coroutine\\Utils\\Destruction"
  },
  {
    "path": "src/Exception/BusinessException.php",
    "chars": 2885,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Exception/ExceptionHandler.php",
    "chars": 3093,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Exception/ExceptionHandlerInterface.php",
    "chars": 878,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Exception/FileException.php",
    "chars": 596,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Exception/NotFoundException.php",
    "chars": 661,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/File.php",
    "chars": 1668,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Finder/ControllerFinder.php",
    "chars": 6935,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Finder/FileInfo.php",
    "chars": 3361,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Finder/Finder.php",
    "chars": 25824,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Http/Request.php",
    "chars": 9742,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Http/Response.php",
    "chars": 2265,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Http/UploadFile.php",
    "chars": 2305,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Install.php",
    "chars": 1287,
    "preview": "<?php\n\nnamespace Webman;\n\nclass Install\n{\n    const WEBMAN_PLUGIN = true;\n\n    /**\n     * @var array\n     */\n    protect"
  },
  {
    "path": "src/Middleware.php",
    "chars": 6404,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/MiddlewareInterface.php",
    "chars": 873,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Route/Route.php",
    "chars": 4480,
    "preview": "<?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 in"
  },
  {
    "path": "src/Route.php",
    "chars": 28520,
    "preview": "<?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 in"
  },
  {
    "path": "src/Session/FileSessionHandler.php",
    "chars": 641,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Session/RedisClusterSessionHandler.php",
    "chars": 618,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Session/RedisSessionHandler.php",
    "chars": 645,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/Util.php",
    "chars": 1156,
    "preview": "<?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 in"
  },
  {
    "path": "src/View.php",
    "chars": 710,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/start.php",
    "chars": 108,
    "preview": "#!/usr/bin/env php\n<?php\nchdir(__DIR__);\nrequire_once __DIR__ . '/vendor/autoload.php';\nsupport\\App::run();\n"
  },
  {
    "path": "src/support/App.php",
    "chars": 6019,
    "preview": "<?php\n\nnamespace support;\n\nuse Dotenv\\Dotenv;\nuse RuntimeException;\nuse Throwable;\nuse Webman\\Config;\nuse Webman\\Util;\nu"
  },
  {
    "path": "src/support/Container.php",
    "chars": 1154,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/Context.php",
    "chars": 544,
    "preview": "<?php\n\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license inform"
  },
  {
    "path": "src/support/Log.php",
    "chars": 4293,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/Plugin.php",
    "chars": 2660,
    "preview": "<?php\n\nnamespace support;\n\nuse function defined;\nuse function is_callable;\nuse function is_file;\nuse function method_exi"
  },
  {
    "path": "src/support/Request.php",
    "chars": 548,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/Response.php",
    "chars": 551,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/Translation.php",
    "chars": 3776,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/View.php",
    "chars": 890,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/annotation/DisableDefaultRoute.php",
    "chars": 284,
    "preview": "<?php\n\nnamespace support\\annotation;\n\nuse Attribute;\n\n/**\n * @deprecated Use support\\annotation\\route\\DisableDefaultRout"
  },
  {
    "path": "src/support/annotation/Middleware.php",
    "chars": 937,
    "preview": "<?php\n\nnamespace support\\annotation;\n\nuse Attribute;\n\n/**\n * Attach middlewares to routes/controllers/functions via attr"
  },
  {
    "path": "src/support/annotation/route/Any.php",
    "chars": 605,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: ['GET','POST','PUT','DE"
  },
  {
    "path": "src/support/annotation/route/Delete.php",
    "chars": 510,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'DELETE', ...)].\n */\n#["
  },
  {
    "path": "src/support/annotation/route/DisableDefaultRoute.php",
    "chars": 232,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Disable webman's default route mapping for a controll"
  },
  {
    "path": "src/support/annotation/route/Get.php",
    "chars": 501,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'GET', ...)].\n */\n#[Att"
  },
  {
    "path": "src/support/annotation/route/Head.php",
    "chars": 504,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'HEAD', ...)].\n */\n#[At"
  },
  {
    "path": "src/support/annotation/route/Options.php",
    "chars": 513,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'OPTIONS', ...)].\n */\n#"
  },
  {
    "path": "src/support/annotation/route/Patch.php",
    "chars": 507,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'PATCH', ...)].\n */\n#[A"
  },
  {
    "path": "src/support/annotation/route/Post.php",
    "chars": 504,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'POST', ...)].\n */\n#[At"
  },
  {
    "path": "src/support/annotation/route/Put.php",
    "chars": 501,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Shortcut for #[Route(methods: 'PUT', ...)].\n */\n#[Att"
  },
  {
    "path": "src/support/annotation/route/Route.php",
    "chars": 994,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Define an explicit route, or restrict allowed HTTP me"
  },
  {
    "path": "src/support/annotation/route/RouteGroup.php",
    "chars": 443,
    "preview": "<?php\n\nnamespace support\\annotation\\route;\n\nuse Attribute;\n\n/**\n * Group routes by controller-level prefix.\n */\n#[Attrib"
  },
  {
    "path": "src/support/bootstrap/Session.php",
    "chars": 1814,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/bootstrap.php",
    "chars": 4101,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/exception/BusinessException.php",
    "chars": 604,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/exception/Handler.php",
    "chars": 1019,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/exception/InputTypeException.php",
    "chars": 567,
    "preview": "<?php\n\nnamespace support\\exception;\n\nuse Throwable;\n\nclass InputTypeException extends PageNotFoundException\n{\n\n    /**\n "
  },
  {
    "path": "src/support/exception/InputValueException.php",
    "chars": 533,
    "preview": "<?php\n\nnamespace support\\exception;\n\nuse Throwable;\n\nclass InputValueException extends PageNotFoundException\n{\n\n    /**\n"
  },
  {
    "path": "src/support/exception/MissingInputException.php",
    "chars": 1442,
    "preview": "<?php\n\nnamespace support\\exception;\n\nuse Throwable;\nuse Webman\\Http\\Request;\nuse Webman\\Http\\Response;\n\nclass MissingInp"
  },
  {
    "path": "src/support/exception/NotFoundException.php",
    "chars": 521,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/exception/PageNotFoundException.php",
    "chars": 2528,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/helpers.php",
    "chars": 18783,
    "preview": "<?php\n\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license inform"
  },
  {
    "path": "src/support/view/Blade.php",
    "chars": 2716,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/view/Raw.php",
    "chars": 2397,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/view/ThinkPHP.php",
    "chars": 2838,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/support/view/Twig.php",
    "chars": 2884,
    "preview": "<?php\n/**\n * This file is part of webman.\n *\n * Licensed under The MIT License\n * For full copyright and license informa"
  },
  {
    "path": "src/windows.php",
    "chars": 3911,
    "preview": "<?php\n/**\n * Start file for windows\n */\nchdir(__DIR__);\nrequire_once __DIR__ . '/vendor/autoload.php';\n\nuse Dotenv\\Doten"
  }
]

About this extraction

This page contains the full source code of the walkor/webman-framework GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 68 files (237.9 KB), approximately 57.9k tokens, and a symbol index with 352 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!