diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1accf15
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+/vendor
+composer.phar
+composer.lock
+.DS_Store
+Thumbs.db
+/.idea
+/.vscode
+/.settings
+/.buildpath
+/.project
+.phpunit.result.cache
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..03466d7
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,119 @@
+如何贡献我的源代码
+===
+
+此文档介绍了 ThinkPHP 团队的组成以及运转机制,您提交的代码将给 ThinkPHP 项目带来什么好处,以及如何才能加入我们的行列。
+
+## 通过 Github 贡献代码
+
+ThinkPHP 目前使用 Git 来控制程序版本,如果你想为 ThinkPHP 贡献源代码,请先大致了解 Git 的使用方法。我们目前把项目托管在 GitHub 上,任何 GitHub 用户都可以向我们贡献代码。
+
+参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请并。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。
+
+我们希望你贡献的代码符合:
+
+* ThinkPHP 的编码规范
+* 适当的注释,能让其他人读懂
+* 遵循 Apache2 开源协议
+
+**如果想要了解更多细节或有任何疑问,请继续阅读下面的内容**
+
+### 注意事项
+
+* 本项目代码格式化标准选用 [**PER-CS2.0**](https://cs.symfony.com/doc/ruleSets/PER-CS2.0.html);
+* 类名和类文件名遵循 [**PSR-4**](http://www.kancloud.cn/thinkphp/php-fig-psr/3144);
+* 对于 Issues 的处理,请使用诸如 `fix #xxx(Issue ID)` 的 commit title 直接关闭 issue。
+* 系统会自动在 PHP 8.0 ~ 8.3 版本上测试修改,请确保你的修改符合 PHP 版本的语法规范;
+* 管理员不会合并造成 CI Failed 的修改,若出现 CI Failed 请检查自己的源代码或修改相应的[单元测试文件](tests);
+
+## GitHub Issue
+
+GitHub 提供了 Issue 功能,该功能可以用于:
+
+* 提出 bug
+* 提出功能改进
+* 反馈使用体验
+
+该功能不应该用于:
+
+ * 提出修改意见(涉及代码署名和修订追溯问题)
+ * 不友善的言论
+
+## 快速修改
+
+**GitHub 提供了快速编辑文件的功能**
+
+1. 登录 GitHub 帐号;
+2. 浏览项目文件,找到要进行修改的文件;
+3. 点击右上角铅笔图标进行修改;
+4. 填写 `Commit changes` 相关内容(Title 必填);
+5. 提交修改,等待 CI 验证和管理员合并。
+
+**若您需要一次提交大量修改,请继续阅读下面的内容**
+
+## 完整流程
+
+1. `fork`本项目;
+2. 克隆(`clone`)你 `fork` 的项目到本地;
+3. 新建分支(`branch`)并检出(`checkout`)新分支;
+4. 添加本项目到你的本地 git 仓库作为上游(`upstream`);
+5. 进行修改,若你的修改包含方法或函数的增减,请记得修改[单元测试文件](tests);
+6. 变基(衍合 `rebase`)你的分支到上游 master 分支;
+7. `push` 你的本地仓库到 GitHub;
+8. 提交 `pull request`;
+9. 等待 CI 验证(若不通过则重复 5~7,GitHub 会自动更新你的 `pull request`);
+10. 等待管理员处理,并及时 `rebase` 你的分支到上游 master 分支(若上游 master 分支有修改)。
+
+*若有必要,可以 `git push -f` 强行推送 rebase 后的分支到自己的 `fork`*
+
+*绝对不可以使用 `git push -f` 强行推送修改到上游*
+
+### 注意事项
+
+* 若对上述流程有任何不清楚的地方,请查阅 GIT 教程,如 [这个](http://backlogtool.com/git-guide/cn/);
+* 对于代码**不同方面**的修改,请在自己 `fork` 的项目中**创建不同的分支**(原因参见`完整流程`第9条备注部分);
+* 变基及交互式变基操作参见 [Git 交互式变基](http://pakchoi.me/2015/03/17/git-interactive-rebase/)
+
+## 推荐资源
+
+### 开发环境
+
+* [XAMPP](https://www.apachefriends.org/zh_cn/download.html) - Windows、Linux、Mac OS
+* [WampServer](https://www.apachefriends.org/zh_cn/download.html) - Windows
+* Docker - Windows、Linux、Mac OS
+
+或自行安装
+
+- Apache / Nginx
+- PHP 8.0 ~ 8.3
+- MySQL / MariaDB
+
+*Windows 用户推荐添加 PHP bin 目录到 PATH,方便使用 composer*
+
+*Linux 用户自行配置环境, Mac 用户推荐使用内置 Apache 配合 Homebrew 安装 PHP 和 MariaDB*
+
+### 编辑器
+
+Sublime Text 3 + phpfmt 插件
+
+phpfmt 插件参数
+
+```json
+{
+ "autocomplete": true,
+ "enable_auto_align": true,
+ "format_on_save": true,
+ "indent_with_space": true,
+ "psr1_naming": false,
+ "psr2": true,
+ "version": 4
+}
+```
+
+或其他 编辑器 / IDE 配合自动格式化工具
+
+### Git GUI
+
+* SourceTree
+* GitHub Desktop
+
+或其他 Git 图形界面客户端
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..3dcd79f
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,32 @@
+
+ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
+版权所有Copyright © 2006-2023 by ThinkPHP (http://thinkphp.cn)
+All rights reserved。
+ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
+
+Apache Licence是著名的非盈利开源组织Apache采用的协议。
+该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
+允许代码修改,再作为开源或商业软件发布。需要满足
+的条件:
+1. 需要给代码的用户一份Apache Licence ;
+2. 如果你修改了代码,需要在被修改的文件中说明;
+3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
+带有原来代码中的协议,商标,专利声明和其他原来作者规
+定需要包含的说明;
+4. 如果再发布的产品中包含一个Notice文件,则在Notice文
+件中需要带有本协议内容。你可以在Notice中增加自己的
+许可,但不可以表现为对Apache Licence构成更改。
+具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f7fdcd7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,85 @@
+
+
+# ThinkPHP 8
+
+[](https://github.com/top-think/framework/actions)
+[](https://packagist.org/packages/topthink/framework)
+[](https://packagist.org/packages/topthink/framework)
+[](http://www.php.net/)
+[](https://packagist.org/packages/topthink/framework)
+
+## 主要特性
+
+- 基于 PHP`8.0+`重构
+- 升级`PSR`依赖
+- 依赖`think-orm`3.0+ 版本
+- 全新的`think-dumper`支持远程调试
+- `6.0`/`6.1`无缝升级
+
+> ThinkPHP8 的运行环境要求 PHP8.0+
+
+现在开始,你可以使用官方提供的[ThinkChat](https://chat.topthink.com/),让你在学习 ThinkPHP 的旅途中享受私人 AI 助理服务!
+
+[](https://chat.topthink.com/)
+
+ThinkPHP 生态服务由[顶想云](https://www.topthink.com)(TOPThink Cloud)提供,为生态提供专业的开发者服务和价值之选。
+
+## 文档
+
+[完全开发手册](https://doc.thinkphp.cn)
+
+基于官方手册的数据训练和提供精准解答服务
+[官方专家智能体](https://chat.topthink.com/chat/eorole)
+
+## 赞助商
+
+全新的[赞助计划](https://www.thinkphp.cn/sponsor)可以让你通过我们的网站、手册、欢迎页及 GIT 仓库获得巨大曝光,同时提升企业的品牌声誉,也更好保障 ThinkPHP 的可持续发展。
+
+[](https://www.thinkphp.cn/sponsor/special)
+
+[](https://www.thinkphp.cn/sponsor)
+
+## 安装
+
+```
+composer create-project topthink/think tp
+```
+
+启动服务
+
+```
+cd tp
+php think run
+```
+
+然后就可以在浏览器中访问
+
+```
+http://localhost:8000
+```
+
+如果需要更新框架使用
+
+```
+composer update topthink/framework
+```
+
+## 命名规范
+
+`ThinkPHP`遵循 PSR-2 命名规范和 PSR-4 自动加载规范。
+
+## 参与开发
+
+直接提交 PR 或者 Issue 即可
+
+## 版权信息
+
+ThinkPHP 遵循 Apache2 开源协议发布,并提供免费使用。
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有 Copyright © 2006-2025 by ThinkPHP (http://thinkphp.cn) All rights reserved。
+
+ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
+
+更多细节参阅 [LICENSE.txt](LICENSE.txt)
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..f844527
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,59 @@
+{
+ "name": "topthink/framework",
+ "description": "The ThinkPHP Framework.",
+ "keywords": [
+ "framework",
+ "thinkphp",
+ "ORM"
+ ],
+ "homepage": "http://thinkphp.cn/",
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ },
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "require": {
+ "php": ">=8.0.0",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "ext-ctype": "*",
+ "psr/log": "^1.0|^2.0|^3.0",
+ "psr/simple-cache": "^1.0|^2.0|^3.0",
+ "psr/http-message": "^1.0",
+ "topthink/think-orm": "^3.0|^4.0",
+ "topthink/think-helper": "^3.1",
+ "topthink/think-container": "^3.0",
+ "topthink/think-validate": "^3.0"
+ },
+ "require-dev": {
+ "mikey179/vfsstream": "^1.6",
+ "mockery/mockery": "^1.2",
+ "phpunit/phpunit": "^9.5",
+ "guzzlehttp/psr7": "^2.1.0"
+ },
+ "autoload": {
+ "files": [],
+ "psr-4": {
+ "think\\": "src/think/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "think\\tests\\": "tests/"
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "config": {
+ "sort-packages": true
+ },
+ "scripts": {
+ "php-cs-fixer": "php-cs-fixer fix src/ --rules=@PER-CS2.0 --dry-run --diff"
+ }
+}
diff --git a/init b/init
deleted file mode 100644
index cdb8d0e..0000000
--- a/init
+++ /dev/null
@@ -1 +0,0 @@
-init
\ No newline at end of file
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..25fd059
Binary files /dev/null and b/logo.png differ
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..4966885
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,27 @@
+
+
+
+
+ ./src/think
+
+
+
+
+ ./tests
+
+
+
diff --git a/src/helper.php b/src/helper.php
new file mode 100644
index 0000000..7de5839
--- /dev/null
+++ b/src/helper.php
@@ -0,0 +1,677 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+//------------------------
+// ThinkPHP 助手函数
+//-------------------------
+
+use think\App;
+use think\Container;
+use think\exception\HttpException;
+use think\exception\HttpResponseException;
+use think\facade\Cache;
+use think\facade\Config;
+use think\facade\Cookie;
+use think\facade\Env;
+use think\facade\Event;
+use think\facade\Lang;
+use think\facade\Log;
+use think\facade\Request;
+use think\facade\Route;
+use think\facade\Session;
+use think\Response;
+use think\response\File;
+use think\response\Json;
+use think\response\Jsonp;
+use think\response\Redirect;
+use think\response\View;
+use think\response\Xml;
+use think\route\Url as UrlBuild;
+use think\Validate;
+use think\validate\ValidateRuleSet;
+
+if (!function_exists('abort')) {
+ /**
+ * 抛出HTTP异常
+ * @param integer|Response $code 状态码 或者 Response对象实例
+ * @param string $message 错误信息
+ * @param array $header 参数
+ */
+ function abort($code, string $message = '', array $header = [])
+ {
+ if ($code instanceof Response) {
+ throw new HttpResponseException($code);
+ } else {
+ throw new HttpException($code, $message, null, $header);
+ }
+ }
+}
+
+if (!function_exists('app')) {
+ /**
+ * 快速获取容器中的实例 支持依赖注入
+ * @template T
+ * @param string|class-string $name 类名或标识 默认获取当前应用实例
+ * @param array $args 参数
+ * @param bool $newInstance 是否每次创建新的实例
+ * @return T|object|App
+ */
+ function app(string $name = '', array $args = [], bool $newInstance = false)
+ {
+ return Container::getInstance()->make($name ?: App::class, $args, $newInstance);
+ }
+}
+
+if (!function_exists('bind')) {
+ /**
+ * 绑定一个类到容器
+ * @param string|array $abstract 类标识、接口(支持批量绑定)
+ * @param mixed $concrete 要绑定的类、闭包或者实例
+ * @return Container
+ */
+ function bind($abstract, $concrete = null)
+ {
+ return Container::getInstance()->bind($abstract, $concrete);
+ }
+}
+
+if (!function_exists('cache')) {
+ /**
+ * 缓存管理
+ * @param string $name 缓存名称
+ * @param mixed $value 缓存值
+ * @param mixed $options 缓存参数
+ * @param string $tag 缓存标签
+ * @return mixed
+ */
+ function cache(?string $name = null, $value = '', $options = null, $tag = null)
+ {
+ if (is_null($name)) {
+ return app('cache');
+ }
+
+ if ('' === $value) {
+ // 获取缓存
+ return str_starts_with($name, '?') ? Cache::has(substr($name, 1)) : Cache::get($name);
+ } elseif (is_null($value)) {
+ // 删除缓存
+ return Cache::delete($name);
+ }
+
+ // 缓存数据
+ if (is_array($options)) {
+ $expire = $options['expire'] ?? null; //修复查询缓存无法设置过期时间
+ } else {
+ $expire = $options;
+ }
+
+ if (is_null($tag)) {
+ return Cache::set($name, $value, $expire);
+ } else {
+ return Cache::tag($tag)->set($name, $value, $expire);
+ }
+ }
+}
+
+if (!function_exists('config')) {
+ /**
+ * 获取和设置配置参数
+ * @param string|array $name 参数名
+ * @param mixed $value 参数值
+ * @return mixed
+ */
+ function config($name = '', $value = null)
+ {
+ if (is_array($name)) {
+ return Config::set($name, $value);
+ }
+
+ return str_starts_with($name, '?') ? Config::has(substr($name, 1)) : Config::get($name, $value);
+ }
+}
+
+if (!function_exists('cookie')) {
+ /**
+ * Cookie管理
+ * @param string $name cookie名称
+ * @param mixed $value cookie值
+ * @param mixed $option 参数
+ * @return mixed
+ */
+ function cookie(string $name, $value = '', $option = null)
+ {
+ if (is_null($value)) {
+ // 删除
+ Cookie::delete($name, $option ?: []);
+ } elseif ('' === $value) {
+ // 获取
+ return str_starts_with($name, '?') ? Cookie::has(substr($name, 1)) : Cookie::get($name);
+ } else {
+ // 设置
+ return Cookie::set($name, $value, $option);
+ }
+ }
+}
+
+if (!function_exists('download')) {
+ /**
+ * 获取\think\response\Download对象实例
+ * @param string $filename 要下载的文件
+ * @param string $name 显示文件名
+ * @param bool $content 是否为内容
+ * @param int $expire 有效期(秒)
+ * @return \think\response\File
+ */
+ function download(string $filename, string $name = '', bool $content = false, int $expire = 180): File
+ {
+ return Response::create($filename, 'file')->name($name)->isContent($content)->expire($expire);
+ }
+}
+
+if (!function_exists('dump')) {
+ /**
+ * 浏览器友好的变量输出
+ * @param mixed $vars 要输出的变量
+ * @return void
+ */
+ function dump(...$vars)
+ {
+ ob_start();
+ var_dump(...$vars);
+
+ $output = ob_get_clean();
+ $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', $output);
+
+ if (PHP_SAPI == 'cli') {
+ $output = PHP_EOL . $output . PHP_EOL;
+ } else {
+ if (!extension_loaded('xdebug')) {
+ $output = htmlspecialchars($output, ENT_SUBSTITUTE);
+ }
+ $output = '' . $output . '
';
+ }
+
+ echo $output;
+ }
+}
+
+if (!function_exists('env')) {
+ /**
+ * 获取环境变量值
+ * @access public
+ * @param string $name 环境变量名(支持二级 .号分割)
+ * @param string $default 默认值
+ * @return mixed
+ */
+ function env(?string $name = null, $default = null)
+ {
+ return Env::get($name, $default);
+ }
+}
+
+if (!function_exists('event')) {
+ /**
+ * 触发事件
+ * @param mixed $event 事件名(或者类名)
+ * @param mixed $args 参数
+ * @return mixed
+ */
+ function event($event, $args = null)
+ {
+ return Event::trigger($event, $args);
+ }
+}
+
+if (!function_exists('halt')) {
+ /**
+ * 调试变量并且中断输出
+ * @param mixed $vars 调试变量或者信息
+ */
+ function halt(...$vars)
+ {
+ dump(...$vars);
+
+ throw new HttpResponseException(Response::create());
+ }
+}
+
+if (!function_exists('input')) {
+ /**
+ * 获取输入数据 支持默认值和过滤
+ * @param string $key 获取的变量名
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ function input(string $key = '', $default = null, $filter = '')
+ {
+ if (str_starts_with($key, '?')) {
+ $key = substr($key, 1);
+ $has = true;
+ }
+
+ if ($pos = strpos($key, '.')) {
+ // 指定参数来源
+ $method = substr($key, 0, $pos);
+ if (in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) {
+ $key = substr($key, $pos + 1);
+ if ('server' == $method && is_null($default)) {
+ $default = '';
+ }
+ } else {
+ $method = 'param';
+ }
+ } else {
+ // 默认为自动判断
+ $method = 'param';
+ }
+
+ return isset($has) ?
+ request()->has($key, $method) :
+ request()->$method($key, $default, $filter);
+ }
+}
+
+if (!function_exists('invoke')) {
+ /**
+ * 调用反射实例化对象或者执行方法 支持依赖注入
+ * @param mixed $call 类名或者callable
+ * @param array $args 参数
+ * @return mixed
+ */
+ function invoke($call, array $args = [])
+ {
+ if (is_callable($call)) {
+ return Container::getInstance()->invoke($call, $args);
+ }
+
+ return Container::getInstance()->invokeClass($call, $args);
+ }
+}
+
+if (!function_exists('json')) {
+ /**
+ * 获取\think\response\Json对象实例
+ * @param mixed $data 返回的数据
+ * @param int $code 状态码
+ * @param array $header 头部
+ * @param array $options 参数
+ * @return \think\response\Json
+ */
+ function json($data = [], $code = 200, $header = [], $options = []): Json
+ {
+ return Response::create($data, 'json', $code)->header($header)->options($options);
+ }
+}
+
+if (!function_exists('jsonp')) {
+ /**
+ * 获取\think\response\Jsonp对象实例
+ * @param mixed $data 返回的数据
+ * @param int $code 状态码
+ * @param array $header 头部
+ * @param array $options 参数
+ * @return \think\response\Jsonp
+ */
+ function jsonp($data = [], $code = 200, $header = [], $options = []): Jsonp
+ {
+ return Response::create($data, 'jsonp', $code)->header($header)->options($options);
+ }
+}
+
+if (!function_exists('lang')) {
+ /**
+ * 获取语言变量值
+ * @param string $name 语言变量名
+ * @param array $vars 动态变量值
+ * @param string $lang 语言
+ * @return mixed
+ */
+ function lang(string $name, array $vars = [], string $lang = '')
+ {
+ return Lang::get($name, $vars, $lang);
+ }
+}
+
+if (!function_exists('parse_name')) {
+ /**
+ * 字符串命名风格转换
+ * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格
+ * @param string $name 字符串
+ * @param int $type 转换类型
+ * @param bool $ucfirst 首字母是否大写(驼峰规则)
+ * @return string
+ */
+ function parse_name(string $name, int $type = 0, bool $ucfirst = true): string
+ {
+ if ($type) {
+ $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) {
+ return strtoupper($match[1]);
+ }, $name);
+
+ return $ucfirst ? ucfirst($name) : lcfirst($name);
+ }
+
+ return strtolower(trim(preg_replace('/[A-Z]/', '_\\0', $name), '_'));
+ }
+}
+
+if (!function_exists('redirect')) {
+ /**
+ * 获取\think\response\Redirect对象实例
+ * @param string $url 重定向地址
+ * @param int $code 状态码
+ * @return \think\response\Redirect
+ */
+ function redirect(string $url = '', int $code = 302): Redirect
+ {
+ return Response::create($url, 'redirect', $code);
+ }
+}
+
+if (!function_exists('request')) {
+ /**
+ * 获取当前Request对象实例
+ * @return Request
+ */
+ function request(): \think\Request
+ {
+ return app('request');
+ }
+}
+
+if (!function_exists('response')) {
+ /**
+ * 创建普通 Response 对象实例
+ * @param mixed $data 输出数据
+ * @param int|string $code 状态码
+ * @param array $header 头信息
+ * @param string $type
+ * @return Response
+ */
+ function response($data = '', $code = 200, $header = [], $type = 'html'): Response
+ {
+ return Response::create($data, $type, $code)->header($header);
+ }
+}
+
+if (!function_exists('session')) {
+ /**
+ * Session管理
+ * @param string $name session名称
+ * @param mixed $value session值
+ * @return mixed
+ */
+ function session($name = '', $value = '')
+ {
+ if (is_null($name)) {
+ // 清除
+ Session::clear();
+ } elseif ('' === $name) {
+ return Session::all();
+ } elseif (is_null($value)) {
+ // 删除
+ Session::delete($name);
+ } elseif ('' === $value) {
+ // 判断或获取
+ return str_starts_with($name, '?') ? Session::has(substr($name, 1)) : Session::get($name);
+ } else {
+ // 设置
+ Session::set($name, $value);
+ }
+ }
+}
+
+if (!function_exists('token')) {
+ /**
+ * 获取Token令牌
+ * @param string $name 令牌名称
+ * @param mixed $type 令牌生成方法
+ * @return string
+ */
+ function token(string $name = '__token__', string $type = 'md5'): string
+ {
+ return Request::buildToken($name, $type);
+ }
+}
+
+if (!function_exists('token_field')) {
+ /**
+ * 生成令牌隐藏表单
+ * @param string $name 令牌名称
+ * @param mixed $type 令牌生成方法
+ * @return string
+ */
+ function token_field(string $name = '__token__', string $type = 'md5'): string
+ {
+ $token = Request::buildToken($name, $type);
+
+ return '';
+ }
+}
+
+if (!function_exists('token_meta')) {
+ /**
+ * 生成令牌meta
+ * @param string $name 令牌名称
+ * @param mixed $type 令牌生成方法
+ * @return string
+ */
+ function token_meta(string $name = '__token__', string $type = 'md5'): string
+ {
+ $token = Request::buildToken($name, $type);
+
+ return '';
+ }
+}
+
+if (!function_exists('trace')) {
+ /**
+ * 记录日志信息
+ * @param mixed $log log信息 支持字符串和数组
+ * @param string $level 日志级别
+ * @return array|void
+ */
+ function trace($log = '[think]', string $level = 'log')
+ {
+ if ('[think]' === $log) {
+ return Log::getLog();
+ }
+
+ Log::record($log, $level);
+ }
+}
+
+if (!function_exists('url')) {
+ /**
+ * Url生成
+ * @param string $url 路由地址
+ * @param array $vars 变量
+ * @param bool|string $suffix 生成的URL后缀
+ * @param bool|string $domain 域名
+ * @return UrlBuild
+ */
+ function url(string $url = '', array $vars = [], $suffix = true, $domain = false): UrlBuild
+ {
+ return Route::buildUrl($url, $vars)->suffix($suffix)->domain($domain);
+ }
+}
+
+if (!function_exists('validate')) {
+ /**
+ * 生成验证对象
+ * @param string|array $validate 验证器类名或者验证规则数组
+ * @param array $message 错误提示信息
+ * @param bool $batch 是否批量验证
+ * @param bool $failException 是否抛出异常
+ * @return Validate
+ */
+ function validate($validate = '', array $message = [], bool $batch = false, bool $failException = true): Validate
+ {
+ if (is_array($validate) || '' === $validate) {
+ $v = new Validate();
+ if (is_array($validate)) {
+ $v->rule($validate);
+ }
+ } else {
+ if (str_contains($validate, '.')) {
+ // 支持场景
+ [$validate, $scene] = explode('.', $validate);
+ }
+
+ $class = str_contains($validate, '\\') ? $validate : app()->parseClass('validate', $validate);
+
+ $v = new $class();
+
+ if (!empty($scene)) {
+ $v->scene($scene);
+ }
+ }
+
+ return $v->message($message)->batch($batch)->failException($failException);
+ }
+}
+
+if (!function_exists('rules')) {
+ /**
+ * 定义ValidateRuleSet规则集合
+ * @param array $rules 验证因子集
+ * @return ValidateRuleSet
+ */
+ function rules(array $rules): ValidateRuleSet
+ {
+ return ValidateRuleSet::rules($rules);
+ }
+}
+
+if (!function_exists('view')) {
+ /**
+ * 渲染模板输出
+ * @param string $template 模板文件
+ * @param array $vars 模板变量
+ * @param int $code 状态码
+ * @param callable $filter 内容过滤
+ * @return \think\response\View
+ */
+ function view(string $template = '', $vars = [], $code = 200, $filter = null): View
+ {
+ return Response::create($template, 'view', $code)->assign($vars)->filter($filter);
+ }
+}
+
+if (!function_exists('display')) {
+ /**
+ * 渲染模板输出
+ * @param string $content 渲染内容
+ * @param array $vars 模板变量
+ * @param int $code 状态码
+ * @param callable $filter 内容过滤
+ * @return \think\response\View
+ */
+ function display(string $content, $vars = [], $code = 200, $filter = null): View
+ {
+ return Response::create($content, 'view', $code)->isContent(true)->assign($vars)->filter($filter);
+ }
+}
+
+if (!function_exists('xml')) {
+ /**
+ * 获取\think\response\Xml对象实例
+ * @param mixed $data 返回的数据
+ * @param int $code 状态码
+ * @param array $header 头部
+ * @param array $options 参数
+ * @return \think\response\Xml
+ */
+ function xml($data = [], $code = 200, $header = [], $options = []): Xml
+ {
+ return Response::create($data, 'xml', $code)->header($header)->options($options);
+ }
+}
+
+if (!function_exists('app_path')) {
+ /**
+ * 获取当前应用目录
+ *
+ * @param string $path
+ * @return string
+ */
+ function app_path($path = '')
+ {
+ return app()->getAppPath() . ($path ? $path . DIRECTORY_SEPARATOR : $path);
+ }
+}
+
+if (!function_exists('base_path')) {
+ /**
+ * 获取应用基础目录
+ *
+ * @param string $path
+ * @return string
+ */
+ function base_path($path = '')
+ {
+ return app()->getBasePath() . ($path ? $path . DIRECTORY_SEPARATOR : $path);
+ }
+}
+
+if (!function_exists('config_path')) {
+ /**
+ * 获取应用配置目录
+ *
+ * @param string $path
+ * @return string
+ */
+ function config_path($path = '')
+ {
+ return app()->getConfigPath() . ($path ? $path . DIRECTORY_SEPARATOR : $path);
+ }
+}
+
+if (!function_exists('public_path')) {
+ /**
+ * 获取web根目录
+ *
+ * @param string $path
+ * @return string
+ */
+ function public_path($path = '')
+ {
+ return app()->getRootPath() . 'public' . DIRECTORY_SEPARATOR . ($path ? ltrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $path);
+ }
+}
+
+if (!function_exists('runtime_path')) {
+ /**
+ * 获取应用运行时目录
+ *
+ * @param string $path
+ * @return string
+ */
+ function runtime_path($path = '')
+ {
+ return app()->getRuntimePath() . ($path ? $path . DIRECTORY_SEPARATOR : $path);
+ }
+}
+
+if (!function_exists('root_path')) {
+ /**
+ * 获取项目根目录
+ *
+ * @param string $path
+ * @return string
+ */
+ function root_path($path = '')
+ {
+ return app()->getRootPath() . ($path ? $path . DIRECTORY_SEPARATOR : $path);
+ }
+}
diff --git a/src/lang/zh-cn.php b/src/lang/zh-cn.php
new file mode 100644
index 0000000..eea41d5
--- /dev/null
+++ b/src/lang/zh-cn.php
@@ -0,0 +1,157 @@
+
+// +----------------------------------------------------------------------
+
+// 核心中文语言包
+return [
+ // 系统错误提示
+ 'Undefined variable' => '未定义变量',
+ 'Undefined index' => '未定义数组索引',
+ 'Undefined offset' => '未定义数组下标',
+ 'Parse error' => '语法解析错误',
+ 'Type error' => '类型错误',
+ 'Fatal error' => '致命错误',
+ 'syntax error' => '语法错误',
+
+ // 框架核心错误提示
+ 'dispatch type not support' => '不支持的调度类型',
+ 'method param miss' => '方法参数错误',
+ 'method not exists' => '方法不存在',
+ 'function not exists' => '函数不存在',
+ 'app not exists' => '应用不存在',
+ 'controller not exists' => '控制器不存在',
+ 'class not exists' => '类不存在',
+ 'property not exists' => '类的属性不存在',
+ 'template not exists' => '模板文件不存在',
+ 'illegal controller name' => '非法的控制器名称',
+ 'illegal action name' => '非法的操作名称',
+ 'url suffix deny' => '禁止的URL后缀访问',
+ 'Undefined cache config' => '缓存配置未定义',
+ 'Route Not Found' => '当前访问路由未定义或不匹配',
+ 'Undefined db config' => '数据库配置未定义',
+ 'Undefined log config' => '日志配置未定义',
+ 'Undefined db type' => '未定义数据库类型',
+ 'variable type error' => '变量类型错误',
+ 'PSR-4 error' => 'PSR-4 规范错误',
+ 'not support type' => '不支持的分页索引字段类型',
+ 'not support total' => '简洁模式下不能获取数据总数',
+ 'not support last' => '简洁模式下不能获取最后一页',
+ 'error session handler' => '错误的SESSION处理器类',
+ 'not allow php tag' => '模板不允许使用PHP语法',
+ 'not support' => '不支持',
+ 'database config error' => '数据库配置信息错误',
+ 'redisd master' => 'Redisd 主服务器错误',
+ 'redisd slave' => 'Redisd 从服务器错误',
+ 'must run at sae' => '必须在SAE运行',
+ 'memcache init error' => '未开通Memcache服务,请在SAE管理平台初始化Memcache服务',
+ 'KVDB init error' => '没有初始化KVDB,请在SAE管理平台初始化KVDB服务',
+ 'fields not exists' => '数据表字段不存在',
+ 'where express error' => '查询表达式错误',
+ 'no data to update' => '没有任何数据需要更新',
+ 'miss data to insert' => '缺少需要写入的数据',
+ 'miss complex primary data' => '缺少复合主键数据',
+ 'miss update condition' => '缺少更新条件',
+ 'model data Not Found' => '模型数据不存在',
+ 'table data not Found' => '表数据不存在',
+ 'delete without condition' => '没有条件不会执行删除操作',
+ 'miss relation data' => '缺少关联表数据',
+ 'tag attr must' => '模板标签属性必须',
+ 'tag error' => '模板标签错误',
+ 'cache write error' => '缓存写入失败',
+ 'sae mc write error' => 'SAE mc 写入错误',
+ 'route name not exists' => '路由标识不存在(或参数不够)',
+ 'invalid request' => '非法请求',
+ 'bind attr has exists' => '模型的属性已经存在',
+ 'relation data not exists' => '关联数据不存在',
+ 'relation not support' => '关联不支持',
+ 'chunk not support order' => 'Chunk不支持调用order方法',
+ 'route pattern error' => '路由变量规则定义错误',
+ 'route behavior will not support' => '路由行为废弃(使用中间件替代)',
+ 'closure not support cache(true)' => '使用闭包查询不支持cache(true),请指定缓存Key',
+
+ // 上传错误信息
+ 'unknown upload error' => '未知上传错误!',
+ 'file write error' => '文件写入失败!',
+ 'upload temp dir not found' => '找不到临时文件夹!',
+ 'no file to uploaded' => '没有文件被上传!',
+ 'only the portion of file is uploaded' => '文件只有部分被上传!',
+ 'upload File size exceeds the maximum value' => '上传文件大小超过了最大值!',
+ 'upload write error' => '文件上传保存错误!',
+ 'has the same filename: {:filename}' => '存在同名文件:{:filename}',
+ 'upload illegal files' => '非法上传文件',
+ 'illegal image files' => '非法图片文件',
+ 'extensions to upload is not allowed' => '上传文件后缀不允许',
+ 'mimetype to upload is not allowed' => '上传文件MIME类型不允许!',
+ 'filesize not match' => '上传文件大小不符!',
+ 'directory {:path} creation failed' => '目录 {:path} 创建失败!',
+
+ 'The middleware must return Response instance' => '中间件方法必须返回Response对象实例',
+ 'The queue was exhausted, with no response returned' => '中间件队列为空',
+ // Validate Error Message
+ ':attribute require' => ':attribute不能为空',
+ ':attribute must' => ':attribute必须',
+ ':attribute must be numeric' => ':attribute必须是数字',
+ ':attribute must be integer' => ':attribute必须是整数',
+ ':attribute must be float' => ':attribute必须是浮点数',
+ ':attribute must be string' => ':attribute必须是字符串',
+ ':attribute must be :rule enum' => ':attribute必须是有效的 :rule 枚举',
+ ':attribute must start with :rule' => ':attribute必须以 :rule 开头',
+ ':attribute must end with :rule' => ':attribute必须以 :rule 结尾',
+ ':attribute must contain :rule' => ':attribute必须包含 :rule',
+ ':attribute must be bool' => ':attribute必须是布尔值',
+ ':attribute not a valid email address' => ':attribute格式不符',
+ ':attribute not a valid mobile' => ':attribute格式不符',
+ ':attribute must be a array' => ':attribute必须是数组',
+ ':attribute must be yes,on,true or 1' => ':attribute必须是yes、on、true或者1',
+ ':attribute must be no,off,false or 0' => ':attribute必须是no、off、false或者0',
+ ':attribute not a valid datetime' => ':attribute不是一个有效的日期或时间格式',
+ ':attribute not a valid file' => ':attribute不是有效的上传文件',
+ ':attribute not a valid image' => ':attribute不是有效的图像文件',
+ ':attribute must be alpha' => ':attribute只能是字母',
+ ':attribute must be alpha-numeric' => ':attribute只能是字母和数字',
+ ':attribute must be alpha-numeric, dash, underscore' => ':attribute只能是字母、数字和下划线_及破折号-',
+ ':attribute not a valid domain or ip' => ':attribute不是有效的域名或者IP',
+ ':attribute must be chinese' => ':attribute只能是汉字',
+ ':attribute must be chinese or alpha' => ':attribute只能是汉字、字母',
+ ':attribute must be chinese,alpha-numeric' => ':attribute只能是汉字、字母和数字',
+ ':attribute must be chinese,alpha-numeric,underscore, dash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-',
+ ':attribute not a valid url' => ':attribute不是有效的URL地址',
+ ':attribute not a valid ip' => ':attribute不是有效的IP地址',
+ ':attribute must be dateFormat of :rule' => ':attribute必须使用日期格式 :rule',
+ ':attribute must be in :rule' => ':attribute必须在 :rule 范围内',
+ ':attribute be notin :rule' => ':attribute不能在 :rule 范围内',
+ ':attribute must between :1 - :2' => ':attribute只能在 :1 - :2 之间',
+ ':attribute not between :1 - :2' => ':attribute不能在 :1 - :2 之间',
+ 'size of :attribute must be :rule' => ':attribute长度不符合要求 :rule',
+ 'max size of :attribute must be :rule' => ':attribute长度不能超过 :rule',
+ 'min size of :attribute must be :rule' => ':attribute长度不能小于 :rule',
+ ':attribute cannot be less than :rule' => ':attribute日期不能小于 :rule',
+ ':attribute cannot exceed :rule' => ':attribute日期不能超过 :rule',
+ ':attribute not within :rule' => '不在有效期内 :rule',
+ 'access IP is not allowed' => '不允许的IP访问',
+ 'access IP denied' => '禁止的IP访问',
+ ':attribute out of accord with :2' => ':attribute和确认字段:2不一致',
+ ':attribute cannot be same with :2' => ':attribute和比较字段:2不能相同',
+ ':attribute must greater than or equal :rule' => ':attribute必须大于等于 :rule',
+ ':attribute must greater than :rule' => ':attribute必须大于 :rule',
+ ':attribute must less than or equal :rule' => ':attribute必须小于等于 :rule',
+ ':attribute must less than :rule' => ':attribute必须小于 :rule',
+ ':attribute must equal :rule' => ':attribute必须等于 :rule',
+ ':attribute must not be equal to :rule' => ':attribute不能等于 :rule',
+ ':attribute has exists' => ':attribute已存在',
+ ':attribute not conform to the rules' => ':attribute不符合指定规则',
+ ':attribute must multiple :rule' => ':attribute必须是 :rule 的倍数',
+ 'invalid Request method' => '无效的请求类型',
+ 'invalid token' => '令牌数据无效',
+ 'not conform to the rules' => '规则错误',
+
+ 'record has update' => '记录已经被更新了',
+];
diff --git a/src/think/App.php b/src/think/App.php
new file mode 100644
index 0000000..3b3a014
--- /dev/null
+++ b/src/think/App.php
@@ -0,0 +1,663 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Composer\InstalledVersions;
+use think\event\AppInit;
+use think\helper\Str;
+use think\initializer\BootService;
+use think\initializer\Error;
+use think\initializer\RegisterService;
+
+/**
+ * App 基础类
+ * @property Route $route
+ * @property Config $config
+ * @property Cache $cache
+ * @property Request $request
+ * @property Http $http
+ * @property Console $console
+ * @property Env $env
+ * @property Event $event
+ * @property Middleware $middleware
+ * @property Log $log
+ * @property Lang $lang
+ * @property Db $db
+ * @property Cookie $cookie
+ * @property Session $session
+ * @property Validate $validate
+ */
+class App extends Container
+{
+ /**
+ * 核心框架版本
+ * @deprecated 已经废弃 请改用version()方法
+ */
+ const VERSION = '8.0.0';
+
+ /**
+ * 应用调试模式
+ * @var bool
+ */
+ protected $appDebug = false;
+
+ /**
+ * 公共环境变量标识
+ * @var string
+ */
+ protected $baseEnvName = '';
+
+ /**
+ * 环境变量标识
+ * @var string
+ */
+ protected $envName = '';
+
+ /**
+ * 应用开始时间
+ * @var float
+ */
+ protected $beginTime;
+
+ /**
+ * 应用内存初始占用
+ * @var integer
+ */
+ protected $beginMem;
+
+ /**
+ * 当前应用类库命名空间
+ * @var string
+ */
+ protected $namespace = 'app';
+
+ /**
+ * 应用根目录
+ * @var string
+ */
+ protected $rootPath = '';
+
+ /**
+ * 框架目录
+ * @var string
+ */
+ protected $thinkPath = '';
+
+ /**
+ * 应用目录
+ * @var string
+ */
+ protected $appPath = '';
+
+ /**
+ * Runtime目录
+ * @var string
+ */
+ protected $runtimePath = '';
+
+ /**
+ * 路由定义目录
+ * @var string
+ */
+ protected $routePath = '';
+
+ /**
+ * 配置后缀
+ * @var string
+ */
+ protected $configExt = '.php';
+
+ /**
+ * 应用初始化器
+ * @var array
+ */
+ protected $initializers = [
+ Error::class,
+ RegisterService::class,
+ BootService::class,
+ ];
+
+ /**
+ * 注册的系统服务
+ * @var array
+ */
+ protected $services = [];
+
+ /**
+ * 初始化
+ * @var bool
+ */
+ protected $initialized = false;
+
+ /**
+ * 容器绑定标识
+ * @var array
+ */
+ protected $bind = [
+ 'app' => App::class,
+ 'cache' => Cache::class,
+ 'config' => Config::class,
+ 'console' => Console::class,
+ 'cookie' => Cookie::class,
+ 'db' => Db::class,
+ 'env' => Env::class,
+ 'event' => Event::class,
+ 'http' => Http::class,
+ 'lang' => Lang::class,
+ 'log' => Log::class,
+ 'middleware' => Middleware::class,
+ 'request' => Request::class,
+ 'response' => Response::class,
+ 'route' => Route::class,
+ 'session' => Session::class,
+ 'validate' => Validate::class,
+ 'view' => View::class,
+ 'think\DbManager' => Db::class,
+ 'think\LogManager' => Log::class,
+ 'think\CacheManager' => Cache::class,
+
+ // 接口依赖注入
+ 'Psr\Log\LoggerInterface' => Log::class,
+ ];
+
+ /**
+ * 架构方法
+ * @access public
+ * @param string $rootPath 应用根目录
+ */
+ public function __construct(string $rootPath = '')
+ {
+ $this->thinkPath = realpath(dirname(__DIR__)) . DIRECTORY_SEPARATOR;
+ $this->rootPath = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $this->getDefaultRootPath();
+ $this->appPath = $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
+ $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR;
+
+ if (is_file($this->appPath . 'provider.php')) {
+ $this->bind(include $this->appPath . 'provider.php');
+ }
+
+ static::setInstance($this);
+
+ $this->instance('app', $this);
+ $this->instance('think\Container', $this);
+ }
+
+ /**
+ * 注册服务
+ * @access public
+ * @param Service|string $service 服务
+ * @param bool $force 强制重新注册
+ * @return Service|null
+ */
+ public function register(Service | string $service, bool $force = false)
+ {
+ $registered = $this->getService($service);
+
+ if ($registered && !$force) {
+ return $registered;
+ }
+
+ if (is_string($service)) {
+ $service = new $service($this);
+ }
+
+ if (method_exists($service, 'register')) {
+ $service->register();
+ }
+
+ if (property_exists($service, 'bind')) {
+ $this->bind($service->bind);
+ }
+
+ $this->services[] = $service;
+ }
+
+ /**
+ * 执行服务
+ * @access public
+ * @param Service $service 服务
+ * @return mixed
+ */
+ public function bootService(Service $service)
+ {
+ if (method_exists($service, 'boot')) {
+ return $this->invoke([$service, 'boot']);
+ }
+ }
+
+ /**
+ * 获取服务
+ * @param string|Service $service
+ * @return Service|null
+ */
+ public function getService(Service | string $service): ?Service
+ {
+ $name = is_string($service) ? $service : $service::class;
+ return array_values(array_filter($this->services, function ($value) use ($name) {
+ return $value instanceof $name;
+ }, ARRAY_FILTER_USE_BOTH))[0] ?? null;
+ }
+
+ /**
+ * 开启应用调试模式
+ * @access public
+ * @param bool $debug 开启应用调试模式
+ * @return $this
+ */
+ public function debug(bool $debug = true)
+ {
+ $this->appDebug = $debug;
+ return $this;
+ }
+
+ /**
+ * 是否为调试模式
+ * @access public
+ * @return bool
+ */
+ public function isDebug(): bool
+ {
+ return $this->appDebug;
+ }
+
+ /**
+ * 设置应用命名空间
+ * @access public
+ * @param string $namespace 应用命名空间
+ * @return $this
+ */
+ public function setNamespace(string $namespace)
+ {
+ $this->namespace = $namespace;
+ return $this;
+ }
+
+ /**
+ * 获取应用类库命名空间
+ * @access public
+ * @return string
+ */
+ public function getNamespace(): string
+ {
+ return $this->namespace;
+ }
+
+ /**
+ * 设置公共环境变量标识
+ * @access public
+ * @param string $name 环境标识
+ * @return $this
+ */
+ public function setBaseEnvName(string $name)
+ {
+ $this->baseEnvName = $name;
+ return $this;
+ }
+
+ /**
+ * 设置环境变量标识
+ * @access public
+ * @param string $name 环境标识
+ * @return $this
+ */
+ public function setEnvName(string $name)
+ {
+ $this->envName = $name;
+ return $this;
+ }
+
+ /**
+ * 获取框架版本
+ * @access public
+ * @return string
+ */
+ public function version(): string
+ {
+ return ltrim(InstalledVersions::getPrettyVersion('topthink/framework'), 'v');
+ }
+
+ /**
+ * 获取应用根目录
+ * @access public
+ * @return string
+ */
+ public function getRootPath(): string
+ {
+ return $this->rootPath;
+ }
+
+ /**
+ * 获取应用基础目录
+ * @access public
+ * @return string
+ */
+ public function getBasePath(): string
+ {
+ return $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
+ }
+
+ /**
+ * 获取当前应用目录
+ * @access public
+ * @return string
+ */
+ public function getAppPath(): string
+ {
+ return $this->appPath;
+ }
+
+ /**
+ * 设置应用目录
+ * @param string $path 应用目录
+ */
+ public function setAppPath(string $path)
+ {
+ $this->appPath = $path;
+ }
+
+ /**
+ * 获取应用运行时目录
+ * @access public
+ * @return string
+ */
+ public function getRuntimePath(): string
+ {
+ return $this->runtimePath;
+ }
+
+ /**
+ * 设置runtime目录
+ * @param string $path 定义目录
+ */
+ public function setRuntimePath(string $path): void
+ {
+ $this->runtimePath = $path;
+ }
+
+ /**
+ * 获取核心框架目录
+ * @access public
+ * @return string
+ */
+ public function getThinkPath(): string
+ {
+ return $this->thinkPath;
+ }
+
+ /**
+ * 获取应用配置目录
+ * @access public
+ * @return string
+ */
+ public function getConfigPath(): string
+ {
+ return $this->rootPath . 'config' . DIRECTORY_SEPARATOR;
+ }
+
+ /**
+ * 获取配置后缀
+ * @access public
+ * @return string
+ */
+ public function getConfigExt(): string
+ {
+ return $this->configExt;
+ }
+
+ /**
+ * 获取应用开启时间
+ * @access public
+ * @return float
+ */
+ public function getBeginTime(): float
+ {
+ return $this->beginTime;
+ }
+
+ /**
+ * 获取应用初始内存占用
+ * @access public
+ * @return integer
+ */
+ public function getBeginMem(): int
+ {
+ return $this->beginMem;
+ }
+
+ /**
+ * 加载环境变量定义
+ * @access public
+ * @param string $envName 环境标识
+ * @return void
+ */
+ public function loadEnv(string $envName = ''): void
+ {
+ // 加载环境变量
+ $envFile = $envName ? $this->rootPath . '.env.' . $envName : $this->rootPath . '.env';
+
+ if (is_file($envFile)) {
+ $this->env->load($envFile);
+ }
+ }
+
+ /**
+ * 初始化应用
+ * @access public
+ * @return $this
+ */
+ public function initialize()
+ {
+ $this->initialized = true;
+
+ $this->beginTime = microtime(true);
+ $this->beginMem = memory_get_usage();
+
+ // 加载环境变量
+ if ($this->baseEnvName) {
+ $this->loadEnv($this->baseEnvName);
+ }
+
+ $this->envName = $this->envName ?: (string) $this->env->get('env_name', '');
+ $this->loadEnv($this->envName);
+
+ $this->configExt = $this->env->get('config_ext', '.php');
+
+ $this->debugModeInit();
+
+ // 加载全局初始化文件
+ $this->load();
+
+ // 加载应用默认语言包
+ $this->loadLangPack();
+
+ // 监听AppInit
+ $this->event->trigger(AppInit::class);
+
+ date_default_timezone_set($this->config->get('app.default_timezone', 'Asia/Shanghai'));
+
+ // 初始化
+ foreach ($this->initializers as $initializer) {
+ $this->make($initializer)->init($this);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 是否初始化过
+ * @return bool
+ */
+ public function initialized()
+ {
+ return $this->initialized;
+ }
+
+ /**
+ * 加载语言包
+ * @return void
+ */
+ public function loadLangPack(): void
+ {
+ // 加载默认语言包
+ $langSet = $this->lang->defaultLangSet();
+ $this->lang->switchLangSet($langSet);
+ }
+
+ /**
+ * 引导应用
+ * @access public
+ * @return void
+ */
+ public function boot(): void
+ {
+ array_walk($this->services, function ($service) {
+ $this->bootService($service);
+ });
+ }
+
+ /**
+ * 加载应用文件和配置
+ * @access protected
+ * @return void
+ */
+ protected function load(): void
+ {
+ $appPath = $this->getAppPath();
+
+ if (is_file($appPath . 'common.php')) {
+ include_once $appPath . 'common.php';
+ }
+
+ include_once $this->thinkPath . 'helper.php';
+
+ if (is_file($this->runtimePath . 'config.php')) {
+ $this->config->set(include $this->runtimePath . 'config.php');
+ } else {
+ $this->loadConfig();
+ }
+
+ if (is_file($appPath . 'event.php')) {
+ $this->loadEvent(include $appPath . 'event.php');
+ }
+
+ if (is_file($appPath . 'service.php')) {
+ $services = include $appPath . 'service.php';
+ foreach ($services as $service) {
+ $this->register($service);
+ }
+ }
+ }
+
+ /**
+ * 加载配置文件
+ * @return void
+ */
+ public function loadConfig()
+ {
+ $configPath = $this->getConfigPath();
+ $files = [];
+
+ if (is_dir($configPath)) {
+ $files = glob($configPath . '*' . $this->configExt);
+ }
+
+ foreach ($files as $file) {
+ $this->config->load($file, pathinfo($file, PATHINFO_FILENAME));
+ }
+ }
+
+ /**
+ * 调试模式设置
+ * @access protected
+ * @return void
+ */
+ protected function debugModeInit(): void
+ {
+ // 应用调试模式
+ if (!$this->appDebug) {
+ $this->appDebug = $this->env->get('app_debug') ? true : false;
+ }
+
+ if (!$this->appDebug) {
+ ini_set('display_errors', 'Off');
+ }
+
+ if (!$this->runningInConsole()) {
+ //重新申请一块比较大的buffer
+ if (ob_get_level() > 0) {
+ $output = ob_get_clean();
+ }
+ ob_start();
+ if (!empty($output)) {
+ echo $output;
+ }
+ }
+ }
+
+ /**
+ * 注册应用事件
+ * @access protected
+ * @param array $event 事件数据
+ * @return void
+ */
+ public function loadEvent(array $event): void
+ {
+ if (isset($event['bind'])) {
+ $this->event->bind($event['bind']);
+ }
+
+ if (isset($event['listen'])) {
+ $this->event->listenEvents($event['listen']);
+ }
+
+ if (isset($event['subscribe'])) {
+ $this->event->subscribe($event['subscribe']);
+ }
+ }
+
+ /**
+ * 解析应用类的类名
+ * @access public
+ * @param string $layer 层名 controller model ...
+ * @param string $name 类名
+ * @return string
+ */
+ public function parseClass(string $layer, string $name): string
+ {
+ $name = str_replace(['/', '.'], '\\', $name);
+ $array = explode('\\', $name);
+ $class = Str::studly(array_pop($array));
+ $path = $array ? implode('\\', $array) . '\\' : '';
+
+ return $this->namespace . '\\' . $layer . '\\' . $path . $class;
+ }
+
+ /**
+ * 是否运行在命令行下
+ * @return bool
+ */
+ public function runningInConsole(): bool
+ {
+ return php_sapi_name() === 'cli' || php_sapi_name() === 'phpdbg';
+ }
+
+ /**
+ * 获取应用根目录
+ * @access protected
+ * @return string
+ */
+ protected function getDefaultRootPath(): string
+ {
+ return dirname($this->thinkPath, 4) . DIRECTORY_SEPARATOR;
+ }
+}
diff --git a/src/think/Cache.php b/src/think/Cache.php
new file mode 100644
index 0000000..bd7ed4f
--- /dev/null
+++ b/src/think/Cache.php
@@ -0,0 +1,200 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types = 1);
+
+namespace think;
+
+use DateInterval;
+use DateTimeInterface;
+use Psr\SimpleCache\CacheInterface;
+use think\cache\Driver;
+use think\cache\TagSet;
+use think\exception\InvalidArgumentException;
+use think\helper\Arr;
+
+/**
+ * 缓存管理类
+ * @mixin Driver
+ * @mixin \think\cache\driver\File
+ */
+class Cache extends Manager implements CacheInterface
+{
+
+ protected $namespace = '\\think\\cache\\driver\\';
+
+ /**
+ * 默认驱动
+ * @return string|null
+ */
+ public function getDefaultDriver(): ?string
+ {
+ return $this->getConfig('default');
+ }
+
+ /**
+ * 获取缓存配置
+ * @access public
+ * @param null|string $name 名称
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getConfig(?string $name = null, $default = null)
+ {
+ if (!is_null($name)) {
+ return $this->app->config->get('cache.' . $name, $default);
+ }
+
+ return $this->app->config->get('cache');
+ }
+
+ /**
+ * 获取驱动配置
+ * @param string $store
+ * @param string $name
+ * @param mixed $default
+ * @return array
+ */
+ public function getStoreConfig(string $store, ?string $name = null, $default = null)
+ {
+ if ($config = $this->getConfig("stores.{$store}")) {
+ return Arr::get($config, $name, $default);
+ }
+
+ throw new \InvalidArgumentException("Store [$store] not found.");
+ }
+
+ protected function resolveType(string $name)
+ {
+ return $this->getStoreConfig($name, 'type', 'file');
+ }
+
+ protected function resolveConfig(string $name)
+ {
+ return $this->getStoreConfig($name);
+ }
+
+ /**
+ * 连接或者切换缓存
+ * @access public
+ * @param string|null $name 连接配置名
+ * @return Driver
+ */
+ public function store(?string $name = null)
+ {
+ return $this->driver($name);
+ }
+
+ /**
+ * 清空缓冲池
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ return $this->store()->clear();
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $key 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($key, mixed $default = null): mixed
+ {
+ return $this->store()->get($key, $default);
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $key 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int|DateTimeInterface|DateInterval $ttl 有效时间 0为永久
+ * @return bool
+ */
+ public function set($key, $value, $ttl = null): bool
+ {
+ return $this->store()->set($key, $value, $ttl);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $key 缓存变量名
+ * @return bool
+ */
+ public function delete($key): bool
+ {
+ return $this->store()->delete($key);
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param iterable $keys 缓存变量名
+ * @param mixed $default 默认值
+ * @return iterable
+ * @throws InvalidArgumentException
+ */
+ public function getMultiple($keys, $default = null): iterable
+ {
+ return $this->store()->getMultiple($keys, $default);
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param iterable $values 缓存数据
+ * @param null|int|\DateInterval $ttl 有效时间 0为永久
+ * @return bool
+ */
+ public function setMultiple($values, $ttl = null): bool
+ {
+ return $this->store()->setMultiple($values, $ttl);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param iterable $keys 缓存变量名
+ * @return bool
+ * @throws InvalidArgumentException
+ */
+ public function deleteMultiple($keys): bool
+ {
+ return $this->store()->deleteMultiple($keys);
+ }
+
+ /**
+ * 判断缓存是否存在
+ * @access public
+ * @param string $key 缓存变量名
+ * @return bool
+ */
+ public function has($key): bool
+ {
+ return $this->store()->has($key);
+ }
+
+ /**
+ * 缓存标签
+ * @access public
+ * @param string|array $name 标签名
+ * @return TagSet
+ */
+ public function tag($name)
+ {
+ return $this->store()->tag($name);
+ }
+
+}
diff --git a/src/think/Config.php b/src/think/Config.php
new file mode 100644
index 0000000..1f49dc8
--- /dev/null
+++ b/src/think/Config.php
@@ -0,0 +1,218 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Closure;
+
+/**
+ * 配置管理类
+ * @package think
+ */
+class Config
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [];
+
+ /**
+ * 注册配置获取器
+ * @var Closure[]
+ */
+ protected $hook = [];
+
+ /**
+ * 构造方法
+ * @access public
+ */
+ public function __construct(protected string $path = '', protected string $ext = '.php')
+ {
+ }
+
+ public static function __make(App $app)
+ {
+ $path = $app->getConfigPath();
+ $ext = $app->getConfigExt();
+
+ return new static($path, $ext);
+ }
+
+ /**
+ * 加载配置文件(多种格式)
+ * @access public
+ * @param string $file 配置文件名
+ * @param string $name 一级配置名
+ * @return array
+ */
+ public function load(string $file, string $name = ''): array
+ {
+ if (is_file($file)) {
+ $filename = $file;
+ } elseif (is_file($this->path . $file . $this->ext)) {
+ $filename = $this->path . $file . $this->ext;
+ }
+
+ if (isset($filename)) {
+ return $this->parse($filename, $name);
+ }
+
+ return $this->config;
+ }
+
+ /**
+ * 解析配置文件
+ * @access public
+ * @param string $file 配置文件名
+ * @param string $name 一级配置名
+ * @return array
+ */
+ protected function parse(string $file, string $name): array
+ {
+ $type = pathinfo($file, PATHINFO_EXTENSION);
+ $config = [];
+ $config = match ($type) {
+ 'php' => include $file,
+ 'yml', 'yaml' => function_exists('yaml_parse_file') ? yaml_parse_file($file) : [],
+ 'ini' => parse_ini_file($file, true, INI_SCANNER_TYPED) ?: [],
+ 'json' => json_decode(file_get_contents($file), true),
+ default => [],
+ };
+
+ return is_array($config) ? $this->set($config, strtolower($name)) : [];
+ }
+
+ /**
+ * 检测配置是否存在
+ * @access public
+ * @param string $name 配置参数名(支持多级配置 .号分割)
+ * @return bool
+ */
+ public function has(string $name): bool
+ {
+ if (!str_contains($name, '.') && !isset($this->config[strtolower($name)])) {
+ return false;
+ }
+
+ return !is_null($this->get($name));
+ }
+
+ /**
+ * 获取一级配置
+ * @access protected
+ * @param string $name 一级配置名
+ * @return array
+ */
+ protected function pull(string $name): array
+ {
+ return $this->config[$name] ?? [];
+ }
+
+ /**
+ * 注册配置获取器
+ * @access public
+ * @param Closure $callback
+ * @param string|null $key
+ * @return void
+ */
+ public function hook(Closure $callback, ?string $key = null)
+ {
+ $this->hook[$key ?? 'global'] = $callback;
+ }
+
+ /**
+ * 获取配置参数 为空则获取所有配置
+ * @access public
+ * @param string $name 配置参数名(支持多级配置 .号分割)
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get(?string $name = null, $default = null)
+ {
+ // 无参数时获取所有
+ if (empty($name)) {
+ return $this->config;
+ }
+
+ if (!str_contains($name, '.')) {
+ $name = strtolower($name);
+ $result = $this->pull($name);
+ return $this->hook ? $this->lazy($name, $result, []) : $result;
+ }
+
+ $item = explode('.', $name);
+ $item[0] = strtolower($item[0]);
+ $config = $this->config;
+
+ foreach ($item as $val) {
+ if (isset($config[$val])) {
+ $config = $config[$val];
+ } else {
+ return $this->hook ? $this->lazy($name, null, $default) : $default;
+ }
+ }
+
+ return $this->hook ? $this->lazy($name, $config, $default) : $config;
+ }
+
+ /**
+ * 通过获取器加载配置
+ * @access public
+ * @param string $name 配置参数
+ * @param mixed $value 配置值
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ protected function lazy(string $name, $value = null, $default = null)
+ {
+ // 通过获取器返回
+ $key = strpos($name, '.') ? strstr($name, '.', true) : $name;
+ if (isset($this->hook[$key])) {
+ $call = $this->hook[$key];
+ } elseif (isset($this->hook['global'])) {
+ $call = $this->hook['global'];
+ }
+ if (isset($call)) {
+ $result = call_user_func_array($call, [$name, $value]);
+ if (is_null($result)) {
+ return $default;
+ }
+ }
+ return $result ?? ($value ?: $default);
+ }
+
+ /**
+ * 设置配置参数 name为数组则为批量设置
+ * @access public
+ * @param array $config 配置参数
+ * @param string $name 配置名
+ * @return array
+ */
+ public function set(array $config, ?string $name = null): array
+ {
+ if (empty($name)) {
+ $this->config = array_merge($this->config, array_change_key_case($config));
+ return $this->config;
+ }
+
+ if (isset($this->config[$name])) {
+ $result = array_merge($this->config[$name], $config);
+ } else {
+ $result = $config;
+ }
+
+ $this->config[$name] = $result;
+
+ return $result;
+ }
+}
diff --git a/src/think/Console.php b/src/think/Console.php
new file mode 100644
index 0000000..c69880b
--- /dev/null
+++ b/src/think/Console.php
@@ -0,0 +1,789 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Closure;
+use InvalidArgumentException;
+use LogicException;
+use think\console\Command;
+use think\console\command\Clear;
+use think\console\command\Help;
+use think\console\command\Help as HelpCommand;
+use think\console\command\Lists;
+use think\console\command\make\Command as MakeCommand;
+use think\console\command\make\Controller;
+use think\console\command\make\Event;
+use think\console\command\make\Listener;
+use think\console\command\make\Middleware;
+use think\console\command\make\Model;
+use think\console\command\make\Service;
+use think\console\command\make\Subscribe;
+use think\console\command\make\Validate;
+use think\console\command\optimize\Config;
+use think\console\command\optimize\Route;
+use think\console\command\optimize\Schema;
+use think\console\command\RouteList;
+use think\console\command\RunServer;
+use think\console\command\ServiceDiscover;
+use think\console\command\VendorPublish;
+use think\console\command\Version;
+use think\console\Input;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Definition as InputDefinition;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+use think\console\output\driver\Buffer;
+
+/**
+ * 控制台应用管理类
+ */
+class Console
+{
+ /** @var Command[] */
+ protected $commands = [];
+
+ protected $wantHelps = false;
+
+ protected $catchExceptions = true;
+ protected $autoExit = true;
+ protected $definition;
+ protected $defaultCommand = 'list';
+
+ protected $defaultCommands = [
+ 'help' => Help::class,
+ 'list' => Lists::class,
+ 'clear' => Clear::class,
+ 'make:command' => MakeCommand::class,
+ 'make:controller' => Controller::class,
+ 'make:model' => Model::class,
+ 'make:middleware' => Middleware::class,
+ 'make:validate' => Validate::class,
+ 'make:event' => Event::class,
+ 'make:listener' => Listener::class,
+ 'make:service' => Service::class,
+ 'make:subscribe' => Subscribe::class,
+ 'optimize:config' => Config::class,
+ 'optimize:route' => Route::class,
+ 'optimize:schema' => Schema::class,
+ 'run' => RunServer::class,
+ 'version' => Version::class,
+ 'route:list' => RouteList::class,
+ 'service:discover' => ServiceDiscover::class,
+ 'vendor:publish' => VendorPublish::class,
+ ];
+
+ /**
+ * 启动器
+ * @var array
+ */
+ protected static $startCallbacks = [];
+
+ public function __construct(protected App $app)
+ {
+ $this->initialize();
+
+ $this->definition = $this->getDefaultInputDefinition();
+
+ //加载指令
+ $this->loadCommands();
+
+ // 设置执行用户
+ $user = $this->app->config->get('console.user');
+ if (!empty($user)) {
+ $this->setUser($user);
+ }
+
+ $this->start();
+ }
+
+ /**
+ * 初始化
+ */
+ protected function initialize():void
+ {
+ if (!$this->app->initialized()) {
+ $this->app->initialize();
+ }
+ $this->makeRequest();
+ }
+
+ /**
+ * 构造request
+ */
+ protected function makeRequest():void
+ {
+ $url = $this->app->config->get('app.url', 'http://localhost');
+
+ $components = parse_url($url);
+
+ $server = $_SERVER;
+
+ if (isset($components['path'])) {
+ $server = array_merge($server, [
+ 'SCRIPT_FILENAME' => $components['path'],
+ 'SCRIPT_NAME' => $components['path'],
+ 'REQUEST_URI' => $components['path'],
+ ]);
+ }
+
+ if (isset($components['host'])) {
+ $server['SERVER_NAME'] = $components['host'];
+ $server['HTTP_HOST'] = $components['host'];
+ }
+
+ if (isset($components['scheme'])) {
+ if ('https' === $components['scheme']) {
+ $server['HTTPS'] = 'on';
+ $server['SERVER_PORT'] = 443;
+ } else {
+ unset($server['HTTPS']);
+ $server['SERVER_PORT'] = 80;
+ }
+ }
+
+ if (isset($components['port'])) {
+ $server['SERVER_PORT'] = $components['port'];
+ $server['HTTP_HOST'] .= ':' . $components['port'];
+ }
+
+ /** @var Request $request */
+ $request = $this->app->make('request');
+
+ $request->withServer($server);
+ }
+
+ /**
+ * 添加初始化器
+ * @param Closure $callback
+ */
+ public static function starting(Closure $callback): void
+ {
+ static::$startCallbacks[] = $callback;
+ }
+
+ /**
+ * 清空启动器
+ */
+ public static function flushStartCallbacks(): void
+ {
+ static::$startCallbacks = [];
+ }
+
+ /**
+ * 设置执行用户
+ * @param $user
+ */
+ public static function setUser(string $user): void
+ {
+ if (extension_loaded('posix')) {
+ $user = posix_getpwnam($user);
+
+ if (!empty($user)) {
+ posix_setgid($user['gid']);
+ posix_setuid($user['uid']);
+ }
+ }
+ }
+
+ /**
+ * 启动
+ */
+ protected function start(): void
+ {
+ foreach (static::$startCallbacks as $callback) {
+ $callback($this);
+ }
+ }
+
+ /**
+ * 加载指令
+ * @access protected
+ */
+ protected function loadCommands(): void
+ {
+ $commands = $this->app->config->get('console.commands', []);
+ $commands = array_merge($this->defaultCommands, $commands);
+
+ $this->addCommands($commands);
+ }
+
+ /**
+ * @access public
+ * @param string $command
+ * @param array $parameters
+ * @param string $driver
+ * @return Output|Buffer
+ */
+ public function call(string $command, array $parameters = [], string $driver = 'buffer')
+ {
+ array_unshift($parameters, $command);
+
+ $input = new Input($parameters);
+ $output = new Output($driver);
+
+ $this->setCatchExceptions(false);
+ $this->find($command)->run($input, $output);
+
+ return $output;
+ }
+
+ /**
+ * 执行当前的指令
+ * @access public
+ * @return int
+ * @throws \Exception
+ * @api
+ */
+ public function run()
+ {
+ $input = new Input();
+ $output = new Output();
+
+ $this->configureIO($input, $output);
+
+ try {
+ $exitCode = $this->doRun($input, $output);
+ } catch (\Exception $e) {
+ if (!$this->catchExceptions) {
+ throw $e;
+ }
+
+ $output->renderException($e);
+
+ $exitCode = $e->getCode();
+ if (is_numeric($exitCode)) {
+ $exitCode = (int) $exitCode;
+ if (0 === $exitCode) {
+ $exitCode = 1;
+ }
+ } else {
+ $exitCode = 1;
+ }
+ }
+
+ if ($this->autoExit) {
+ if ($exitCode > 255) {
+ $exitCode = 255;
+ }
+
+ exit($exitCode);
+ }
+
+ return $exitCode;
+ }
+
+ /**
+ * 执行指令
+ * @access public
+ * @param Input $input
+ * @param Output $output
+ * @return int
+ */
+ public function doRun(Input $input, Output $output)
+ {
+ if (true === $input->hasParameterOption(['--version', '-V'])) {
+ $output->writeln($this->getLongVersion());
+
+ return 0;
+ }
+
+ $name = $this->getCommandName($input);
+
+ if (true === $input->hasParameterOption(['--help', '-h'])) {
+ if (!$name) {
+ $name = 'help';
+ $input = new Input(['help']);
+ } else {
+ $this->wantHelps = true;
+ }
+ }
+
+ if (!$name) {
+ $name = $this->defaultCommand;
+ $input = new Input([$this->defaultCommand]);
+ }
+
+ $command = $this->find($name);
+
+ return $this->doRunCommand($command, $input, $output);
+ }
+
+ /**
+ * 设置输入参数定义
+ * @access public
+ * @param InputDefinition $definition
+ */
+ public function setDefinition(InputDefinition $definition): void
+ {
+ $this->definition = $definition;
+ }
+
+ /**
+ * 获取输入参数定义
+ * @access public
+ * @return InputDefinition The InputDefinition instance
+ */
+ public function getDefinition(): InputDefinition
+ {
+ return $this->definition;
+ }
+
+ /**
+ * Gets the help message.
+ * @access public
+ * @return string A help message.
+ */
+ public function getHelp(): string
+ {
+ return $this->getLongVersion();
+ }
+
+ /**
+ * 是否捕获异常
+ * @access public
+ * @param bool $boolean
+ * @api
+ */
+ public function setCatchExceptions(bool $boolean): void
+ {
+ $this->catchExceptions = $boolean;
+ }
+
+ /**
+ * 是否自动退出
+ * @access public
+ * @param bool $boolean
+ * @api
+ */
+ public function setAutoExit(bool $boolean): void
+ {
+ $this->autoExit = $boolean;
+ }
+
+ /**
+ * 获取完整的版本号
+ * @access public
+ * @return string
+ */
+ public function getLongVersion(): string
+ {
+ if ($this->app->version()) {
+ return sprintf('version %s', $this->app->version());
+ }
+
+ return 'Console Tool';
+ }
+
+ /**
+ * 添加指令集
+ * @access public
+ * @param array $commands
+ */
+ public function addCommands(array $commands): void
+ {
+ foreach ($commands as $key => $command) {
+ if (is_subclass_of($command, Command::class)) {
+ // 注册指令
+ $this->addCommand($command, is_numeric($key) ? '' : $key);
+ }
+ }
+ }
+
+ /**
+ * 添加一个指令
+ * @access public
+ * @param string|Command $command 指令对象或者指令类名
+ * @param string $name 指令名 留空则自动获取
+ * @return Command|void
+ */
+ public function addCommand(string|Command $command, string $name = '')
+ {
+ if ($name) {
+ $this->commands[$name] = $command;
+ return;
+ }
+
+ if (is_string($command)) {
+ $command = $this->app->invokeClass($command);
+ }
+
+ $command->setConsole($this);
+
+ if (!$command->isEnabled()) {
+ $command->setConsole(null);
+ return;
+ }
+
+ $command->setApp($this->app);
+
+ if (null === $command->getDefinition()) {
+ throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', $command::class));
+ }
+
+ $this->commands[$command->getName()] = $command;
+
+ foreach ($command->getAliases() as $alias) {
+ $this->commands[$alias] = $command;
+ }
+
+ return $command;
+ }
+
+ /**
+ * 获取指令
+ * @access public
+ * @param string $name 指令名称
+ * @return Command
+ * @throws InvalidArgumentException
+ */
+ public function getCommand(string $name): Command
+ {
+ if (!isset($this->commands[$name])) {
+ throw new InvalidArgumentException(sprintf('The command "%s" does not exist.', $name));
+ }
+
+ $command = $this->commands[$name];
+
+ if (is_string($command)) {
+ $command = $this->app->invokeClass($command);
+ /** @var Command $command */
+ $command->setConsole($this);
+ $command->setApp($this->app);
+ }
+
+ if ($this->wantHelps) {
+ $this->wantHelps = false;
+
+ /** @var HelpCommand $helpCommand */
+ $helpCommand = $this->getCommand('help');
+ $helpCommand->setCommand($command);
+
+ return $helpCommand;
+ }
+
+ return $command;
+ }
+
+ /**
+ * 某个指令是否存在
+ * @access public
+ * @param string $name 指令名称
+ * @return bool
+ */
+ public function hasCommand(string $name): bool
+ {
+ return isset($this->commands[$name]);
+ }
+
+ /**
+ * 获取所有的命名空间
+ * @access public
+ * @return array
+ */
+ public function getNamespaces(): array
+ {
+ $namespaces = [];
+ foreach ($this->commands as $key => $command) {
+ if (is_string($command)) {
+ $namespaces = array_merge($namespaces, $this->extractAllNamespaces($key));
+ } else {
+ $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
+
+ foreach ($command->getAliases() as $alias) {
+ $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias));
+ }
+ }
+ }
+
+ return array_values(array_unique(array_filter($namespaces)));
+ }
+
+ /**
+ * 查找注册命名空间中的名称或缩写。
+ * @access public
+ * @param string $namespace
+ * @return string
+ * @throws InvalidArgumentException
+ */
+ public function findNamespace(string $namespace): string
+ {
+ $allNamespaces = $this->getNamespaces();
+ $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
+ return preg_quote($matches[1]) . '[^:]*';
+ }, $namespace);
+ $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces);
+
+ if (empty($namespaces)) {
+ $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
+
+ if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
+ if (1 == count($alternatives)) {
+ $message .= "\n\nDid you mean this?\n ";
+ } else {
+ $message .= "\n\nDid you mean one of these?\n ";
+ }
+
+ $message .= implode("\n ", $alternatives);
+ }
+
+ throw new InvalidArgumentException($message);
+ }
+
+ $exact = in_array($namespace, $namespaces, true);
+ if (count($namespaces) > 1 && !$exact) {
+ throw new InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))));
+ }
+
+ return $exact ? $namespace : reset($namespaces);
+ }
+
+ /**
+ * 查找指令
+ * @access public
+ * @param string $name 名称或者别名
+ * @return Command
+ * @throws InvalidArgumentException
+ */
+ public function find(string $name): Command
+ {
+ $allCommands = array_keys($this->commands);
+
+ $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
+ return preg_quote($matches[1]) . '[^:]*';
+ }, $name);
+
+ $commands = preg_grep('{^' . $expr . '}', $allCommands);
+
+ if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) {
+ if (false !== $pos = strrpos($name, ':')) {
+ $this->findNamespace(substr($name, 0, $pos));
+ }
+
+ $message = sprintf('Command "%s" is not defined.', $name);
+
+ if ($alternatives = $this->findAlternatives($name, $allCommands)) {
+ if (1 == count($alternatives)) {
+ $message .= "\n\nDid you mean this?\n ";
+ } else {
+ $message .= "\n\nDid you mean one of these?\n ";
+ }
+ $message .= implode("\n ", $alternatives);
+ }
+
+ throw new InvalidArgumentException($message);
+ }
+
+ $exact = in_array($name, $commands, true);
+ if (count($commands) > 1 && !$exact) {
+ $suggestions = $this->getAbbreviationSuggestions(array_values($commands));
+
+ throw new InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions));
+ }
+
+ return $this->getCommand($exact ? $name : reset($commands));
+ }
+
+ /**
+ * 获取所有的指令
+ * @access public
+ * @param string $namespace 命名空间
+ * @return Command[]
+ * @api
+ */
+ public function all(?string $namespace = null): array
+ {
+ if (null === $namespace) {
+ return $this->commands;
+ }
+
+ $commands = [];
+ foreach ($this->commands as $name => $command) {
+ if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) {
+ $commands[$name] = $command;
+ }
+ }
+
+ return $commands;
+ }
+
+ /**
+ * 配置基于用户的参数和选项的输入和输出实例。
+ * @access protected
+ * @param Input $input 输入实例
+ * @param Output $output 输出实例
+ */
+ protected function configureIO(Input $input, Output $output): void
+ {
+ if (true === $input->hasParameterOption(['--ansi'])) {
+ $output->setDecorated(true);
+ } elseif (true === $input->hasParameterOption(['--no-ansi'])) {
+ $output->setDecorated(false);
+ }
+
+ if (true === $input->hasParameterOption(['--no-interaction', '-n'])) {
+ $input->setInteractive(false);
+ }
+
+ if (true === $input->hasParameterOption(['--quiet', '-q'])) {
+ $output->setVerbosity(Output::VERBOSITY_QUIET);
+ } elseif ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) {
+ $output->setVerbosity(Output::VERBOSITY_DEBUG);
+ } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) {
+ $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE);
+ } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) {
+ $output->setVerbosity(Output::VERBOSITY_VERBOSE);
+ }
+ }
+
+ /**
+ * 执行指令
+ * @access protected
+ * @param Command $command 指令实例
+ * @param Input $input 输入实例
+ * @param Output $output 输出实例
+ * @return int
+ * @throws \Exception
+ */
+ protected function doRunCommand(Command $command, Input $input, Output $output)
+ {
+ return $command->run($input, $output);
+ }
+
+ /**
+ * 获取指令的基础名称
+ * @access protected
+ * @param Input $input
+ * @return string
+ */
+ protected function getCommandName(Input $input): string
+ {
+ return $input->getFirstArgument() ?: '';
+ }
+
+ /**
+ * 获取默认输入定义
+ * @access protected
+ * @return InputDefinition
+ */
+ protected function getDefaultInputDefinition(): InputDefinition
+ {
+ return new InputDefinition([
+ new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
+ new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
+ new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'),
+ new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
+ new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
+ new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
+ new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
+ new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
+ ]);
+ }
+
+ /**
+ * 获取可能的建议
+ * @access private
+ * @param array $abbrevs
+ * @return string
+ */
+ private function getAbbreviationSuggestions(array $abbrevs): string
+ {
+ return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
+ }
+
+ /**
+ * 返回命名空间部分
+ * @access public
+ * @param string $name 指令
+ * @param int $limit 部分的命名空间的最大数量
+ * @return string
+ */
+ public function extractNamespace(string $name, int $limit = 0): string
+ {
+ $parts = explode(':', $name);
+ array_pop($parts);
+
+ return implode(':', 0 === $limit ? $parts : array_slice($parts, 0, $limit));
+ }
+
+ /**
+ * 查找可替代的建议
+ * @access private
+ * @param string $name
+ * @param array|\Traversable $collection
+ * @return array
+ */
+ private function findAlternatives(string $name, array|\Traversable $collection): array
+ {
+ $threshold = 1e3;
+ $alternatives = [];
+
+ $collectionParts = [];
+ foreach ($collection as $item) {
+ $collectionParts[$item] = explode(':', $item);
+ }
+
+ foreach (explode(':', $name) as $i => $subname) {
+ foreach ($collectionParts as $collectionName => $parts) {
+ $exists = isset($alternatives[$collectionName]);
+ if (!isset($parts[$i]) && $exists) {
+ $alternatives[$collectionName] += $threshold;
+ continue;
+ } elseif (!isset($parts[$i])) {
+ continue;
+ }
+
+ $lev = levenshtein($subname, $parts[$i]);
+ if ($lev <= strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) {
+ $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
+ } elseif ($exists) {
+ $alternatives[$collectionName] += $threshold;
+ }
+ }
+ }
+
+ foreach ($collection as $item) {
+ $lev = levenshtein($name, $item);
+ if ($lev <= strlen($name) / 3 || str_contains($item, $name)) {
+ $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
+ }
+ }
+
+ $alternatives = array_filter($alternatives, function ($lev) use ($threshold) {
+ return $lev < 2 * $threshold;
+ });
+ asort($alternatives);
+
+ return array_keys($alternatives);
+ }
+
+ /**
+ * 返回所有的命名空间
+ * @access private
+ * @param string $name
+ * @return array
+ */
+ private function extractAllNamespaces(string $name): array
+ {
+ $parts = explode(':', $name, -1);
+ $namespaces = [];
+
+ foreach ($parts as $part) {
+ if (count($namespaces)) {
+ $namespaces[] = end($namespaces) . ':' . $part;
+ } else {
+ $namespaces[] = $part;
+ }
+ }
+
+ return $namespaces;
+ }
+
+}
diff --git a/src/think/Cookie.php b/src/think/Cookie.php
new file mode 100644
index 0000000..585d812
--- /dev/null
+++ b/src/think/Cookie.php
@@ -0,0 +1,223 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types=1);
+
+namespace think;
+
+use DateTimeInterface;
+
+/**
+ * Cookie管理类
+ * @package think
+ */
+class Cookie
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [
+ // cookie 保存时间
+ 'expire' => 0,
+ // cookie 保存路径
+ 'path' => '/',
+ // cookie 有效域名
+ 'domain' => '',
+ // cookie 启用安全传输
+ 'secure' => false,
+ // httponly设置
+ 'httponly' => false,
+ // samesite 设置,支持 'strict' 'lax'
+ 'samesite' => '',
+ ];
+
+ /**
+ * Cookie写入数据
+ * @var array
+ */
+ protected $cookie = [];
+
+ /**
+ * 构造方法
+ * @access public
+ */
+ public function __construct(protected Request $request, array $config = [])
+ {
+ $this->config = array_merge($this->config, array_change_key_case($config));
+ }
+
+ public static function __make(Request $request, Config $config)
+ {
+ return new static($request, $config->get('cookie'));
+ }
+
+ /**
+ * 获取cookie
+ * @access public
+ * @param mixed $name 数据名称
+ * @param string $default 默认值
+ * @return mixed
+ */
+ public function get(string $name = '', $default = null)
+ {
+ return $this->request->cookie($name, $default);
+ }
+
+ /**
+ * 是否存在Cookie参数
+ * @access public
+ * @param string $name 变量名
+ * @return bool
+ */
+ public function has(string $name): bool
+ {
+ return $this->request->has($name, 'cookie');
+ }
+
+ /**
+ * Cookie 设置
+ *
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param mixed $option 可选参数
+ * @return void
+ */
+ public function set(string $name, string $value, $option = null): void
+ {
+ // 参数设置(会覆盖黙认设置)
+ if (!is_null($option)) {
+ if (is_numeric($option) || $option instanceof DateTimeInterface) {
+ $option = ['expire' => $option];
+ }
+
+ $config = array_merge($this->config, array_change_key_case($option));
+ } else {
+ $config = $this->config;
+ }
+
+ if ($config['expire'] instanceof DateTimeInterface) {
+ $expire = $config['expire']->getTimestamp();
+ } else {
+ $expire = !empty($config['expire']) ? time() + intval($config['expire']) : 0;
+ }
+
+ $this->setCookie($name, $value, $expire, $config);
+ $this->request->setCookie($name, $value);
+ }
+
+ /**
+ * Cookie 保存
+ *
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param int $expire 有效期
+ * @param array $option 可选参数
+ * @return void
+ */
+ protected function setCookie(string $name, string $value, int $expire, array $option = []): void
+ {
+ $this->cookie[$name] = [$value, $expire, $option];
+ }
+
+ /**
+ * 永久保存Cookie数据
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param mixed $option 可选参数 可能会是 null|integer|string
+ * @return void
+ */
+ public function forever(string $name, string $value = '', $option = null): void
+ {
+ if (is_null($option) || is_numeric($option)) {
+ $option = [];
+ }
+
+ $option['expire'] = 315360000;
+
+ $this->set($name, $value, $option);
+ }
+
+ /**
+ * Cookie删除
+ * @access public
+ * @param string $name cookie名称
+ * @param array $options cookie参数
+ * @return void
+ */
+ public function delete(string $name, array $options = []): void
+ {
+ $config = array_merge($this->config, array_change_key_case($options));
+ $this->setCookie($name, '', time() - 3600, $config);
+ $this->request->setCookie($name, null);
+ }
+
+ /**
+ * 获取cookie保存数据
+ * @access public
+ * @return array
+ */
+ public function getCookie(): array
+ {
+ return $this->cookie;
+ }
+
+ /**
+ * 保存Cookie
+ * @access public
+ * @return void
+ */
+ public function save(): void
+ {
+ foreach ($this->cookie as $name => $val) {
+ [$value, $expire, $option] = $val;
+
+ $this->saveCookie(
+ (string) $name,
+ $value,
+ $expire,
+ $option['path'],
+ $option['domain'],
+ (bool) $option['secure'],
+ (bool) $option['httponly'],
+ $option['samesite'],
+ );
+ }
+ }
+
+ /**
+ * 保存Cookie
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param int $expire cookie过期时间
+ * @param string $path 有效的服务器路径
+ * @param string $domain 有效域名/子域名
+ * @param bool $secure 是否仅仅通过HTTPS
+ * @param bool $httponly 仅可通过HTTP访问
+ * @param string $samesite 防止CSRF攻击和用户追踪
+ * @return void
+ */
+ protected function saveCookie(string $name, string $value, int $expire, string $path, string $domain, bool $secure, bool $httponly, string $samesite): void
+ {
+ setcookie($name, $value, [
+ 'expires' => $expire,
+ 'path' => $path,
+ 'domain' => $domain,
+ 'secure' => $secure,
+ 'httponly' => $httponly,
+ 'samesite' => $samesite,
+ ]);
+ }
+}
diff --git a/src/think/Db.php b/src/think/Db.php
new file mode 100644
index 0000000..c3c04a6
--- /dev/null
+++ b/src/think/Db.php
@@ -0,0 +1,117 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+/**
+ * 数据库管理类
+ * @package think
+ * @property Config $config
+ */
+class Db extends DbManager
+{
+ /**
+ * @param Event $event
+ * @param Config $config
+ * @param Log $log
+ * @param Cache $cache
+ * @return Db
+ * @codeCoverageIgnore
+ */
+ public static function __make(Event $event, Config $config, Log $log, Cache $cache)
+ {
+ $db = new static();
+ $db->setConfig($config);
+ $db->setEvent($event);
+ $db->setLog($log);
+
+ $store = $db->getConfig('cache_store');
+ $db->setCache($cache->store($store));
+ $db->triggerSql();
+
+ return $db;
+ }
+
+ /**
+ * 注入模型对象
+ * @access public
+ * @return void
+ */
+ protected function modelMaker(): void
+ {
+ }
+
+ /**
+ * 设置配置对象
+ * @access public
+ * @param Config $config 配置对象
+ * @return void
+ */
+ public function setConfig($config): void
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * 获取配置参数
+ * @access public
+ * @param string $name 配置参数
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getConfig(string $name = '', $default = null)
+ {
+ if ('' !== $name) {
+ return $this->config->get('database.' . $name, $default);
+ }
+
+ return $this->config->get('database', []);
+ }
+
+ /**
+ * 设置Event对象
+ * @param Event $event
+ */
+ public function setEvent(Event $event): void
+ {
+ $this->event = $event;
+ }
+
+ /**
+ * 注册回调方法
+ * @access public
+ * @param string $event 事件名
+ * @param callable $callback 回调方法
+ * @return void
+ */
+ public function event(string $event, callable $callback): void
+ {
+ if ($this->event) {
+ $this->event->listen('db.' . $event, $callback);
+ }
+ }
+
+ /**
+ * 触发事件
+ * @access public
+ * @param string $event 事件名
+ * @param mixed $params 传入参数
+ * @param bool $once
+ * @return mixed
+ */
+ public function trigger(string $event, $params = null, bool $once = false)
+ {
+ if ($this->event) {
+ return $this->event->trigger('db.' . $event, $params, $once);
+ }
+ }
+}
diff --git a/src/think/Env.php b/src/think/Env.php
new file mode 100644
index 0000000..91e54a5
--- /dev/null
+++ b/src/think/Env.php
@@ -0,0 +1,199 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use ArrayAccess;
+
+/**
+ * Env管理类
+ * @package think
+ */
+class Env implements ArrayAccess
+{
+ /**
+ * 环境变量数据
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 数据转换映射
+ * @var array
+ */
+ protected $convert = [
+ 'true' => true,
+ 'false' => false,
+ 'off' => false,
+ 'on' => true,
+ ];
+
+ public function __construct()
+ {
+ $this->data = $_ENV;
+ }
+
+ /**
+ * 读取环境变量定义文件
+ * @access public
+ * @param string $file 环境变量定义文件
+ * @return void
+ */
+ public function load(string $file): void
+ {
+ $env = parse_ini_file($file, true, INI_SCANNER_RAW) ?: [];
+ $this->set($env);
+ }
+
+ /**
+ * 获取环境变量值
+ * @access public
+ * @param string $name 环境变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get(?string $name = null, $default = null)
+ {
+ if (is_null($name)) {
+ return $this->data;
+ }
+
+ $name = strtoupper(str_replace('.', '_', $name));
+ if (isset($this->data[$name])) {
+ $result = $this->data[$name];
+
+ if (is_string($result) && isset($this->convert[$result])) {
+ return $this->convert[$result];
+ }
+
+ return $result;
+ }
+
+ return $this->getEnv($name, $default);
+ }
+
+ protected function getEnv(string $name, $default = null)
+ {
+ $result = getenv('PHP_' . $name);
+
+ if (false === $result) {
+ return $default;
+ }
+
+ if (isset($this->convert[$result])) {
+ $result = $this->convert[$result];
+ }
+
+ if (!isset($this->data[$name])) {
+ $this->data[$name] = $result;
+ }
+
+ return $result;
+ }
+
+ /**
+ * 设置环境变量值
+ * @access public
+ * @param string|array $env 环境变量
+ * @param mixed $value 值
+ * @return void
+ */
+ public function set($env, $value = null): void
+ {
+ if (is_array($env)) {
+ $env = array_change_key_case($env, CASE_UPPER);
+
+ foreach ($env as $key => $val) {
+ if (is_array($val)) {
+ foreach ($val as $k => $v) {
+ if (is_string($k)) {
+ $this->data[$key . '_' . strtoupper($k)] = $v;
+ } else {
+ $this->data[$key][$k] = $v;
+ }
+ }
+ } else {
+ $this->data[$key] = $val;
+ }
+ }
+ } else {
+ $name = strtoupper(str_replace('.', '_', $env));
+
+ $this->data[$name] = $value;
+ }
+ }
+
+ /**
+ * 检测是否存在环境变量
+ * @access public
+ * @param string $name 参数名
+ * @return bool
+ */
+ public function has(string $name): bool
+ {
+ return !is_null($this->get($name));
+ }
+
+ /**
+ * 设置环境变量
+ * @access public
+ * @param string $name 参数名
+ * @param mixed $value 值
+ */
+ public function __set(string $name, $value): void
+ {
+ $this->set($name, $value);
+ }
+
+ /**
+ * 获取环境变量
+ * @access public
+ * @param string $name 参数名
+ * @return mixed
+ */
+ public function __get(string $name)
+ {
+ return $this->get($name);
+ }
+
+ /**
+ * 检测是否存在环境变量
+ * @access public
+ * @param string $name 参数名
+ * @return bool
+ */
+ public function __isset(string $name): bool
+ {
+ return $this->has($name);
+ }
+
+ // ArrayAccess
+ public function offsetSet(mixed $name, mixed $value): void
+ {
+ $this->set($name, $value);
+ }
+
+ public function offsetExists(mixed $name): bool
+ {
+ return $this->__isset($name);
+ }
+
+ public function offsetUnset(mixed $name): void
+ {
+ throw new Exception('not support: unset');
+ }
+
+ public function offsetGet(mixed $name): mixed
+ {
+ return $this->get($name);
+ }
+}
diff --git a/src/think/Event.php b/src/think/Event.php
new file mode 100644
index 0000000..f653954
--- /dev/null
+++ b/src/think/Event.php
@@ -0,0 +1,291 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use ReflectionClass;
+use ReflectionMethod;
+use think\helper\Str;
+
+/**
+ * 事件管理类
+ * @package think
+ */
+class Event
+{
+ /**
+ * 监听者
+ * @var array
+ */
+ protected $listener = [];
+
+ /**
+ * 观察者
+ * @var array
+ */
+ protected $observer = [];
+
+ /**
+ * 事件别名
+ * @var array
+ */
+ protected $bind = [
+ 'AppInit' => event\AppInit::class,
+ 'HttpRun' => event\HttpRun::class,
+ 'HttpEnd' => event\HttpEnd::class,
+ 'RouteLoaded' => event\RouteLoaded::class,
+ 'LogWrite' => event\LogWrite::class,
+ 'LogRecord' => event\LogRecord::class,
+ ];
+
+ /**
+ * 应用对象
+ * @var App
+ */
+ protected $app;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ }
+
+ /**
+ * 批量注册事件监听
+ * @access public
+ * @param array $events 事件定义
+ * @return $this
+ */
+ public function listenEvents(array $events)
+ {
+ foreach ($events as $event => $listeners) {
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ $this->listener[$event] = array_merge($this->listener[$event] ?? [], $listeners);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 注册事件监听
+ * @access public
+ * @param string $event 事件名称
+ * @param mixed $listener 监听操作(或者类名)
+ * @param bool $first 是否优先执行
+ * @return $this
+ */
+ public function listen(string $event, $listener, bool $first = false)
+ {
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ if ($first && isset($this->listener[$event])) {
+ array_unshift($this->listener[$event], $listener);
+ } else {
+ $this->listener[$event][] = $listener;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 是否存在事件监听
+ * @access public
+ * @param string $event 事件名称
+ * @return bool
+ */
+ public function hasListener(string $event): bool
+ {
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ return isset($this->listener[$event]);
+ }
+
+ /**
+ * 移除事件监听
+ * @access public
+ * @param string $event 事件名称
+ * @return void
+ */
+ public function remove(string $event): void
+ {
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ unset($this->listener[$event]);
+ }
+
+ /**
+ * 指定事件别名标识 便于调用
+ * @access public
+ * @param array $events 事件别名
+ * @return $this
+ */
+ public function bind(array $events)
+ {
+ $this->bind = array_merge($this->bind, $events);
+
+ return $this;
+ }
+
+ /**
+ * 注册事件订阅者
+ * @access public
+ * @param mixed $subscriber 订阅者
+ * @return $this
+ */
+ public function subscribe($subscriber)
+ {
+ $subscribers = is_object($subscriber) ? [$subscriber] : (array) $subscriber;
+
+ foreach ($subscribers as $name => $subscriber) {
+ if (is_string($subscriber)) {
+ $subscriber = $this->app->make($subscriber);
+ }
+
+ if (method_exists($subscriber, 'subscribe')) {
+ // 手动订阅
+ $subscriber->subscribe($this);
+ } elseif (!is_numeric($name)) {
+ // 注册观察者
+ $this->observer[$name] = $subscriber;
+ } else {
+ // 智能订阅
+ $this->observe($subscriber);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 自动注册事件监听
+ * @access public
+ * @param string|object $observer 观察者
+ * @param null|string $prefix 事件名前缀
+ * @return $this
+ */
+ public function observe($observer, string $prefix = '')
+ {
+ if (is_string($observer)) {
+ $observer = $this->app->make($observer);
+ }
+
+ $reflect = new ReflectionClass($observer);
+ $methods = $reflect->getMethods(ReflectionMethod::IS_PUBLIC);
+
+ if (empty($prefix) && $reflect->hasProperty('eventPrefix')) {
+ $reflectProperty = $reflect->getProperty('eventPrefix');
+ $reflectProperty->setAccessible(true);
+ $prefix = $reflectProperty->getValue($observer);
+ }
+
+ foreach ($methods as $method) {
+ $name = $method->getName();
+ if (str_starts_with($name, 'on')) {
+ $this->listen($prefix . substr($name, 2), [$observer, $name]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 触发事件
+ * @access public
+ * @param string|object $event 事件名称
+ * @param mixed $params 传入参数
+ * @param bool $once 只获取一个有效返回值
+ * @return mixed
+ */
+ public function trigger($event, $params = null, bool $once = false)
+ {
+ if (is_object($event)) {
+ $params = $event;
+ $event = $event::class;
+ }
+
+ if (isset($this->bind[$event])) {
+ $event = $this->bind[$event];
+ }
+
+ $result = [];
+ $listeners = $this->listener[$event] ?? [];
+
+ if (str_contains($event, '.')) {
+ [$prefix, $name] = explode('.', $event, 2);
+ if (isset($this->observer[$prefix])) {
+ // 检查观察者事件响应方法
+ $observer = $this->observer[$prefix];
+ $method = 'on' . Str::studly($name);
+ if (method_exists($observer, $method)) {
+ return $this->dispatch([$observer, $method], $params);
+ }
+ }
+
+ $name = substr($event, 0, strrpos($event, '.'));
+ if (isset($this->listener[$name . '.*'])) {
+ $listeners = array_merge($listeners, $this->listener[$name . '.*']);
+ }
+ }
+
+ $listeners = array_unique($listeners, SORT_REGULAR);
+
+ foreach ($listeners as $key => $listener) {
+ $result[$key] = $this->dispatch($listener, $params);
+
+ if (false === $result[$key] || (!is_null($result[$key]) && $once)) {
+ break;
+ }
+ }
+
+ return $once ? end($result) : $result;
+ }
+
+ /**
+ * 触发事件(只获取一个有效返回值)
+ * @param $event
+ * @param null $params
+ * @return mixed
+ */
+ public function until($event, $params = null)
+ {
+ return $this->trigger($event, $params, true);
+ }
+
+ /**
+ * 执行事件调度
+ * @access protected
+ * @param mixed $event 事件方法
+ * @param mixed $params 参数
+ * @return mixed
+ */
+ protected function dispatch($event, $params = null)
+ {
+ if (!is_string($event)) {
+ $call = $event;
+ } elseif (str_contains($event, '::')) {
+ $call = $event;
+ } else {
+ $obj = $this->app->make($event);
+ $call = [$obj, 'handle'];
+ }
+
+ return $this->app->invoke($call, [$params]);
+ }
+}
diff --git a/src/think/Exception.php b/src/think/Exception.php
new file mode 100644
index 0000000..048c336
--- /dev/null
+++ b/src/think/Exception.php
@@ -0,0 +1,59 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+/**
+ * 异常基础类
+ * @package think
+ */
+class Exception extends \Exception
+{
+ /**
+ * 保存异常页面显示的额外Debug数据
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 设置异常额外的Debug数据
+ * 数据将会显示为下面的格式
+ *
+ * Exception Data
+ * --------------------------------------------------
+ * Label 1
+ * key1 value1
+ * key2 value2
+ * Label 2
+ * key1 value1
+ * key2 value2
+ *
+ * @access protected
+ * @param string $label 数据分类,用于异常页面显示
+ * @param array $data 需要显示的数据,必须为关联数组
+ */
+ final protected function setData(string $label, array $data)
+ {
+ $this->data[$label] = $data;
+ }
+
+ /**
+ * 获取异常额外Debug数据
+ * 主要用于输出到异常页面便于调试
+ * @access public
+ * @return array 由setData设置的Debug数据
+ */
+ final public function getData(): array
+ {
+ return $this->data;
+ }
+}
diff --git a/src/think/File.php b/src/think/File.php
new file mode 100644
index 0000000..9651154
--- /dev/null
+++ b/src/think/File.php
@@ -0,0 +1,198 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use SplFileInfo;
+use Closure;
+use think\exception\FileException;
+
+/**
+ * 文件上传类
+ * @package think
+ */
+class File extends SplFileInfo
+{
+
+ /**
+ * 文件hash规则
+ * @var array
+ */
+ protected $hash = [];
+
+ protected $hashName;
+
+ /**
+ * 保存的文件后缀
+ * @var string
+ */
+ protected $extension;
+
+ public function __construct(string $path, bool $checkPath = true)
+ {
+ if ($checkPath && !is_file($path)) {
+ throw new FileException(sprintf('The file "%s" does not exist', $path));
+ }
+
+ parent::__construct($path);
+ }
+
+ /**
+ * 获取文件的哈希散列值
+ * @access public
+ * @param string $type
+ * @return string
+ */
+ public function hash(string $type = 'sha1'): string
+ {
+ if (!isset($this->hash[$type])) {
+ $this->hash[$type] = hash_file($type, $this->getPathname());
+ }
+
+ return $this->hash[$type];
+ }
+
+ /**
+ * 获取文件的MD5值
+ * @access public
+ * @return string
+ */
+ public function md5(): string
+ {
+ return $this->hash('md5');
+ }
+
+ /**
+ * 获取文件的SHA1值
+ * @access public
+ * @return string
+ */
+ public function sha1(): string
+ {
+ return $this->hash('sha1');
+ }
+
+ /**
+ * 获取文件类型信息
+ * @access public
+ * @return string
+ */
+ public function getMime(): string
+ {
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+
+ return finfo_file($finfo, $this->getPathname());
+ }
+
+ /**
+ * 移动文件
+ * @access public
+ * @param string $directory 保存路径
+ * @param string|null $name 保存的文件名
+ * @return File
+ */
+ public function move(string $directory, ?string $name = null): File
+ {
+ $target = $this->getTargetFile($directory, $name);
+
+ set_error_handler(function ($type, $msg) use (&$error) {
+ $error = $msg;
+ });
+ $renamed = rename($this->getPathname(), (string) $target);
+ restore_error_handler();
+ if (!$renamed) {
+ throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error)));
+ }
+
+ @chmod((string) $target, 0666 & ~umask());
+
+ return $target;
+ }
+
+ /**
+ * 实例化一个新文件
+ * @param string $directory
+ * @param null|string $name
+ * @return File
+ */
+ protected function getTargetFile(string $directory, ?string $name = null): File
+ {
+ if (!is_dir($directory)) {
+ if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) {
+ throw new FileException(sprintf('Unable to create the "%s" directory', $directory));
+ }
+ } elseif (!is_writable($directory)) {
+ throw new FileException(sprintf('Unable to write in the "%s" directory', $directory));
+ }
+
+ $target = rtrim($directory, '/\\') . \DIRECTORY_SEPARATOR . (null === $name ? $this->getBasename() : $this->getName($name));
+
+ return new self($target, false);
+ }
+
+ /**
+ * 获取文件名
+ * @param string $name
+ * @return string
+ */
+ protected function getName(string $name): string
+ {
+ $originalName = str_replace('\\', '/', $name);
+ $pos = strrpos($originalName, '/');
+ $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1);
+
+ return $originalName;
+ }
+
+ /**
+ * 文件扩展名
+ * @return string
+ */
+ public function extension(): string
+ {
+ return $this->getExtension();
+ }
+
+ /**
+ * 指定保存文件的扩展名
+ * @param string $extension
+ * @return void
+ */
+ public function setExtension(string $extension): void
+ {
+ $this->extension = $extension;
+ }
+
+ /**
+ * 自动生成文件名
+ * @access public
+ * @param string|Closure|null $rule
+ * @return string
+ */
+ public function hashName(string|Closure|null $rule = null): string
+ {
+ if (!$this->hashName) {
+ if ($rule instanceof Closure) {
+ $this->hashName = call_user_func_array($rule, [$this]);
+ } else {
+ $this->hashName = match (true) {
+ in_array($rule, hash_algos()) && $hash = $this->hash($rule) => substr($hash, 0, 2) . DIRECTORY_SEPARATOR . substr($hash, 2),
+ is_callable($rule) => call_user_func($rule),
+ default => date('Ymd') . DIRECTORY_SEPARATOR . md5(microtime(true) . $this->getPathname()),
+ };
+ }
+ }
+
+ $extension = $this->extension ?? $this->extension();
+ return $this->hashName . ($extension ? '.' . $extension : '');
+ }
+}
diff --git a/src/think/Http.php b/src/think/Http.php
new file mode 100644
index 0000000..ba3019f
--- /dev/null
+++ b/src/think/Http.php
@@ -0,0 +1,279 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use think\event\HttpEnd;
+use think\event\HttpRun;
+use think\event\RouteLoaded;
+use think\exception\Handle;
+use Throwable;
+
+/**
+ * Web应用管理类
+ * @package think
+ */
+class Http
+{
+ /**
+ * 应用名称
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * 应用路径
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * 路由路径
+ * @var string
+ */
+ protected $routePath;
+
+ /**
+ * 是否绑定应用
+ * @var bool
+ */
+ protected $isBind = false;
+
+ public function __construct(protected App $app)
+ {
+ $this->routePath = $this->app->getRootPath() . 'route' . DIRECTORY_SEPARATOR;
+ }
+
+ /**
+ * 设置应用名称
+ * @access public
+ * @param string $name 应用名称
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * 获取应用名称
+ * @access public
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name ?: '';
+ }
+
+ /**
+ * 设置应用目录
+ * @access public
+ * @param string $path 应用目录
+ * @return $this
+ */
+ public function path(string $path)
+ {
+ if (!str_ends_with($path, DIRECTORY_SEPARATOR)) {
+ $path .= DIRECTORY_SEPARATOR;
+ }
+
+ $this->path = $path;
+ return $this;
+ }
+
+ /**
+ * 获取应用路径
+ * @access public
+ * @return string
+ */
+ public function getPath(): string
+ {
+ return $this->path ?: '';
+ }
+
+ /**
+ * 获取路由目录
+ * @access public
+ * @return string
+ */
+ public function getRoutePath(): string
+ {
+ return $this->routePath;
+ }
+
+ /**
+ * 设置路由目录
+ * @access public
+ * @param string $path 路由定义目录
+ */
+ public function setRoutePath(string $path): void
+ {
+ $this->routePath = $path;
+ }
+
+ /**
+ * 设置应用绑定
+ * @access public
+ * @param bool $bind 是否绑定
+ * @return $this
+ */
+ public function setBind(bool $bind = true)
+ {
+ $this->isBind = $bind;
+ return $this;
+ }
+
+ /**
+ * 是否绑定应用
+ * @access public
+ * @return bool
+ */
+ public function isBind(): bool
+ {
+ return $this->isBind;
+ }
+
+ /**
+ * 执行应用程序
+ * @access public
+ * @param Request|null $request
+ * @return Response
+ */
+ public function run(?Request $request = null): Response
+ {
+ //初始化
+ $this->initialize();
+
+ //自动创建request对象
+ $request = $request ?? $this->app->make('request', [], true);
+ $this->app->instance('request', $request);
+
+ try {
+ $response = $this->runWithRequest($request);
+ } catch (Throwable $e) {
+ $this->reportException($e);
+
+ $response = $this->renderException($request, $e);
+ }
+
+ return $response;
+ }
+
+ /**
+ * 初始化
+ */
+ protected function initialize()
+ {
+ if (!$this->app->initialized()) {
+ $this->app->initialize();
+ }
+ }
+
+ /**
+ * 执行应用程序
+ * @param Request $request
+ * @return mixed
+ */
+ protected function runWithRequest(Request $request)
+ {
+ // 加载全局中间件
+ $this->loadMiddleware();
+
+ // 监听HttpRun
+ $this->app->event->trigger(HttpRun::class);
+
+ return $this->app->middleware->pipeline()
+ ->send($request)
+ ->then(function ($request) {
+ return $this->dispatchToRoute($request);
+ });
+ }
+
+ protected function dispatchToRoute($request)
+ {
+ $withRoute = $this->app->config->get('app.with_route', true) ? function () {
+ $this->loadRoutes();
+ } : false;
+
+ return $this->app->route->dispatch($request, $withRoute);
+ }
+
+ /**
+ * 加载全局中间件
+ */
+ protected function loadMiddleware(): void
+ {
+ if (is_file($this->app->getBasePath() . 'middleware.php')) {
+ $this->app->middleware->import(include $this->app->getBasePath() . 'middleware.php');
+ }
+ }
+
+ /**
+ * 加载路由
+ * @access protected
+ * @return void
+ */
+ protected function loadRoutes(): void
+ {
+ // 加载路由定义
+ $routePath = $this->getRoutePath();
+
+ if (is_dir($routePath)) {
+ $files = glob($routePath . '*.php');
+ foreach ($files as $file) {
+ include $file;
+ }
+ }
+
+ $this->app->event->trigger(RouteLoaded::class);
+ }
+
+ /**
+ * Report the exception to the exception handler.
+ *
+ * @param Throwable $e
+ * @return void
+ */
+ protected function reportException(Throwable $e)
+ {
+ $this->app->make(Handle::class)->report($e);
+ }
+
+ /**
+ * Render the exception to a response.
+ *
+ * @param Request $request
+ * @param Throwable $e
+ * @return Response
+ */
+ protected function renderException($request, Throwable $e)
+ {
+ return $this->app->make(Handle::class)->render($request, $e);
+ }
+
+ /**
+ * HttpEnd
+ * @param Response $response
+ * @return void
+ */
+ public function end(Response $response): void
+ {
+ $this->app->event->trigger(HttpEnd::class, $response);
+
+ //执行中间件
+ $this->app->middleware->end($response);
+
+ // 写入日志
+ $this->app->log->save();
+ }
+}
diff --git a/src/think/Lang.php b/src/think/Lang.php
new file mode 100644
index 0000000..a310dc9
--- /dev/null
+++ b/src/think/Lang.php
@@ -0,0 +1,273 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+/**
+ * 多语言管理类
+ * @package think
+ */
+class Lang
+{
+ protected $app;
+
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [
+ // 默认语言
+ 'default_lang' => 'zh-cn',
+ // 自动侦测浏览器语言
+ 'auto_detect_browser' => true,
+ // 允许的语言列表
+ 'allow_lang_list' => [],
+ // 是否使用Cookie记录
+ 'use_cookie' => true,
+ // 扩展语言包
+ 'extend_list' => [],
+ // 多语言cookie变量
+ 'cookie_var' => 'think_lang',
+ // 多语言header变量
+ 'header_var' => 'think-lang',
+ // 多语言自动侦测变量名
+ 'detect_var' => 'lang',
+ // Accept-Language转义为对应语言包名称
+ 'accept_language' => [
+ 'zh-hans-cn' => 'zh-cn',
+ ],
+ // 是否支持语言分组
+ 'allow_group' => false,
+ ];
+
+ /**
+ * 多语言信息
+ * @var array
+ */
+ private $lang = [];
+
+ /**
+ * 当前语言
+ * @var string
+ */
+ private $range = 'zh-cn';
+
+ /**
+ * 构造方法
+ * @access public
+ * @param array $config
+ */
+ public function __construct(App $app, array $config = [])
+ {
+ $this->config = array_merge($this->config, array_change_key_case($config));
+ $this->range = $this->config['default_lang'];
+ $this->app = $app;
+ }
+
+ public static function __make(App $app, Config $config)
+ {
+ return new static($app, $config->get('lang'));
+ }
+
+ /**
+ * 获取当前语言配置
+ * @access public
+ * @return array
+ */
+ public function getConfig(): array
+ {
+ return $this->config;
+ }
+
+ /**
+ * 设置当前语言
+ * @access public
+ * @param string $lang 语言
+ * @return void
+ */
+ public function setLangSet(string $lang): void
+ {
+ $this->range = $lang;
+ }
+
+ /**
+ * 获取当前语言
+ * @access public
+ * @return string
+ */
+ public function getLangSet(): string
+ {
+ return $this->range;
+ }
+
+ /**
+ * 获取默认语言
+ * @access public
+ * @return string
+ */
+ public function defaultLangSet()
+ {
+ return $this->config['default_lang'];
+ }
+
+ /**
+ * 切换语言
+ * @access public
+ * @param string $langset 语言
+ * @return void
+ */
+ public function switchLangSet(string $langset)
+ {
+ if (empty($langset)) {
+ return;
+ }
+
+ $this->setLangSet($langset);
+
+ // 加载系统语言包
+ $this->load([
+ $this->app->getThinkPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.php',
+ ]);
+
+ // 加载应用语言包(支持多种类型)
+ $files = glob($this->app->getAppPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.*');
+ $this->load($files);
+
+ // 加载扩展(自定义)语言包
+ $list = $this->app->config->get('lang.extend_list', []);
+
+ if (isset($list[$langset])) {
+ $this->load($list[$langset]);
+ }
+ }
+
+ /**
+ * 加载语言定义(不区分大小写)
+ * @access public
+ * @param string|array $file 语言文件
+ * @param string $range 语言作用域
+ * @return array
+ */
+ public function load($file, $range = ''): array
+ {
+ $range = $range ?: $this->range;
+ if (!isset($this->lang[$range])) {
+ $this->lang[$range] = [];
+ }
+
+ $lang = [];
+
+ foreach ((array) $file as $name) {
+ if (is_file($name)) {
+ $result = $this->parse($name);
+ $lang = array_change_key_case($result) + $lang;
+ }
+ }
+
+ if (!empty($lang)) {
+ $this->lang[$range] = $lang + $this->lang[$range];
+ }
+
+ return $this->lang[$range];
+ }
+
+ /**
+ * 解析语言文件
+ * @access protected
+ * @param string $file 语言文件名
+ * @return array
+ */
+ protected function parse(string $file): array
+ {
+ $type = pathinfo($file, PATHINFO_EXTENSION);
+ $result = match ($type) {
+ 'php' => include $file,
+ 'yml', 'yaml' => function_exists('yaml_parse_file') ? yaml_parse_file($file) : [],
+ 'json' => json_decode(file_get_contents($file), true),
+ default => [],
+ };
+
+ return is_array($result) ? $result : [];
+ }
+
+ /**
+ * 判断是否存在语言定义(不区分大小写)
+ * @access public
+ * @param string $name 语言变量
+ * @param string $range 语言作用域
+ * @return bool
+ */
+ public function has(string $name, string $range = ''): bool
+ {
+ $range = $range ?: $this->range;
+
+ if ($this->config['allow_group'] && str_contains($name, '.')) {
+ [$name1, $name2] = explode('.', $name, 2);
+ return isset($this->lang[$range][strtolower($name1)][$name2]);
+ }
+
+ return isset($this->lang[$range][strtolower($name)]);
+ }
+
+ /**
+ * 获取语言定义(不区分大小写)
+ * @access public
+ * @param string|null $name 语言变量
+ * @param array $vars 变量替换
+ * @param string $range 语言作用域
+ * @return mixed
+ */
+ public function get(?string $name = null, array $vars = [], string $range = '')
+ {
+ $range = $range ?: $this->range;
+
+ if (!isset($this->lang[$range])) {
+ $this->switchLangSet($range);
+ }
+
+ // 空参数返回所有定义
+ if (is_null($name)) {
+ return $this->lang[$range] ?? [];
+ }
+
+ if ($this->config['allow_group'] && str_contains($name, '.')) {
+ [$name1, $name2] = explode('.', $name, 2);
+
+ $value = $this->lang[$range][strtolower($name1)][$name2] ?? $name;
+ } else {
+ $value = $this->lang[$range][strtolower($name)] ?? $name;
+ }
+
+ // 变量解析
+ if (!empty($vars) && is_array($vars)) {
+ /**
+ * Notes:
+ * 为了检测的方便,数字索引的判断仅仅是参数数组的第一个元素的key为数字0
+ * 数字索引采用的是系统的 sprintf 函数替换,用法请参考 sprintf 函数
+ */
+ if (key($vars) === 0) {
+ // 数字索引解析
+ array_unshift($vars, $value);
+ $value = call_user_func_array('sprintf', $vars);
+ } else {
+ // 关联索引解析
+ $replace = array_keys($vars);
+ foreach ($replace as &$v) {
+ $v = "{:{$v}}";
+ }
+ $value = str_replace($replace, $vars, $value);
+ }
+ }
+
+ return $value;
+ }
+}
diff --git a/src/think/Log.php b/src/think/Log.php
new file mode 100644
index 0000000..990f4e8
--- /dev/null
+++ b/src/think/Log.php
@@ -0,0 +1,249 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use InvalidArgumentException;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LoggerTrait;
+use Stringable;
+use think\event\LogWrite;
+use think\helper\Arr;
+use think\log\Channel;
+use think\log\ChannelSet;
+
+/**
+ * 日志管理类
+ * @package think
+ * @mixin Channel
+ */
+class Log extends Manager implements LoggerInterface
+{
+ use LoggerTrait;
+ const EMERGENCY = 'emergency';
+ const ALERT = 'alert';
+ const CRITICAL = 'critical';
+ const ERROR = 'error';
+ const WARNING = 'warning';
+ const NOTICE = 'notice';
+ const INFO = 'info';
+ const DEBUG = 'debug';
+ const SQL = 'sql';
+
+ protected $namespace = '\\think\\log\\driver\\';
+
+ /**
+ * 默认驱动
+ * @return string|null
+ */
+ public function getDefaultDriver(): ?string
+ {
+ return $this->getConfig('default');
+ }
+
+ /**
+ * 获取日志配置
+ * @access public
+ * @param null|string $name 名称
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getConfig(?string $name = null, $default = null)
+ {
+ if (!is_null($name)) {
+ return $this->app->config->get('log.' . $name, $default);
+ }
+
+ return $this->app->config->get('log');
+ }
+
+ /**
+ * 获取渠道配置
+ * @param string $channel
+ * @param string $name
+ * @param mixed $default
+ * @return array
+ */
+ public function getChannelConfig(string $channel, ?string $name = null, $default = null)
+ {
+ if ($config = $this->getConfig("channels.{$channel}")) {
+ return Arr::get($config, $name, $default);
+ }
+
+ throw new InvalidArgumentException("Channel [$channel] not found.");
+ }
+
+ /**
+ * driver()的别名
+ * @param string|array $name 渠道名
+ * @return Channel|ChannelSet
+ */
+ public function channel(string|array|null $name = null)
+ {
+ if (is_array($name)) {
+ return new ChannelSet($this, $name);
+ }
+
+ return $this->driver($name);
+ }
+
+ protected function resolveType(string $name)
+ {
+ return $this->getChannelConfig($name, 'type', 'file');
+ }
+
+ public function createDriver(string $name)
+ {
+ $driver = parent::createDriver($name);
+
+ $lazy = !$this->getChannelConfig($name, "realtime_write", false) && !$this->app->runningInConsole();
+ $allow = array_merge($this->getConfig("level", []), $this->getChannelConfig($name, "level", []));
+
+ return new Channel($name, $driver, $allow, $lazy, $this->app->event);
+ }
+
+ protected function resolveConfig(string $name)
+ {
+ return $this->getChannelConfig($name);
+ }
+
+ /**
+ * 清空日志信息
+ * @access public
+ * @param string|array $channel 日志通道名
+ * @return $this
+ */
+ public function clear(string|array $channel = '*')
+ {
+ if ('*' == $channel) {
+ $channel = array_keys($this->drivers);
+ }
+
+ $this->channel($channel)->clear();
+
+ return $this;
+ }
+
+ /**
+ * 关闭本次请求日志写入
+ * @access public
+ * @param string|array $channel 日志通道名
+ * @return $this
+ */
+ public function close(string|array $channel = '*')
+ {
+ if ('*' == $channel) {
+ $channel = array_keys($this->drivers);
+ }
+
+ $this->channel($channel)->close();
+
+ return $this;
+ }
+
+ /**
+ * 获取日志信息
+ * @access public
+ * @param string $channel 日志通道名
+ * @return array
+ */
+ public function getLog(?string $channel = null): array
+ {
+ return $this->channel($channel)->getLog();
+ }
+
+ /**
+ * 保存日志信息
+ * @access public
+ * @return bool
+ */
+ public function save(): bool
+ {
+ /** @var Channel $channel */
+ foreach ($this->drivers as $channel) {
+ $channel->save();
+ }
+
+ return true;
+ }
+
+ /**
+ * 记录日志信息
+ * @access public
+ * @param mixed $msg 日志信息
+ * @param string $type 日志级别
+ * @param array $context 替换内容
+ * @param bool $lazy
+ * @return $this
+ */
+ public function record($msg, string $type = 'info', array $context = [], bool $lazy = true)
+ {
+ $channel = $this->getConfig('type_channel.' . $type);
+
+ $this->channel($channel)->record($msg, $type, $context, $lazy);
+
+ return $this;
+ }
+
+ /**
+ * 实时写入日志信息
+ * @access public
+ * @param mixed $msg 调试信息
+ * @param string $type 日志级别
+ * @param array $context 替换内容
+ * @return $this
+ */
+ public function write($msg, string $type = 'info', array $context = [])
+ {
+ return $this->record($msg, $type, $context, false);
+ }
+
+ /**
+ * 注册日志写入事件监听
+ * @param $listener
+ * @return Event
+ */
+ public function listen($listener)
+ {
+ return $this->app->event->listen(LogWrite::class, $listener);
+ }
+
+ /**
+ * 记录日志信息
+ * @access public
+ * @param mixed $level 日志级别
+ * @param string|Stringable $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function log($level, $message, array $context = []): void
+ {
+ $this->record($message, $level, $context);
+ }
+
+ /**
+ * 记录sql信息
+ * @access public
+ * @param string|Stringable $message 日志信息
+ * @param array $context 替换内容
+ * @return void
+ */
+ public function sql($message, array $context = []): void
+ {
+ $this->log(__FUNCTION__, $message, $context);
+ }
+
+ public function __call($method, $parameters)
+ {
+ $this->log($method, ...$parameters);
+ }
+}
diff --git a/src/think/Manager.php b/src/think/Manager.php
new file mode 100644
index 0000000..d423d12
--- /dev/null
+++ b/src/think/Manager.php
@@ -0,0 +1,173 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use InvalidArgumentException;
+use think\helper\Str;
+
+abstract class Manager
+{
+ /**
+ * 驱动
+ * @var array
+ */
+ protected $drivers = [];
+
+ /**
+ * 驱动的命名空间
+ * @var string
+ */
+ protected $namespace = null;
+
+ public function __construct(protected App $app)
+ {
+ }
+
+ /**
+ * 获取驱动实例
+ * @param null|string $name
+ * @return mixed
+ */
+ protected function driver(?string $name = null)
+ {
+ $name = $name ?: $this->getDefaultDriver();
+
+ if (is_null($name)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Unable to resolve NULL driver for [%s].',
+ static::class
+ ));
+ }
+
+ return $this->drivers[$name] = $this->getDriver($name);
+ }
+
+ /**
+ * 获取驱动实例
+ * @param string $name
+ * @return mixed
+ */
+ protected function getDriver(string $name)
+ {
+ return $this->drivers[$name] ?? $this->createDriver($name);
+ }
+
+ /**
+ * 获取驱动类型
+ * @param string $name
+ * @return mixed
+ */
+ protected function resolveType(string $name)
+ {
+ return $name;
+ }
+
+ /**
+ * 获取驱动配置
+ * @param string $name
+ * @return mixed
+ */
+ protected function resolveConfig(string $name)
+ {
+ return $name;
+ }
+
+ /**
+ * 获取驱动类
+ * @param string $type
+ * @return string
+ */
+ protected function resolveClass(string $type): string
+ {
+ if ($this->namespace || str_contains($type, '\\')) {
+ $class = str_contains($type, '\\') ? $type : $this->namespace . Str::studly($type);
+
+ if (class_exists($class)) {
+ return $class;
+ }
+ }
+
+ throw new InvalidArgumentException("Driver [$type] not supported.");
+ }
+
+ /**
+ * 获取驱动参数
+ * @param $name
+ * @return array
+ */
+ protected function resolveParams($name): array
+ {
+ $config = $this->resolveConfig($name);
+ return [$config];
+ }
+
+ /**
+ * 创建驱动
+ *
+ * @param string $name
+ * @return mixed
+ *
+ */
+ protected function createDriver(string $name)
+ {
+ $type = $this->resolveType($name);
+
+ $method = 'create' . Str::studly($type) . 'Driver';
+
+ $params = $this->resolveParams($name);
+
+ if (method_exists($this, $method)) {
+ return $this->$method(...$params);
+ }
+
+ $class = $this->resolveClass($type);
+
+ return $this->app->invokeClass($class, $params);
+ }
+
+ /**
+ * 移除一个驱动实例
+ *
+ * @param array|string|null $name
+ * @return $this
+ */
+ public function forgetDriver($name = null)
+ {
+ $name = $name ?? $this->getDefaultDriver();
+
+ foreach ((array) $name as $cacheName) {
+ if (isset($this->drivers[$cacheName])) {
+ unset($this->drivers[$cacheName]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 默认驱动
+ * @return string|null
+ */
+ abstract public function getDefaultDriver();
+
+ /**
+ * 动态调用
+ * @param string $method
+ * @param array $parameters
+ * @return mixed
+ */
+ public function __call($method, $parameters)
+ {
+ return $this->driver()->$method(...$parameters);
+ }
+}
diff --git a/src/think/Middleware.php b/src/think/Middleware.php
new file mode 100644
index 0000000..82c8adc
--- /dev/null
+++ b/src/think/Middleware.php
@@ -0,0 +1,248 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use Closure;
+use LogicException;
+use think\exception\Handle;
+use Throwable;
+
+/**
+ * 中间件管理类
+ * @package think
+ */
+class Middleware
+{
+ /**
+ * 中间件执行队列
+ * @var array
+ */
+ protected $queue = [];
+
+ public function __construct(protected App $app)
+ {
+ }
+
+ /**
+ * 导入中间件
+ * @access public
+ * @param array $middlewares
+ * @param string $type 中间件类型
+ * @return void
+ */
+ public function import(array $middlewares = [], string $type = 'global'): void
+ {
+ foreach ($middlewares as $middleware) {
+ $this->add($middleware, $type);
+ }
+ }
+
+ /**
+ * 注册中间件
+ * @access public
+ * @param mixed $middleware
+ * @param string $type 中间件类型
+ * @return void
+ */
+ public function add(array|string|Closure $middleware, string $type = 'global'): void
+ {
+ $middleware = $this->buildMiddleware($middleware, $type);
+
+ if (!empty($middleware)) {
+ $this->queue[$type][] = $middleware;
+ $this->queue[$type] = array_unique($this->queue[$type], SORT_REGULAR);
+ }
+ }
+
+ /**
+ * 注册路由中间件
+ * @access public
+ * @param mixed $middleware
+ * @return void
+ */
+ public function route(array|string|Closure $middleware): void
+ {
+ $this->add($middleware, 'route');
+ }
+
+ /**
+ * 注册控制器中间件
+ * @access public
+ * @param mixed $middleware
+ * @return void
+ */
+ public function controller(array|string|Closure $middleware): void
+ {
+ $this->add($middleware, 'controller');
+ }
+
+ /**
+ * 注册中间件到开始位置
+ * @access public
+ * @param mixed $middleware
+ * @param string $type 中间件类型
+ */
+ public function unshift(array|string|Closure $middleware, string $type = 'global')
+ {
+ $middleware = $this->buildMiddleware($middleware, $type);
+
+ if (!empty($middleware)) {
+ if (!isset($this->queue[$type])) {
+ $this->queue[$type] = [];
+ }
+
+ array_unshift($this->queue[$type], $middleware);
+ }
+ }
+
+ /**
+ * 获取注册的中间件
+ * @access public
+ * @param string $type 中间件类型
+ * @return array
+ */
+ public function all(string $type = 'global'): array
+ {
+ return $this->queue[$type] ?? [];
+ }
+
+ /**
+ * 调度管道
+ * @access public
+ * @param string $type 中间件类型
+ * @return Pipeline
+ */
+ public function pipeline(string $type = 'global')
+ {
+ return (new Pipeline())
+ ->through(array_map(function ($middleware) {
+ return function ($request, $next) use ($middleware) {
+ [$call, $params] = $middleware;
+ if (is_array($call) && is_string($call[0])) {
+ $call = [$this->app->make($call[0]), $call[1]];
+ }
+ $response = call_user_func($call, $request, $next, ...$params);
+
+ if (!$response instanceof Response) {
+ throw new LogicException('The middleware must return Response instance');
+ }
+ return $response;
+ };
+ }, $this->sortMiddleware($this->queue[$type] ?? [])))
+ ->whenException([$this, 'handleException']);
+ }
+
+ /**
+ * 结束调度
+ * @param Response $response
+ */
+ public function end(Response $response)
+ {
+ foreach ($this->queue as $queue) {
+ foreach ($queue as $middleware) {
+ [$call] = $middleware;
+ if (is_array($call) && is_string($call[0])) {
+ $instance = $this->app->make($call[0]);
+ if (method_exists($instance, 'end')) {
+ $instance->end($response);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 异常处理
+ * @param Request $passable
+ * @param Throwable $e
+ * @return Response
+ */
+ public function handleException($passable, Throwable $e)
+ {
+ /** @var Handle $handler */
+ $handler = $this->app->make(Handle::class);
+
+ $handler->report($e);
+
+ return $handler->render($passable, $e);
+ }
+
+ /**
+ * 解析中间件
+ * @access protected
+ * @param array|string|Closure $middleware
+ * @param string $type 中间件类型
+ * @return array
+ */
+ protected function buildMiddleware(array|string|Closure $middleware, string $type): array
+ {
+ if (empty($middleware)) {
+ return [];
+ }
+
+ if (is_array($middleware)) {
+ [$middleware, $params] = $middleware;
+ }
+
+ if ($middleware instanceof Closure) {
+ return [$middleware, $params ?? []];
+ }
+
+ //中间件别名检查
+ $alias = $this->app->config->get('middleware.alias', []);
+
+ if (isset($alias[$middleware])) {
+ $middleware = $alias[$middleware];
+ }
+
+ if (is_array($middleware)) {
+ $this->import($middleware, $type);
+ return [];
+ }
+
+ return [[$middleware, 'handle'], $params ?? []];
+ }
+
+ /**
+ * 中间件排序
+ * @param array $middlewares
+ * @return array
+ */
+ protected function sortMiddleware(array $middlewares)
+ {
+ $priority = $this->app->config->get('middleware.priority', []);
+ uasort($middlewares, function ($a, $b) use ($priority) {
+ $aPriority = $this->getMiddlewarePriority($priority, $a);
+ $bPriority = $this->getMiddlewarePriority($priority, $b);
+ return $bPriority - $aPriority;
+ });
+
+ return $middlewares;
+ }
+
+ /**
+ * 获取中间件优先级
+ * @param $priority
+ * @param $middleware
+ * @return int
+ */
+ protected function getMiddlewarePriority($priority, $middleware)
+ {
+ [$call] = $middleware;
+ if (is_array($call) && is_string($call[0])) {
+ $index = array_search($call[0], array_reverse($priority));
+ return false === $index ? -1 : $index;
+ }
+ return -1;
+ }
+}
diff --git a/src/think/Pipeline.php b/src/think/Pipeline.php
new file mode 100644
index 0000000..9f5c3b3
--- /dev/null
+++ b/src/think/Pipeline.php
@@ -0,0 +1,106 @@
+
+// +----------------------------------------------------------------------
+namespace think;
+
+use Closure;
+use Exception;
+use Throwable;
+
+class Pipeline
+{
+ protected $passable;
+
+ protected $pipes = [];
+
+ protected $exceptionHandler;
+
+ /**
+ * 初始数据
+ * @param $passable
+ * @return $this
+ */
+ public function send($passable)
+ {
+ $this->passable = $passable;
+ return $this;
+ }
+
+ /**
+ * 调用栈
+ * @param $pipes
+ * @return $this
+ */
+ public function through($pipes)
+ {
+ $this->pipes = is_array($pipes) ? $pipes : func_get_args();
+ return $this;
+ }
+
+ /**
+ * 执行
+ * @param Closure $destination
+ * @return mixed
+ */
+ public function then(Closure $destination)
+ {
+ $pipeline = array_reduce(
+ array_reverse($this->pipes),
+ $this->carry(),
+ function ($passable) use ($destination) {
+ try {
+ return $destination($passable);
+ } catch (Throwable | Exception $e) {
+ return $this->handleException($passable, $e);
+ }
+ }
+ );
+
+ return $pipeline($this->passable);
+ }
+
+ /**
+ * 设置异常处理器
+ * @param callable $handler
+ * @return $this
+ */
+ public function whenException($handler)
+ {
+ $this->exceptionHandler = $handler;
+ return $this;
+ }
+
+ protected function carry()
+ {
+ return function ($stack, $pipe) {
+ return function ($passable) use ($stack, $pipe) {
+ try {
+ return $pipe($passable, $stack);
+ } catch (Throwable | Exception $e) {
+ return $this->handleException($passable, $e);
+ }
+ };
+ };
+ }
+
+ /**
+ * 异常处理
+ * @param $passable
+ * @param $e
+ * @return mixed
+ */
+ protected function handleException($passable, Throwable $e)
+ {
+ if ($this->exceptionHandler) {
+ return call_user_func($this->exceptionHandler, $passable, $e);
+ }
+ throw $e;
+ }
+}
diff --git a/src/think/Request.php b/src/think/Request.php
new file mode 100644
index 0000000..1a1e373
--- /dev/null
+++ b/src/think/Request.php
@@ -0,0 +1,2215 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types=1);
+
+namespace think;
+
+use ArrayAccess;
+use think\facade\Lang;
+use think\file\UploadedFile;
+use think\route\Rule;
+
+/**
+ * 请求管理类
+ * @package think
+ */
+class Request implements ArrayAccess
+{
+ /**
+ * 兼容PATH_INFO获取
+ * @var array
+ */
+ protected $pathinfoFetch = ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL', 'PHP_SELF'];
+
+ /**
+ * PATHINFO变量名 用于兼容模式
+ * @var string
+ */
+ protected $varPathinfo = 's';
+
+ /**
+ * 请求类型
+ * @var string
+ */
+ protected $varMethod = '_method';
+
+ /**
+ * 表单ajax伪装变量
+ * @var string
+ */
+ protected $varAjax = '_ajax';
+
+ /**
+ * 表单pjax伪装变量
+ * @var string
+ */
+ protected $varPjax = '_pjax';
+
+ /**
+ * 域名根
+ * @var string
+ */
+ protected $rootDomain = '';
+
+ /**
+ * 特殊域名根标识 用于识别com.cn org.cn 这种
+ * @var array
+ */
+ protected $domainSpecialSuffix = ['com', 'net', 'org', 'edu', 'gov', 'mil', 'co', 'info'];
+
+ /**
+ * HTTPS代理标识
+ * @var string
+ */
+ protected $httpsAgentName = '';
+
+ /**
+ * 前端代理服务器IP
+ * @var array
+ */
+ protected $proxyServerIp = [];
+
+ /**
+ * 前端代理服务器真实IP头
+ * @var array
+ */
+ protected $proxyServerIpHeader = ['HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP'];
+
+ /**
+ * 请求类型
+ * @var string
+ */
+ protected $method;
+
+ /**
+ * 域名(含协议及端口)
+ * @var string
+ */
+ protected $domain;
+
+ /**
+ * HOST(含端口)
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * 子域名
+ * @var string
+ */
+ protected $subDomain;
+
+ /**
+ * 泛域名
+ * @var string
+ */
+ protected $panDomain;
+
+ /**
+ * 当前URL地址
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * 基础URL
+ * @var string
+ */
+ protected $baseUrl;
+
+ /**
+ * 当前执行的文件
+ * @var string
+ */
+ protected $baseFile;
+
+ /**
+ * 访问的ROOT地址
+ * @var string
+ */
+ protected $root;
+
+ /**
+ * pathinfo
+ * @var string
+ */
+ protected $pathinfo;
+
+ /**
+ * pathinfo(不含后缀)
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * 当前请求的IP地址
+ * @var string
+ */
+ protected $realIP;
+
+ /**
+ * 当前控制器分层名
+ * @var string
+ */
+ protected $layer;
+
+ /**
+ * 当前控制器名
+ * @var string
+ */
+ protected $controller;
+
+ /**
+ * 当前操作名
+ * @var string
+ */
+ protected $action;
+
+ /**
+ * 当前请求参数
+ * @var array
+ */
+ protected $param = [];
+
+ /**
+ * 当前GET参数
+ * @var array
+ */
+ protected $get = [];
+
+ /**
+ * 当前POST参数
+ * @var array
+ */
+ protected $post = [];
+
+ /**
+ * 当前REQUEST参数
+ * @var array
+ */
+ protected $request = [];
+
+ /**
+ * 当前路由对象
+ * @var Rule
+ */
+ protected $rule;
+
+ /**
+ * 当前ROUTE参数
+ * @var array
+ */
+ protected $route = [];
+
+ /**
+ * 中间件传递的参数
+ * @var array
+ */
+ protected $middleware = [];
+
+ /**
+ * 当前PUT参数
+ * @var array
+ */
+ protected $put;
+
+ /**
+ * SESSION对象
+ * @var Session
+ */
+ protected $session;
+
+ /**
+ * COOKIE数据
+ * @var array
+ */
+ protected $cookie = [];
+
+ /**
+ * ENV对象
+ * @var Env
+ */
+ protected $env;
+
+ /**
+ * 当前SERVER参数
+ * @var array
+ */
+ protected $server = [];
+
+ /**
+ * 当前FILE参数
+ * @var array
+ */
+ protected $file = [];
+
+ /**
+ * 当前HEADER参数
+ * @var array
+ */
+ protected $header = [];
+
+ /**
+ * 资源类型定义
+ * @var array
+ */
+ protected $mimeType = [
+ 'xml' => 'application/xml,text/xml,application/x-xml',
+ 'json' => 'application/json,text/x-json,application/jsonrequest,text/json',
+ 'js' => 'text/javascript,application/javascript,application/x-javascript',
+ 'css' => 'text/css',
+ 'rss' => 'application/rss+xml',
+ 'yaml' => 'application/x-yaml,text/yaml',
+ 'atom' => 'application/atom+xml',
+ 'pdf' => 'application/pdf',
+ 'text' => 'text/plain',
+ 'image' => 'image/png,image/jpg,image/jpeg,image/pjpeg,image/gif,image/webp,image/*',
+ 'csv' => 'text/csv',
+ 'html' => 'text/html,application/xhtml+xml,*/*',
+ ];
+
+ /**
+ * 当前请求内容
+ * @var string
+ */
+ protected $content;
+
+ /**
+ * 全局过滤规则
+ * @var array
+ */
+ protected $filter;
+
+ /**
+ * php://input内容
+ * @var string
+ */
+ // php://input
+ protected $input;
+
+ /**
+ * 请求安全Key
+ * @var string
+ */
+ protected $secureKey;
+
+ /**
+ * 是否合并Param
+ * @var bool
+ */
+ protected $mergeParam = false;
+
+ /**
+ * 架构函数
+ * @access public
+ */
+ public function __construct()
+ {
+ // 保存 php://input
+ $this->input = file_get_contents('php://input');
+ }
+
+ public static function __make(App $app)
+ {
+ $request = new static();
+
+ if (function_exists('apache_request_headers') && $result = apache_request_headers()) {
+ $header = $result;
+ } else {
+ $header = [];
+ $server = $_SERVER;
+ foreach ($server as $key => $val) {
+ if (str_starts_with($key, 'HTTP_')) {
+ $key = str_replace('_', '-', strtolower(substr($key, 5)));
+ $header[$key] = $val;
+ }
+ }
+ if (isset($server['CONTENT_TYPE'])) {
+ $header['content-type'] = $server['CONTENT_TYPE'];
+ }
+ if (isset($server['CONTENT_LENGTH'])) {
+ $header['content-length'] = $server['CONTENT_LENGTH'];
+ }
+ }
+
+ $request->header = array_change_key_case($header);
+ $request->server = $_SERVER;
+ $request->env = $app->env;
+
+ $inputData = $request->getInputData($request->input);
+
+ $request->get = $_GET;
+ $request->post = $_POST ?: $inputData;
+ $request->put = $inputData;
+ $request->request = $_REQUEST;
+ $request->cookie = $_COOKIE;
+ $request->file = $_FILES ?? [];
+
+ return $request;
+ }
+
+ /**
+ * 设置当前包含协议的域名
+ * @access public
+ * @param string $domain 域名
+ * @return $this
+ */
+ public function setDomain(string $domain)
+ {
+ $this->domain = $domain;
+ return $this;
+ }
+
+ /**
+ * 获取当前包含协议的域名
+ * @access public
+ * @param bool $port 是否需要去除端口号
+ * @return string
+ */
+ public function domain(bool $port = false): string
+ {
+ return $this->scheme() . '://' . $this->host($port);
+ }
+
+ /**
+ * 设置根域名
+ * @param string $domain
+ * @return $this
+ */
+ public function setRootDomain(string $domain)
+ {
+ $this->rootDomain = $domain;
+ return $this;
+ }
+
+ /**
+ * 获取当前根域名
+ * @access public
+ * @return string
+ */
+ public function rootDomain(): string
+ {
+ $root = $this->rootDomain;
+
+ if (!$root) {
+ $item = explode('.', $this->host(true));
+ $count = count($item);
+ if ($count > 1) {
+ $root = $item[$count - 2] . '.' . $item[$count - 1];
+ if ($count > 2 && in_array($item[$count - 2], $this->domainSpecialSuffix)) {
+ $root = $item[$count - 3] . '.' . $root;
+ }
+ } else {
+ $root = $item[0];
+ }
+ }
+
+ return $root;
+ }
+
+ /**
+ * 设置当前泛域名的值
+ * @access public
+ * @param string $domain 域名
+ * @return $this
+ */
+ public function setSubDomain(string $domain)
+ {
+ $this->subDomain = $domain;
+ return $this;
+ }
+
+ /**
+ * 获取当前子域名
+ * @access public
+ * @return string
+ */
+ public function subDomain(): string
+ {
+ if (is_null($this->subDomain)) {
+ // 获取当前主域名
+ $rootDomain = $this->rootDomain();
+
+ if ($rootDomain) {
+ $sub = stristr($this->host(), $rootDomain, true);
+ $this->subDomain = $sub ? rtrim($sub, '.') : '';
+ } else {
+ $this->subDomain = '';
+ }
+ }
+
+ return $this->subDomain;
+ }
+
+ /**
+ * 设置当前泛域名的值
+ * @access public
+ * @param string $domain 域名
+ * @return $this
+ */
+ public function setPanDomain(string $domain)
+ {
+ $this->panDomain = $domain;
+ return $this;
+ }
+
+ /**
+ * 获取当前泛域名的值
+ * @access public
+ * @return string
+ */
+ public function panDomain(): string
+ {
+ return $this->panDomain ?: '';
+ }
+
+ /**
+ * 设置当前完整URL 包括QUERY_STRING
+ * @access public
+ * @param string $url URL地址
+ * @return $this
+ */
+ public function setUrl(string $url)
+ {
+ $this->url = $url;
+ return $this;
+ }
+
+ /**
+ * 获取当前完整URL 包括QUERY_STRING
+ * @access public
+ * @param bool $complete 是否包含完整域名
+ * @return string
+ */
+ public function url(bool $complete = false): string
+ {
+ if ($this->url) {
+ $url = $this->url;
+ } elseif ($this->server('HTTP_X_REWRITE_URL')) {
+ $url = $this->server('HTTP_X_REWRITE_URL');
+ } elseif ($this->server('REQUEST_URI')) {
+ $url = $this->server('REQUEST_URI');
+ } elseif ($this->server('ORIG_PATH_INFO')) {
+ $url = $this->server('ORIG_PATH_INFO') . (!empty($this->server('QUERY_STRING')) ? '?' . $this->server('QUERY_STRING') : '');
+ } elseif (isset($_SERVER['argv'][1])) {
+ $url = $_SERVER['argv'][1];
+ } else {
+ $url = '';
+ }
+
+ return $complete ? $this->domain() . $url : $url;
+ }
+
+ /**
+ * 设置当前URL 不含QUERY_STRING
+ * @access public
+ * @param string $url URL地址
+ * @return $this
+ */
+ public function setBaseUrl(string $url)
+ {
+ $this->baseUrl = $url;
+ return $this;
+ }
+
+ /**
+ * 获取当前URL 不含QUERY_STRING
+ * @access public
+ * @param bool $complete 是否包含完整域名
+ * @return string
+ */
+ public function baseUrl(bool $complete = false): string
+ {
+ if (!$this->baseUrl) {
+ $str = $this->url();
+ $this->baseUrl = str_contains($str, '?') ? strstr($str, '?', true) : $str;
+ }
+
+ return $complete ? $this->domain() . $this->baseUrl : $this->baseUrl;
+ }
+
+ /**
+ * 获取当前执行的文件 SCRIPT_NAME
+ * @access public
+ * @param bool $complete 是否包含完整域名
+ * @return string
+ */
+ public function baseFile(bool $complete = false): string
+ {
+ if (!$this->baseFile) {
+ $url = '';
+ if (!$this->isCli()) {
+ $script_name = basename($this->server('SCRIPT_FILENAME'));
+ if (basename($this->server('SCRIPT_NAME')) === $script_name) {
+ $url = $this->server('SCRIPT_NAME');
+ } elseif (basename($this->server('PHP_SELF')) === $script_name) {
+ $url = $this->server('PHP_SELF');
+ } elseif (basename($this->server('ORIG_SCRIPT_NAME')) === $script_name) {
+ $url = $this->server('ORIG_SCRIPT_NAME');
+ } elseif (($pos = strpos($this->server('PHP_SELF'), '/' . $script_name)) !== false) {
+ $url = substr($this->server('SCRIPT_NAME'), 0, $pos) . '/' . $script_name;
+ } elseif ($this->server('DOCUMENT_ROOT') && str_starts_with($this->server('SCRIPT_FILENAME'), $this->server('DOCUMENT_ROOT'))) {
+ $url = str_replace('\\', '/', str_replace($this->server('DOCUMENT_ROOT'), '', $this->server('SCRIPT_FILENAME')));
+ }
+ }
+ $this->baseFile = $url;
+ }
+
+ return $complete ? $this->domain() . $this->baseFile : $this->baseFile;
+ }
+
+ /**
+ * 设置URL访问根地址
+ * @access public
+ * @param string $url URL地址
+ * @return $this
+ */
+ public function setRoot(string $url)
+ {
+ $this->root = $url;
+ return $this;
+ }
+
+ /**
+ * 获取URL访问根地址
+ * @access public
+ * @param bool $complete 是否包含完整域名
+ * @return string
+ */
+ public function root(bool $complete = false): string
+ {
+ if (!$this->root) {
+ $file = $this->baseFile();
+ if ($file && !str_starts_with($this->url(), $file)) {
+ $file = str_replace('\\', '/', dirname($file));
+ }
+ $this->root = rtrim($file, '/');
+ }
+
+ return $complete ? $this->domain() . $this->root : $this->root;
+ }
+
+ /**
+ * 获取URL访问根目录
+ * @access public
+ * @return string
+ */
+ public function rootUrl(): string
+ {
+ $base = $this->root();
+ $root = str_contains($base, '.') ? ltrim(dirname($base), DIRECTORY_SEPARATOR) : $base;
+
+ if ('' != $root) {
+ $root = '/' . ltrim($root, '/');
+ }
+
+ return $root;
+ }
+
+ /**
+ * 设置当前请求的pathinfo
+ * @access public
+ * @param string $pathinfo
+ * @return $this
+ */
+ public function setPathinfo(string $pathinfo)
+ {
+ $this->pathinfo = $pathinfo;
+ return $this;
+ }
+
+ /**
+ * 获取当前请求URL的pathinfo信息(含URL后缀)
+ * @access public
+ * @return string
+ */
+ public function pathinfo(): string
+ {
+ if (is_null($this->pathinfo)) {
+ if (isset($_GET[$this->varPathinfo])) {
+ // 判断URL里面是否有兼容模式参数
+ $pathinfo = $_GET[$this->varPathinfo];
+ unset($_GET[$this->varPathinfo]);
+ unset($this->get[$this->varPathinfo]);
+ } elseif ($this->server('PATH_INFO')) {
+ $pathinfo = $this->server('PATH_INFO');
+ } elseif (str_contains(PHP_SAPI, 'cli')) {
+ $pathinfo = str_contains($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI');
+ }
+
+ // 分析PATHINFO信息
+ if (!isset($pathinfo)) {
+ foreach ($this->pathinfoFetch as $type) {
+ if ($this->server($type)) {
+ $pathinfo = str_starts_with($this->server($type), $this->server('SCRIPT_NAME')) ?
+ substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type);
+ break;
+ }
+ }
+ }
+
+ if (!empty($pathinfo)) {
+ unset($this->get[$pathinfo], $this->request[$pathinfo]);
+ }
+
+ $this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/');
+ }
+
+ return $this->pathinfo;
+ }
+
+ /**
+ * 当前URL的访问后缀
+ * @access public
+ * @return string
+ */
+ public function ext(): string
+ {
+ return pathinfo($this->pathinfo(), PATHINFO_EXTENSION);
+ }
+
+ /**
+ * 获取当前请求的时间
+ * @access public
+ * @param bool $float 是否使用浮点类型
+ * @return integer|float
+ */
+ public function time(bool $float = false)
+ {
+ return $float ? $this->server('REQUEST_TIME_FLOAT') : $this->server('REQUEST_TIME');
+ }
+
+ /**
+ * 当前请求的资源类型
+ * @access public
+ * @return string
+ */
+ public function type(): string
+ {
+ $accept = $this->server('HTTP_ACCEPT');
+
+ if (empty($accept)) {
+ return '';
+ }
+
+ foreach ($this->mimeType as $key => $val) {
+ $array = explode(',', $val);
+ foreach ($array as $k => $v) {
+ if (stristr($accept, $v)) {
+ return $key;
+ }
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * 设置资源类型
+ * @access public
+ * @param string|array $type 资源类型名
+ * @param string $val 资源类型
+ * @return void
+ */
+ public function mimeType($type, $val = ''): void
+ {
+ if (is_array($type)) {
+ $this->mimeType = array_merge($this->mimeType, $type);
+ } else {
+ $this->mimeType[$type] = $val;
+ }
+ }
+
+ /**
+ * 设置请求类型
+ * @access public
+ * @param string $method 请求类型
+ * @return $this
+ */
+ public function setMethod(string $method)
+ {
+ $this->method = strtoupper($method);
+ return $this;
+ }
+
+ /**
+ * 当前的请求类型
+ * @access public
+ * @param bool $origin 是否获取原始请求类型
+ * @return string
+ */
+ public function method(bool $origin = false): string
+ {
+ if ($origin) {
+ // 获取原始请求类型
+ return $this->server('REQUEST_METHOD') ?: 'GET';
+ }
+
+ if (!$this->method) {
+ if (isset($this->post[$this->varMethod])) {
+ $method = strtolower($this->post[$this->varMethod]);
+ if (in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) {
+ $this->method = strtoupper($method);
+ $this->{$method} = $this->post;
+ } else {
+ $this->method = 'POST';
+ }
+ unset($this->post[$this->varMethod]);
+ } elseif ($this->server('HTTP_X_HTTP_METHOD_OVERRIDE')) {
+ $this->method = strtoupper($this->server('HTTP_X_HTTP_METHOD_OVERRIDE'));
+ } else {
+ $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
+ }
+ }
+
+ return $this->method;
+ }
+
+ /**
+ * 是否为GET请求
+ * @access public
+ * @return bool
+ */
+ public function isGet(): bool
+ {
+ return $this->method() == 'GET';
+ }
+
+ /**
+ * 是否为POST请求
+ * @access public
+ * @return bool
+ */
+ public function isPost(): bool
+ {
+ return $this->method() == 'POST';
+ }
+
+ /**
+ * 是否为PUT请求
+ * @access public
+ * @return bool
+ */
+ public function isPut(): bool
+ {
+ return $this->method() == 'PUT';
+ }
+
+ /**
+ * 是否为DELTE请求
+ * @access public
+ * @return bool
+ */
+ public function isDelete(): bool
+ {
+ return $this->method() == 'DELETE';
+ }
+
+ /**
+ * 是否为HEAD请求
+ * @access public
+ * @return bool
+ */
+ public function isHead(): bool
+ {
+ return $this->method() == 'HEAD';
+ }
+
+ /**
+ * 是否为PATCH请求
+ * @access public
+ * @return bool
+ */
+ public function isPatch(): bool
+ {
+ return $this->method() == 'PATCH';
+ }
+
+ /**
+ * 是否为OPTIONS请求
+ * @access public
+ * @return bool
+ */
+ public function isOptions(): bool
+ {
+ return $this->method() == 'OPTIONS';
+ }
+
+ /**
+ * 是否为cli
+ * @access public
+ * @return bool
+ */
+ public function isCli(): bool
+ {
+ return PHP_SAPI == 'cli';
+ }
+
+ /**
+ * 是否为cgi
+ * @access public
+ * @return bool
+ */
+ public function isCgi(): bool
+ {
+ return str_starts_with(PHP_SAPI, 'cgi');
+ }
+
+ /**
+ * 获取当前请求的参数
+ * @access public
+ * @param string|array $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function param($name = '', $default = null, string | array | null $filter = '')
+ {
+ if (empty($this->mergeParam)) {
+ $method = $this->method(true);
+
+ // 自动获取请求变量
+ $vars = match ($method) {
+ 'POST' => $this->post(false),
+ 'PUT', 'DELETE', 'PATCH' => $this->put(false),
+ default => [],
+ };
+
+ // 当前请求参数和URL地址中的参数合并
+ $this->param = array_merge($this->param, $this->route(false), $this->get(false), $vars);
+
+ $this->mergeParam = true;
+ }
+
+ if (is_array($name)) {
+ return $this->only($name, $this->param, $filter);
+ }
+
+ return $this->input($this->param, $name, $default, $filter);
+ }
+
+ /**
+ * 获取包含文件在内的请求参数
+ * @access public
+ * @param string|array $name 变量名
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function all(string | array $name = '', string | array | null $filter = '')
+ {
+ $data = array_merge($this->param(), $this->file() ?: []);
+
+ if (is_array($name)) {
+ $data = $this->only($name, $data, $filter);
+ } elseif ($name) {
+ $data = $data[$name] ?? null;
+ }
+
+ return $data;
+ }
+
+ /**
+ * 设置路由变量
+ * @access public
+ * @param Rule $rule 路由对象
+ * @return $this
+ */
+ public function setRule(Rule $rule)
+ {
+ $this->rule = $rule;
+ return $this;
+ }
+
+ /**
+ * 获取当前路由对象
+ * @access public
+ * @return Rule|null
+ */
+ public function rule()
+ {
+ return $this->rule;
+ }
+
+ /**
+ * 设置路由变量
+ * @access public
+ * @param array $route 路由变量
+ * @return $this
+ */
+ public function setRoute(array $route)
+ {
+ $this->route = array_merge($this->route, $route);
+ $this->mergeParam = false;
+ return $this;
+ }
+
+ /**
+ * 获取路由参数
+ * @access public
+ * @param string|array|bool $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function route(string | array | bool $name = '', $default = null, string | array | null $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->route, $filter);
+ }
+
+ return $this->input($this->route, $name, $default, $filter);
+ }
+
+ /**
+ * 获取GET参数
+ * @access public
+ * @param string|array|bool $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function get(string | array | bool $name = '', $default = null, string | array | null $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->get, $filter);
+ }
+
+ return $this->input($this->get, $name, $default, $filter);
+ }
+
+ /**
+ * 获取中间件传递的参数
+ * @access public
+ * @param string|null $name 变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function middleware(?string $name = null, $default = null)
+ {
+ if (is_null($name)) {
+ return $this->middleware;
+ }
+ return $this->middleware[$name] ?? $default;
+ }
+
+ /**
+ * 获取POST参数
+ * @access public
+ * @param bool|string|array $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function post(string | array | bool $name = '', $default = null, string | array | null $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->post, $filter);
+ }
+
+ return $this->input($this->post, $name, $default, $filter);
+ }
+
+ /**
+ * 获取PUT参数
+ * @access public
+ * @param string|array|bool $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function put(string | array | bool $name = '', $default = null, string | array | null $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->put, $filter);
+ }
+
+ return $this->input($this->put, $name, $default, $filter);
+ }
+
+ protected function getInputData(string $content): array
+ {
+ $contentType = $this->contentType();
+ if ('application/x-www-form-urlencoded' == $contentType) {
+ parse_str($content, $data);
+ return $data;
+ }
+
+ if (str_contains($contentType, 'json')) {
+ return (array) json_decode($content, true);
+ }
+
+ return [];
+ }
+
+ /**
+ * 设置获取DELETE参数
+ * @access public
+ * @param mixed $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function delete(string | array | bool $name = '', $default = null, string | array | null $filter = '')
+ {
+ return $this->put($name, $default, $filter);
+ }
+
+ /**
+ * 设置获取PATCH参数
+ * @access public
+ * @param mixed $name 变量名
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function patch(string | array | bool $name = '', $default = null, string | array | null $filter = '')
+ {
+ return $this->put($name, $default, $filter);
+ }
+
+ /**
+ * 获取request变量
+ * @access public
+ * @param string|array|bool $name 数据名称
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function request(string | array | bool $name = '', $default = null, string | array | null $filter = '')
+ {
+ if (is_array($name)) {
+ return $this->only($name, $this->request, $filter);
+ }
+
+ return $this->input($this->request, $name, $default, $filter);
+ }
+
+ /**
+ * 获取环境变量
+ * @access public
+ * @param string $name 数据名称
+ * @param string|null $default 默认值
+ * @return mixed
+ */
+ public function env(string $name = '', ?string $default = null)
+ {
+ if (empty($name)) {
+ return $this->env->get();
+ }
+ return $this->env->get(strtoupper($name), $default);
+ }
+
+ /**
+ * 获取session数据
+ * @access public
+ * @param string $name 数据名称
+ * @param string|null $default 默认值
+ * @return mixed
+ */
+ public function session(string $name = '', $default = null)
+ {
+ if ('' === $name) {
+ return $this->session->all();
+ }
+ return $this->session->get($name, $default);
+ }
+
+ /**
+ * 获取cookie参数
+ * @access public
+ * @param mixed $name 数据名称
+ * @param string|null $default 默认值
+ * @param string|array|null $filter 过滤方法
+ * @return mixed
+ */
+ public function cookie(string $name = '', $default = null, string | array | null $filter = '')
+ {
+ if (!empty($name)) {
+ $data = $this->getData($this->cookie, $name, $default);
+ } else {
+ $data = $this->cookie;
+ }
+
+ // 解析过滤器
+ $filter = $this->getFilter($filter, $default);
+
+ if (is_array($data)) {
+ array_walk_recursive($data, [$this, 'filterValue'], $filter);
+ } else {
+ $this->filterValue($data, $name, $filter);
+ }
+
+ return $data;
+ }
+
+ /**
+ * 获取server参数
+ * @access public
+ * @param string $name 数据名称
+ * @param string $default 默认值
+ * @return mixed
+ */
+ public function server(string $name = '', string $default = '')
+ {
+ if (empty($name)) {
+ return $this->server;
+ }
+ return $this->server[strtoupper($name)] ?? $default;
+ }
+
+ /**
+ * 获取上传的文件信息
+ * @access public
+ * @param string $name 名称
+ * @return null|array|UploadedFile
+ */
+ public function file(string $name = '')
+ {
+ $files = $this->file;
+ if (!empty($files)) {
+ if (str_contains($name, '.')) {
+ [$name, $sub] = explode('.', $name);
+ }
+
+ // 处理上传文件
+ $array = $this->dealUploadFile($files, $name);
+
+ if ('' === $name) {
+ // 获取全部文件
+ return $array;
+ } elseif (isset($sub) && isset($array[$name][$sub])) {
+ return $array[$name][$sub];
+ } elseif (isset($array[$name])) {
+ return $array[$name];
+ }
+ }
+ }
+
+ protected function dealUploadFile(array $files, string $name): array
+ {
+ $array = [];
+ foreach ($files as $key => $file) {
+ if (is_array($file['name'])) {
+ $item = [];
+ $keys = array_keys($file);
+ $count = count($file['name']);
+
+ for ($i = 0; $i < $count; $i++) {
+ if ($file['error'][$i] > 0) {
+ if ($name == $key) {
+ $this->throwUploadFileError($file['error'][$i]);
+ } else {
+ continue;
+ }
+ }
+
+ $temp['key'] = $key;
+
+ foreach ($keys as $_key) {
+ $temp[$_key] = $file[$_key][$i];
+ }
+
+ $item[] = new UploadedFile($temp['tmp_name'], $temp['name'], $temp['type'], $temp['error']);
+ }
+
+ $array[$key] = $item;
+ } else {
+ if ($file instanceof File) {
+ $array[$key] = $file;
+ } else {
+ if ($file['error'] > 0) {
+ if ($key == $name) {
+ $this->throwUploadFileError($file['error']);
+ } else {
+ continue;
+ }
+ }
+
+ $array[$key] = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error']);
+ }
+ }
+ }
+
+ return $array;
+ }
+
+ protected function throwUploadFileError($error)
+ {
+ static $fileUploadErrors = [
+ 1 => 'upload File size exceeds the maximum value',
+ 2 => 'upload File size exceeds the maximum value',
+ 3 => 'only the portion of file is uploaded',
+ 4 => 'no file to uploaded',
+ 6 => 'upload temp dir not found',
+ 7 => 'file write error',
+ ];
+
+ $msg = Lang::get($fileUploadErrors[$error]);
+ throw new Exception($msg, $error);
+ }
+
+ /**
+ * 设置或者获取当前的Header
+ * @access public
+ * @param string $name header名称
+ * @param string|null $default 默认值
+ * @return string|array|null
+ */
+ public function header(string $name = '', ?string $default = null)
+ {
+ if ('' === $name) {
+ return $this->header;
+ }
+
+ $name = str_replace('_', '-', strtolower($name));
+ return $this->header[$name] ?? $default;
+ }
+
+ /**
+ * 获取变量 支持过滤和默认值
+ * @access public
+ * @param array $data 数据源
+ * @param string|false $name 字段名
+ * @param mixed $default 默认值
+ * @param string|array|null $filter 过滤函数
+ * @return mixed
+ */
+ public function input(array $data = [], string | bool $name = '', $default = null, string | array | null $filter = '')
+ {
+ if (false === $name) {
+ // 获取原始数据
+ return $data;
+ }
+
+ $name = (string) $name;
+ if ('' != $name) {
+ // 解析name
+ if (str_contains($name, '/')) {
+ [$name, $type] = explode('/', $name);
+ }
+
+ $data = $this->getData($data, $name);
+ }
+
+ return $this->filterData($data, $filter, $name, $default, $type ?? '');
+ }
+
+ protected function filterData($data, $filter, $name, $default, $type)
+ {
+ if (is_null($data)) {
+ return $default;
+ }
+
+ if (is_object($data)) {
+ return $data;
+ }
+
+ // 解析过滤器
+ $filter = $this->getFilter($filter, $default);
+
+ if (is_array($data)) {
+ array_walk_recursive($data, [$this, 'filterValue'], $filter);
+ } else {
+ $this->filterValue($data, $name, $filter);
+ }
+
+ if ($type) {
+ // 强制类型转换
+ $this->typeCast($data, $type);
+ }
+
+ return $data;
+ }
+
+ /**
+ * 强制类型转换
+ * @access protected
+ * @param mixed $data
+ * @param string $type
+ * @return mixed
+ */
+ protected function typeCast(&$data, string $type)
+ {
+ $data = match (strtolower($type)) {
+ 'a' => (array) $data,
+ 'b' => (bool) $data,
+ 'd' => (int) $data,
+ 'f' => (float) $data,
+ 's' => is_scalar($data) ? (string) $data : throw new \InvalidArgumentException('variable type error:' . gettype($data)),
+ default => $data,
+ };
+ }
+
+ /**
+ * 获取数据
+ * @access protected
+ * @param array $data 数据源
+ * @param string $name 字段名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ protected function getData(array $data, string $name, $default = null)
+ {
+ foreach (explode('.', $name) as $val) {
+ if (isset($data[$val])) {
+ $data = $data[$val];
+ } else {
+ return $default;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * 设置或获取当前的过滤规则
+ * @access public
+ * @param mixed $filter 过滤规则
+ * @return mixed
+ */
+ public function filter($filter = null)
+ {
+ if (is_null($filter)) {
+ return $this->filter;
+ }
+
+ $this->filter = $filter;
+
+ return $this;
+ }
+
+ protected function getFilter($filter, $default): array
+ {
+ if (is_null($filter)) {
+ $filter = [];
+ } else {
+ $filter = $filter ?: $this->filter;
+ if (is_string($filter) && !str_contains($filter, '/')) {
+ $filter = explode(',', $filter);
+ } else {
+ $filter = (array) $filter;
+ }
+ }
+
+ $filter[] = $default;
+
+ return $filter;
+ }
+
+ /**
+ * 递归过滤给定的值
+ * @access public
+ * @param mixed $value 键值
+ * @param mixed $key 键名
+ * @param array $filters 过滤方法+默认值
+ * @return mixed
+ */
+ public function filterValue(&$value, $key, $filters)
+ {
+ $default = array_pop($filters);
+
+ foreach ($filters as $filter) {
+ if (is_callable($filter)) {
+ // 调用函数或者方法过滤
+ if (is_null($value)) {
+ continue;
+ }
+
+ $value = call_user_func($filter, $value);
+ } elseif (is_scalar($value)) {
+ if (is_string($filter) && str_contains($filter, '/')) {
+ // 正则过滤
+ if (!preg_match($filter, $value)) {
+ // 匹配不成功返回默认值
+ $value = $default;
+ break;
+ }
+ } elseif (!empty($filter)) {
+ // filter函数不存在时, 则使用filter_var进行过滤
+ // filter为非整形值时, 调用filter_id取得过滤id
+ $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
+ if (false === $value) {
+ $value = $default;
+ break;
+ }
+ }
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * 是否存在某个请求参数
+ * @access public
+ * @param string $name 变量名
+ * @param string $type 变量类型
+ * @param bool $checkEmpty 是否检测空值
+ * @return bool
+ */
+ public function has(string $name, string $type = 'param', bool $checkEmpty = false): bool
+ {
+ if (!in_array($type, ['param', 'get', 'post', 'put', 'patch', 'route', 'delete', 'cookie', 'session', 'env', 'request', 'server', 'header', 'file'])) {
+ return false;
+ }
+
+ $param = empty($this->$type) ? $this->$type() : $this->$type;
+
+ if (is_object($param)) {
+ return $param->has($name);
+ }
+
+ // 按.拆分成多维数组进行判断
+ foreach (explode('.', $name) as $val) {
+ if (isset($param[$val])) {
+ $param = $param[$val];
+ } else {
+ return false;
+ }
+ }
+
+ return ($checkEmpty && '' === $param) ? false : true;
+ }
+
+ /**
+ * 获取指定的参数
+ * @access public
+ * @param array $name 变量名
+ * @param mixed $data 数据或者变量类型
+ * @param string|array|null $filter 过滤方法
+ * @return array
+ */
+ public function only(array $name, $data = 'param', string | array | null $filter = ''): array
+ {
+ $data = is_array($data) ? $data : $this->$data();
+
+ $item = [];
+ foreach ($name as $key => $val) {
+ $type = '';
+ if (is_int($key)) {
+ if (str_contains($val, '/')) {
+ [$val, $type] = explode('/', $val);
+ }
+ $default = null;
+ $key = $val;
+ if (!key_exists($key, $data)) {
+ continue;
+ }
+ } else {
+ if (str_contains($key, '/')) {
+ [$key, $type] = explode('/', $key);
+ }
+ $default = $val;
+ }
+
+ $item[$key] = $this->filterData($data[$key] ?? $default, $filter, $key, $default, $type);
+ }
+
+ return $item;
+ }
+
+ /**
+ * 排除指定参数获取
+ * @access public
+ * @param array $name 变量名
+ * @param string $type 变量类型
+ * @return mixed
+ */
+ public function except(array $name, string $type = 'param'): array
+ {
+ $param = $this->$type();
+
+ foreach ($name as $key) {
+ if (isset($param[$key])) {
+ unset($param[$key]);
+ }
+ }
+
+ return $param;
+ }
+
+ /**
+ * 当前是否ssl
+ * @access public
+ * @return bool
+ */
+ public function isSsl(): bool
+ {
+ if ($this->server('HTTPS') && ('1' == $this->server('HTTPS') || 'on' == strtolower($this->server('HTTPS')))) {
+ return true;
+ } elseif ('https' == $this->server('REQUEST_SCHEME')) {
+ return true;
+ } elseif ('443' == $this->server('SERVER_PORT')) {
+ return true;
+ } elseif ('https' == $this->server('HTTP_X_FORWARDED_PROTO')) {
+ return true;
+ } elseif ($this->httpsAgentName && $this->server($this->httpsAgentName)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 当前是否JSON请求
+ * @access public
+ * @return bool
+ */
+ public function isJson(): bool
+ {
+ $acceptType = $this->type();
+
+ return str_contains($acceptType, 'json');
+ }
+
+ /**
+ * 当前是否Ajax请求
+ * @access public
+ * @param bool $ajax true 获取原始ajax请求
+ * @return bool
+ */
+ public function isAjax(bool $ajax = false): bool
+ {
+ $value = $this->server('HTTP_X_REQUESTED_WITH');
+ $result = $value && 'xmlhttprequest' == strtolower($value) ? true : false;
+
+ if (true === $ajax) {
+ return $result;
+ }
+
+ return $this->param($this->varAjax) ? true : $result;
+ }
+
+ /**
+ * 当前是否Pjax请求
+ * @access public
+ * @param bool $pjax true 获取原始pjax请求
+ * @return bool
+ */
+ public function isPjax(bool $pjax = false): bool
+ {
+ $result = !empty($this->server('HTTP_X_PJAX')) ? true : false;
+
+ if (true === $pjax) {
+ return $result;
+ }
+
+ return $this->param($this->varPjax) ? true : $result;
+ }
+
+ /**
+ * 获取客户端IP地址
+ * @access public
+ * @return string
+ */
+ public function ip(): string
+ {
+ if (!empty($this->realIP)) {
+ return $this->realIP;
+ }
+
+ $this->realIP = $this->server('REMOTE_ADDR', '');
+
+ // 如果指定了前端代理服务器IP以及其会发送的IP头
+ // 则尝试获取前端代理服务器发送过来的真实IP
+ $proxyIp = $this->proxyServerIp;
+ $proxyIpHeader = $this->proxyServerIpHeader;
+
+ if (count($proxyIp) > 0 && count($proxyIpHeader) > 0) {
+ // 从指定的HTTP头中依次尝试获取IP地址
+ // 直到获取到一个合法的IP地址
+ foreach ($proxyIpHeader as $header) {
+ $tempIP = $this->server($header);
+
+ if (empty($tempIP)) {
+ continue;
+ }
+
+ $tempIP = trim(explode(',', $tempIP)[0]);
+
+ if (!$this->isValidIP($tempIP)) {
+ $tempIP = null;
+ } else {
+ break;
+ }
+ }
+
+ // tempIP不为空,说明获取到了一个IP地址
+ // 这时我们检查 REMOTE_ADDR 是不是指定的前端代理服务器之一
+ // 如果是的话说明该 IP头 是由前端代理服务器设置的
+ // 否则则是伪装的
+ if (!empty($tempIP)) {
+ $realIPBin = $this->ip2bin($this->realIP);
+
+ foreach ($proxyIp as $ip) {
+ $serverIPElements = explode('/', $ip);
+ $serverIP = $serverIPElements[0];
+ $serverIPPrefix = $serverIPElements[1] ?? 128;
+ $serverIPBin = $this->ip2bin($serverIP);
+
+ // IP类型不符
+ if (strlen($realIPBin) !== strlen($serverIPBin)) {
+ continue;
+ }
+
+ if (strncmp($realIPBin, $serverIPBin, (int) $serverIPPrefix) === 0) {
+ $this->realIP = $tempIP;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!$this->isValidIP($this->realIP)) {
+ $this->realIP = '0.0.0.0';
+ }
+
+ return $this->realIP;
+ }
+
+ /**
+ * 检测是否是合法的IP地址
+ *
+ * @param string $ip IP地址
+ * @param string $type IP地址类型 (ipv4, ipv6)
+ *
+ * @return boolean
+ */
+ public function isValidIP(string $ip, string $type = ''): bool
+ {
+ $flag = match (strtolower($type)) {
+ 'ipv4' => FILTER_FLAG_IPV4,
+ 'ipv6' => FILTER_FLAG_IPV6,
+ default => 0,
+ };
+
+ return boolval(filter_var($ip, FILTER_VALIDATE_IP, $flag));
+ }
+
+ /**
+ * 将IP地址转换为二进制字符串
+ *
+ * @param string $ip
+ *
+ * @return string
+ */
+ public function ip2bin(string $ip): string
+ {
+ if ($this->isValidIP($ip, 'ipv6')) {
+ $IPHex = str_split(bin2hex(inet_pton($ip)), 4);
+ foreach ($IPHex as $key => $value) {
+ $IPHex[$key] = intval($value, 16);
+ }
+ $IPBin = vsprintf('%016b%016b%016b%016b%016b%016b%016b%016b', $IPHex);
+ } else {
+ $IPHex = str_split(bin2hex(inet_pton($ip)), 2);
+ foreach ($IPHex as $key => $value) {
+ $IPHex[$key] = intval($value, 16);
+ }
+ $IPBin = vsprintf('%08b%08b%08b%08b', $IPHex);
+ }
+
+ return $IPBin;
+ }
+
+ /**
+ * 检测是否使用手机访问
+ * @access public
+ * @return bool
+ */
+ public function isMobile(): bool
+ {
+ if ($this->server('HTTP_VIA') && stristr($this->server('HTTP_VIA'), "wap")) {
+ return true;
+ } elseif ($this->server('HTTP_ACCEPT') && str_contains(strtoupper($this->server('HTTP_ACCEPT')), "VND.WAP.WML")) {
+ return true;
+ } elseif ($this->server('HTTP_X_WAP_PROFILE') || $this->server('HTTP_PROFILE')) {
+ return true;
+ } elseif ($this->server('HTTP_USER_AGENT') && preg_match('/(blackberry|configuration\/cldc|hp |hp-|htc |htc_|htc-|iemobile|kindle|midp|mmp|motorola|mobile|nokia|opera mini|opera |Googlebot-Mobile|YahooSeeker\/M1A1-R2D2|android|iphone|ipod|mobi|palm|palmos|pocket|portalmmm|ppc;|smartphone|sonyericsson|sqh|spv|symbian|treo|up.browser|up.link|vodafone|windows ce|xda |xda_)/i', $this->server('HTTP_USER_AGENT'))) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 当前URL地址中的scheme参数
+ * @access public
+ * @return string
+ */
+ public function scheme(): string
+ {
+ return $this->isSsl() ? 'https' : 'http';
+ }
+
+ /**
+ * 当前请求URL地址中的query参数
+ * @access public
+ * @return string
+ */
+ public function query(): string
+ {
+ return $this->server('QUERY_STRING', '');
+ }
+
+ /**
+ * 设置当前请求的host(包含端口)
+ * @access public
+ * @param string $host 主机名(含端口)
+ * @return $this
+ */
+ public function setHost(string $host)
+ {
+ $this->host = $host;
+
+ return $this;
+ }
+
+ /**
+ * 当前请求的host
+ * @access public
+ * @param bool $strict true 仅仅获取HOST
+ * @return string
+ */
+ public function host(bool $strict = false): string
+ {
+ if ($this->host) {
+ $host = $this->host;
+ } else {
+ $host = strval($this->server('HTTP_X_FORWARDED_HOST') ?: $this->server('HTTP_HOST'));
+ }
+
+ return true === $strict && str_contains($host, ':') ? strstr($host, ':', true) : $host;
+ }
+
+ /**
+ * 当前请求URL地址中的port参数
+ * @access public
+ * @return int
+ */
+ public function port(): int
+ {
+ return (int) ($this->server('HTTP_X_FORWARDED_PORT') ?: $this->server('SERVER_PORT', ''));
+ }
+
+ /**
+ * 当前请求 SERVER_PROTOCOL
+ * @access public
+ * @return string
+ */
+ public function protocol(): string
+ {
+ return $this->server('SERVER_PROTOCOL', '');
+ }
+
+ /**
+ * 当前请求 REMOTE_PORT
+ * @access public
+ * @return int
+ */
+ public function remotePort(): int
+ {
+ return (int) $this->server('REMOTE_PORT', '');
+ }
+
+ /**
+ * 当前请求 HTTP_CONTENT_TYPE
+ * @access public
+ * @return string
+ */
+ public function contentType(): string
+ {
+ $contentType = $this->header('Content-Type');
+
+ if ($contentType) {
+ if (str_contains($contentType, ';')) {
+ [$type] = explode(';', $contentType);
+ } else {
+ $type = $contentType;
+ }
+ return trim($type);
+ }
+
+ return '';
+ }
+
+ /**
+ * 获取当前请求的安全Key
+ * @access public
+ * @return string
+ */
+ public function secureKey(): string
+ {
+ if (is_null($this->secureKey)) {
+ $this->secureKey = uniqid('', true);
+ }
+
+ return $this->secureKey;
+ }
+
+ /**
+ * 设置当前的分层名
+ * @access public
+ * @param string $layer 控制器分层名
+ * @return $this
+ */
+ public function setLayer(string $layer)
+ {
+ $this->layer = $layer;
+ return $this;
+ }
+
+ /**
+ * 设置当前的控制器名
+ * @access public
+ * @param string $controller 控制器名
+ * @return $this
+ */
+ public function setController(string $controller)
+ {
+ $this->controller = $controller;
+ return $this;
+ }
+
+ /**
+ * 设置当前的操作名
+ * @access public
+ * @param string $action 操作名
+ * @return $this
+ */
+ public function setAction(string $action)
+ {
+ $this->action = $action;
+ return $this;
+ }
+
+ /**
+ * 获取当前的模块名
+ * @access public
+ * @param bool $convert 转换为小写
+ * @return string
+ */
+ public function layer(bool $convert = false): string
+ {
+ $name = $this->layer ?: '';
+ return $convert ? strtolower($name) : $name;
+ }
+
+ /**
+ * 获取当前的控制器名
+ * @access public
+ * @param bool $convert 转换为小写
+ * @param bool $base 仅返回basename
+ * @return string
+ */
+ public function controller(bool $convert = false, bool $base = false): string
+ {
+ $name = $this->controller ?: '';
+ if ($base) {
+ $name = basename(str_replace('.', '/', $name));
+ }
+ return $convert ? strtolower($name) : $name;
+ }
+
+ /**
+ * 获取当前的操作名
+ * @access public
+ * @param bool $convert 转换为小写
+ * @return string
+ */
+ public function action(bool $convert = false): string
+ {
+ $name = $this->action ?: '';
+ return $convert ? strtolower($name) : $name;
+ }
+
+ /**
+ * 设置或者获取当前请求的content
+ * @access public
+ * @return string
+ */
+ public function getContent(): string
+ {
+ if (is_null($this->content)) {
+ $this->content = $this->input;
+ }
+
+ return $this->content;
+ }
+
+ /**
+ * 获取当前请求的php://input
+ * @access public
+ * @return string
+ */
+ public function getInput(): string
+ {
+ return $this->input;
+ }
+
+ /**
+ * 生成请求令牌
+ * @access public
+ * @param string $name 令牌名称
+ * @param mixed $type 令牌生成方法
+ * @return string
+ */
+ public function buildToken(string $name = '__token__', $type = 'md5'): string
+ {
+ $type = is_callable($type) ? $type : 'md5';
+ $token = call_user_func($type, $this->server('REQUEST_TIME_FLOAT'));
+
+ $this->session->set($name, $token);
+
+ return $token;
+ }
+
+ /**
+ * 检查请求令牌
+ * @access public
+ * @param string $token 令牌名称
+ * @param array $data 表单数据
+ * @return bool
+ */
+ public function checkToken(string $token = '__token__', array $data = []): bool
+ {
+ if (in_array($this->method(), ['GET', 'HEAD', 'OPTIONS'], true)) {
+ return true;
+ }
+
+ if (!$this->session->has($token)) {
+ // 令牌数据无效
+ return false;
+ }
+
+ // Header验证
+ if ($this->header('X-CSRF-TOKEN') && $this->session->get($token) === $this->header('X-CSRF-TOKEN')) {
+ // 防止重复提交
+ $this->session->delete($token); // 验证完成销毁session
+ return true;
+ }
+
+ if (empty($data)) {
+ $data = $this->post();
+ }
+
+ // 令牌验证
+ if (isset($data[$token]) && $this->session->get($token) === $data[$token]) {
+ // 防止重复提交
+ $this->session->delete($token); // 验证完成销毁session
+ return true;
+ }
+
+ // 开启TOKEN重置
+ $this->session->delete($token);
+ return false;
+ }
+
+ /**
+ * 设置在中间件传递的数据
+ * @access public
+ * @param array $middleware 数据
+ * @return $this
+ */
+ public function withMiddleware(array $middleware)
+ {
+ $this->middleware = array_merge($this->middleware, $middleware);
+ return $this;
+ }
+
+ /**
+ * 设置GET数据
+ * @access public
+ * @param array $get 数据
+ * @return $this
+ */
+ public function withGet(array $get)
+ {
+ $this->get = $get;
+ return $this;
+ }
+
+ /**
+ * 设置POST数据
+ * @access public
+ * @param array $post 数据
+ * @return $this
+ */
+ public function withPost(array $post)
+ {
+ $this->post = $post;
+ return $this;
+ }
+
+ /**
+ * 设置COOKIE数据
+ * @access public
+ * @param array $cookie 数据
+ * @return $this
+ */
+ public function withCookie(array $cookie)
+ {
+ $this->cookie = $cookie;
+ return $this;
+ }
+
+ /**
+ * 更新COOKIE数据
+ * @access public
+ * @param string $name cookie名
+ * @param mixed $value 数据
+ * @return void
+ */
+ public function setCookie(string $name, mixed $value)
+ {
+ $this->cookie[$name] = $value;
+ }
+
+ /**
+ * 设置SESSION数据
+ * @access public
+ * @param Session $session 数据
+ * @return $this
+ */
+ public function withSession(Session $session)
+ {
+ $this->session = $session;
+ return $this;
+ }
+
+ /**
+ * 设置SERVER数据
+ * @access public
+ * @param array $server 数据
+ * @return $this
+ */
+ public function withServer(array $server)
+ {
+ $this->server = array_change_key_case($server, CASE_UPPER);
+ return $this;
+ }
+
+ /**
+ * 设置HEADER数据
+ * @access public
+ * @param array $header 数据
+ * @return $this
+ */
+ public function withHeader(array $header)
+ {
+ $this->header = array_change_key_case($header);
+ return $this;
+ }
+
+ /**
+ * 设置ENV数据
+ * @access public
+ * @param Env $env 数据
+ * @return $this
+ */
+ public function withEnv(Env $env)
+ {
+ $this->env = $env;
+ return $this;
+ }
+
+ /**
+ * 设置php://input数据
+ * @access public
+ * @param string $input RAW数据
+ * @return $this
+ */
+ public function withInput(string $input)
+ {
+ $this->input = $input;
+ if (!empty($input)) {
+ $inputData = $this->getInputData($input);
+ if (!empty($inputData)) {
+ $this->post = $inputData;
+ $this->put = $inputData;
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * 设置文件上传数据
+ * @access public
+ * @param array $files 上传信息
+ * @return $this
+ */
+ public function withFiles(array $files)
+ {
+ $this->file = $files;
+ return $this;
+ }
+
+ /**
+ * 设置ROUTE变量
+ * @access public
+ * @param array $route 数据
+ * @return $this
+ */
+ public function withRoute(array $route)
+ {
+ $this->route = $route;
+ return $this;
+ }
+
+ /**
+ * 设置中间传递数据
+ * @access public
+ * @param string $name 参数名
+ * @param mixed $value 值
+ */
+ public function __set(string $name, $value)
+ {
+ $this->middleware[$name] = $value;
+ }
+
+ /**
+ * 获取中间传递数据的值
+ * @access public
+ * @param string $name 名称
+ * @return mixed
+ */
+ public function __get(string $name)
+ {
+ return $this->middleware($name);
+ }
+
+ /**
+ * 检测中间传递数据的值
+ * @access public
+ * @param string $name 名称
+ * @return boolean
+ */
+ public function __isset(string $name): bool
+ {
+ return isset($this->middleware[$name]);
+ }
+
+ // ArrayAccess
+ public function offsetExists(mixed $name): bool
+ {
+ return $this->has($name);
+ }
+
+ public function offsetGet(mixed $name): mixed
+ {
+ return $this->param($name);
+ }
+
+ public function offsetSet(mixed $name, mixed $value): void {}
+
+ public function offsetUnset(mixed $name): void {}
+}
diff --git a/src/think/Response.php b/src/think/Response.php
new file mode 100644
index 0000000..c0387e7
--- /dev/null
+++ b/src/think/Response.php
@@ -0,0 +1,425 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+/**
+ * 响应输出基础类
+ * @package think
+ */
+abstract class Response
+{
+ /**
+ * 原始数据
+ * @var mixed
+ */
+ protected $data;
+
+ /**
+ * 当前contentType
+ * @var string
+ */
+ protected $contentType = 'text/html';
+
+ /**
+ * 字符集
+ * @var string
+ */
+ protected $charset = 'utf-8';
+
+ /**
+ * 状态码
+ * @var integer
+ */
+ protected $code = 200;
+
+ /**
+ * 是否允许请求缓存
+ * @var bool
+ */
+ protected $allowCache = true;
+
+ /**
+ * 输出参数
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * header参数
+ * @var array
+ */
+ protected $header = [];
+
+ /**
+ * 输出内容
+ * @var string
+ */
+ protected $content = null;
+
+ /**
+ * Cookie对象
+ * @var Cookie
+ */
+ protected $cookie;
+
+ /**
+ * Session对象
+ * @var Session
+ */
+ protected $session;
+
+ /**
+ * 初始化
+ * @access protected
+ * @param mixed $data 输出数据
+ * @param int $code 状态码
+ */
+ protected function init($data = '', int $code = 200)
+ {
+ $this->data($data);
+ $this->code = $code;
+
+ $this->contentType($this->contentType, $this->charset);
+ }
+
+ /**
+ * 创建Response对象
+ * @access public
+ * @param mixed $data 输出数据
+ * @param string $type 输出类型
+ * @param int $code 状态码
+ * @return Response
+ */
+ public static function create($data = '', string $type = 'html', int $code = 200): Response
+ {
+ $class = str_contains($type, '\\') ? $type : '\\think\\response\\' . ucfirst(strtolower($type));
+
+ return Container::getInstance()->invokeClass($class, [$data, $code]);
+ }
+
+ /**
+ * 设置Session对象
+ * @access public
+ * @param Session $session Session对象
+ * @return $this
+ */
+ public function setSession(Session $session)
+ {
+ $this->session = $session;
+ return $this;
+ }
+
+ /**
+ * 发送数据到客户端
+ * @access public
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ public function send(): void
+ {
+ // 处理输出数据
+ $data = $this->getContent();
+
+ if (!headers_sent()) {
+ if (!empty($this->header)) {
+ // 发送状态码
+ http_response_code($this->code);
+ // 发送头部信息
+ foreach ($this->header as $name => $val) {
+ header($name . (!is_null($val) ? ':' . $val : ''));
+ }
+ }
+
+ if ($this->cookie) {
+ $this->cookie->save();
+ }
+ }
+
+ $this->sendData($data);
+
+ if (function_exists('fastcgi_finish_request')) {
+ // 提高页面响应
+ fastcgi_finish_request();
+ }
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ */
+ protected function output($data)
+ {
+ return $data;
+ }
+
+ /**
+ * 输出数据
+ * @access protected
+ * @param string $data 要处理的数据
+ * @return void
+ */
+ protected function sendData(string $data): void
+ {
+ echo $data;
+ }
+
+ /**
+ * 输出的参数
+ * @access public
+ * @param mixed $options 输出参数
+ * @return $this
+ */
+ public function options(array $options = [])
+ {
+ $this->options = array_merge($this->options, $options);
+
+ return $this;
+ }
+
+ /**
+ * 输出数据设置
+ * @access public
+ * @param mixed $data 输出数据
+ * @return $this
+ */
+ public function data($data)
+ {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * 是否允许请求缓存
+ * @access public
+ * @param bool $cache 允许请求缓存
+ * @return $this
+ */
+ public function allowCache(bool $cache)
+ {
+ $this->allowCache = $cache;
+
+ return $this;
+ }
+
+ /**
+ * 是否允许请求缓存
+ * @access public
+ * @return bool
+ */
+ public function isAllowCache()
+ {
+ return $this->allowCache;
+ }
+
+ /**
+ * 设置Cookie
+ * @access public
+ * @param string $name cookie名称
+ * @param string $value cookie值
+ * @param mixed $option 可选参数
+ * @return $this
+ */
+ public function cookie(string $name, string $value, $option = null)
+ {
+ $this->cookie->set($name, $value, $option);
+
+ return $this;
+ }
+
+ /**
+ * 设置响应头
+ * @access public
+ * @param array $header 参数
+ * @return $this
+ */
+ public function header(array $header = [])
+ {
+ $this->header = array_merge($this->header, $header);
+
+ return $this;
+ }
+
+ /**
+ * 设置页面输出内容
+ * @access public
+ * @param mixed $content
+ * @return $this
+ */
+ public function content($content)
+ {
+ if (
+ null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([
+ $content,
+ '__toString',
+ ])
+ ) {
+ throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content)));
+ }
+
+ $this->content = (string) $content;
+
+ return $this;
+ }
+
+ /**
+ * 发送HTTP状态
+ * @access public
+ * @param integer $code 状态码
+ * @return $this
+ */
+ public function code(int $code)
+ {
+ $this->code = $code;
+
+ return $this;
+ }
+
+ /**
+ * LastModified
+ * @access public
+ * @param string $time
+ * @return $this
+ */
+ public function lastModified(string $time)
+ {
+ $this->header['Last-Modified'] = $time;
+
+ return $this;
+ }
+
+ /**
+ * Expires
+ * @access public
+ * @param string $time
+ * @return $this
+ */
+ public function expires(string $time)
+ {
+ $this->header['Expires'] = $time;
+
+ return $this;
+ }
+
+ /**
+ * ETag
+ * @access public
+ * @param string $eTag
+ * @return $this
+ */
+ public function eTag(string $eTag)
+ {
+ $this->header['ETag'] = $eTag;
+
+ return $this;
+ }
+
+ /**
+ * 页面缓存控制
+ * @access public
+ * @param string $cache 状态码
+ * @return $this
+ */
+ public function cacheControl(string $cache)
+ {
+ $this->header['Cache-control'] = $cache;
+
+ return $this;
+ }
+
+ /**
+ * 页面输出类型
+ * @access public
+ * @param string $contentType 输出类型
+ * @param string $charset 输出编码
+ * @return $this
+ */
+ public function contentType(string $contentType, string $charset = 'utf-8')
+ {
+ $this->header['Content-Type'] = $contentType . '; charset=' . $charset;
+
+ return $this;
+ }
+
+ /**
+ * 获取头部信息
+ * @access public
+ * @param string $name 头部名称
+ * @return mixed
+ */
+ public function getHeader(string $name = '')
+ {
+ if (!empty($name)) {
+ return $this->header[$name] ?? null;
+ }
+
+ return $this->header;
+ }
+
+ /**
+ * 获取原始数据
+ * @access public
+ * @return mixed
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * 获取输出数据
+ * @access public
+ * @return string
+ */
+ public function getContent(): string
+ {
+ if (null == $this->content) {
+ $content = $this->output($this->data);
+
+ if (
+ null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([
+ $content,
+ '__toString',
+ ])
+ ) {
+ throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content)));
+ }
+
+ $this->content = (string) $content;
+ }
+
+ return $this->content;
+ }
+
+ /**
+ * 获取状态码
+ * @access public
+ * @return integer
+ */
+ public function getCode(): int
+ {
+ return $this->code;
+ }
+
+ /**
+ * 获取Cookie对象
+ * @access public
+ * @return Cookie
+ */
+ public function getCookie()
+ {
+ return $this->cookie;
+ }
+}
diff --git a/src/think/Route.php b/src/think/Route.php
new file mode 100644
index 0000000..aea87cb
--- /dev/null
+++ b/src/think/Route.php
@@ -0,0 +1,889 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think;
+
+use Closure;
+use think\exception\RouteNotFoundException;
+use think\route\Dispatch;
+use think\route\dispatch\Callback;
+use think\route\Domain;
+use think\route\Resource;
+use think\route\ResourceRegister;
+use think\route\Rule;
+use think\route\RuleGroup;
+use think\route\RuleItem;
+use think\route\RuleName;
+use think\route\Url as UrlBuild;
+
+/**
+ * 路由管理类
+ * @package think
+ */
+class Route
+{
+ /**
+ * REST定义
+ * @var array
+ */
+ protected $rest = [
+ 'index' => ['get', '', 'index'],
+ 'create' => ['get', '/create', 'create'],
+ 'edit' => ['get', '//edit', 'edit'],
+ 'read' => ['get', '/', 'read'],
+ 'save' => ['post', '', 'save'],
+ 'update' => ['put', '/', 'update'],
+ 'delete' => ['delete', '/', 'delete'],
+ ];
+
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [
+ // pathinfo分隔符
+ 'pathinfo_depr' => '/',
+ // 是否开启路由延迟解析
+ 'url_lazy_route' => false,
+ // 是否强制使用路由
+ 'url_route_must' => false,
+ // 是否区分大小写
+ 'url_case_sensitive' => false,
+ // 合并路由规则
+ 'route_rule_merge' => false,
+ // 路由是否完全匹配
+ 'route_complete_match' => false,
+ // 子目录是否自动路由分组
+ 'route_auto_group' => false,
+ // 去除斜杠
+ 'remove_slash' => false,
+ // 默认的路由变量规则
+ 'default_route_pattern' => '[\w\.]+',
+ // URL伪静态后缀
+ 'url_html_suffix' => 'html',
+ // 访问控制器层名称
+ 'controller_layer' => 'controller',
+ // 空控制器名
+ 'empty_controller' => 'Error',
+ // 是否使用控制器后缀
+ 'controller_suffix' => false,
+ // 默认模块名
+ 'default_module' => 'index',
+ // 默认控制器名
+ 'default_controller' => 'Index',
+ // 默认操作名
+ 'default_action' => 'index',
+ // 操作方法后缀
+ 'action_suffix' => '',
+ // 非路由变量是否使用普通参数方式(用于URL生成)
+ 'url_common_param' => true,
+ // 操作方法的参数绑定方式 route get param
+ 'action_bind_param' => 'get',
+ // API版本header变量
+ 'api_version' => 'Api-Version',
+ ];
+
+ /**
+ * 请求对象
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * @var RuleName
+ */
+ protected $ruleName;
+
+ /**
+ * 当前HOST
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * 当前分组对象
+ * @var RuleGroup
+ */
+ protected $group;
+
+ /**
+ * 域名对象
+ * @var Domain[]
+ */
+ protected $domains = [];
+
+ /**
+ * 跨域路由规则
+ * @var RuleGroup
+ */
+ protected $cross;
+
+ /**
+ * 路由是否延迟解析
+ * @var bool
+ */
+ protected $lazy = false;
+
+ /**
+ * (分组)路由规则是否合并解析
+ * @var bool
+ */
+ protected $mergeRuleRegex = false;
+
+ /**
+ * 是否去除URL最后的斜线
+ * @var bool
+ */
+ protected $removeSlash = false;
+
+ public function __construct(protected App $app)
+ {
+ $this->ruleName = new RuleName();
+ $this->setDefaultDomain();
+
+ if (is_file($this->app->getRuntimePath() . 'route.php')) {
+ // 读取路由映射文件
+ $this->import(include $this->app->getRuntimePath() . 'route.php');
+ }
+
+ $this->config = array_merge($this->config, $this->app->config->get('route'));
+
+ $this->init();
+ }
+
+ protected function init()
+ {
+ if (!empty($this->config['middleware'])) {
+ $this->app->middleware->import($this->config['middleware'], 'route');
+ }
+
+ $this->lazy($this->config['url_lazy_route']);
+ $this->mergeRuleRegex = $this->config['route_rule_merge'];
+ $this->removeSlash = $this->config['remove_slash'];
+
+ $this->group->removeSlash($this->removeSlash);
+
+ // 注册全局MISS路由
+ $this->miss(function () {
+ return Response::create('', 'html', 204)->header(['Allow' => 'GET, POST, PUT, DELETE']);
+ }, 'options');
+ }
+
+ public function config(?string $name = null)
+ {
+ if (is_null($name)) {
+ return $this->config;
+ }
+
+ return $this->config[$name] ?? null;
+ }
+
+ /**
+ * 设置路由域名及分组(包括资源路由)是否延迟解析
+ * @access public
+ * @param bool $lazy 路由是否延迟解析
+ * @return $this
+ */
+ public function lazy(bool $lazy = true)
+ {
+ $this->lazy = $lazy;
+ return $this;
+ }
+
+ /**
+ * 设置路由域名及分组(包括资源路由)是否合并解析
+ * @access public
+ * @param bool $merge 路由是否合并解析
+ * @return $this
+ */
+ public function mergeRuleRegex(bool $merge = true)
+ {
+ $this->mergeRuleRegex = $merge;
+ $this->group->mergeRuleRegex($merge);
+
+ return $this;
+ }
+
+ /**
+ * 初始化默认域名
+ * @access protected
+ * @return void
+ */
+ protected function setDefaultDomain(): void
+ {
+ // 注册默认域名
+ $domain = new Domain($this);
+
+ $this->domains['-'] = $domain;
+
+ // 默认分组
+ $this->group = $domain;
+ }
+
+ /**
+ * 设置当前分组
+ * @access public
+ * @param RuleGroup $group 域名
+ * @return void
+ */
+ public function setGroup(RuleGroup $group): void
+ {
+ $this->group = $group;
+ }
+
+ /**
+ * 获取指定标识的路由分组 不指定则获取当前分组
+ * @access public
+ * @param string $name 分组标识
+ * @return RuleGroup
+ */
+ public function getGroup(?string $name = null)
+ {
+ return $name ? $this->ruleName->getGroup($name) : $this->group;
+ }
+
+ /**
+ * 注册变量规则
+ * @access public
+ * @param array $pattern 变量规则
+ * @return $this
+ */
+ public function pattern(array $pattern)
+ {
+ $this->group->pattern($pattern);
+
+ return $this;
+ }
+
+ /**
+ * 注册路由参数
+ * @access public
+ * @param array $option 参数
+ * @return $this
+ */
+ public function option(array $option)
+ {
+ $this->group->option($option);
+
+ return $this;
+ }
+
+ /**
+ * 注册域名路由
+ * @access public
+ * @param string|array $name 子域名
+ * @param mixed $rule 路由规则
+ * @return Domain
+ */
+ public function domain(string | array $name, $rule = null): Domain
+ {
+ // 支持多个域名使用相同路由规则
+ $domainName = is_array($name) ? array_shift($name) : $name;
+
+ if (!isset($this->domains[$domainName])) {
+ $domain = (new Domain($this, $domainName, $rule, $this->lazy))
+ ->removeSlash($this->removeSlash)
+ ->mergeRuleRegex($this->mergeRuleRegex);
+
+ $this->domains[$domainName] = $domain;
+ } else {
+ $domain = $this->domains[$domainName];
+ $domain->parseGroupRule($rule);
+ }
+
+ if (is_array($name) && !empty($name)) {
+ foreach ($name as $item) {
+ $this->domains[$item] = $domainName;
+ }
+ }
+
+ // 返回域名对象
+ return $domain;
+ }
+
+ /**
+ * 获取域名
+ * @access public
+ * @return array
+ */
+ public function getDomains(): array
+ {
+ return $this->domains;
+ }
+
+ /**
+ * 获取域名路由的绑定信息
+ * @access public
+ * @param string $domain 子域名
+ * @return string|null
+ */
+ public function getDomainBind(?string $domain = null)
+ {
+ if ($domain && isset($this->domains[$domain])) {
+ $item = $this->domains[$domain];
+ if (is_string($item)) {
+ $item = $this->domains[$item];
+ }
+ return $item->getBind();
+ }
+ }
+
+ /**
+ * 获取RuleName对象
+ * @access public
+ * @return RuleName
+ */
+ public function getRuleName(): RuleName
+ {
+ return $this->ruleName;
+ }
+
+ /**
+ * 读取路由标识
+ * @access public
+ * @param string $name 路由标识
+ * @param string $domain 域名
+ * @param string $method 请求类型
+ * @return array
+ */
+ public function getName(?string $name = null, ?string $domain = null, string $method = '*'): array
+ {
+ return $this->ruleName->getName($name, $domain, $method);
+ }
+
+ /**
+ * 批量导入路由标识
+ * @access public
+ * @param array $name 路由标识
+ * @return void
+ */
+ public function import(array $name): void
+ {
+ $this->ruleName->import($name);
+ }
+
+ /**
+ * 注册路由标识
+ * @access public
+ * @param string $name 路由标识
+ * @param RuleItem $ruleItem 路由规则
+ * @param bool $first 是否优先
+ * @return void
+ */
+ public function setName(string $name, RuleItem $ruleItem, bool $first = false): void
+ {
+ $this->ruleName->setName($name, $ruleItem, $first);
+ }
+
+ /**
+ * 保存路由规则
+ * @access public
+ * @param string $rule 路由规则
+ * @param RuleItem $ruleItem RuleItem对象
+ * @return void
+ */
+ public function setRule(string $rule, ?RuleItem $ruleItem = null): void
+ {
+ $this->ruleName->setRule($rule, $ruleItem);
+ }
+
+ /**
+ * 读取路由
+ * @access public
+ * @param string $rule 路由规则
+ * @return RuleItem[]
+ */
+ public function getRule(string $rule): array
+ {
+ return $this->ruleName->getRule($rule);
+ }
+
+ /**
+ * 读取路由列表
+ * @access public
+ * @return array
+ */
+ public function getRuleList(): array
+ {
+ return $this->ruleName->getRuleList();
+ }
+
+ /**
+ * 清空路由规则
+ * @access public
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->ruleName->clear();
+
+ if ($this->group) {
+ $this->group->clear();
+ }
+ }
+
+ /**
+ * 注册路由规则
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @param string $method 请求类型
+ * @return RuleItem
+ */
+ public function rule(string $rule, $route = null, string $method = '*'): RuleItem
+ {
+ return $this->group->addRule($rule, $route, $method);
+ }
+
+ /**
+ * 设置路由规则全局有效
+ * @access public
+ * @param Rule $rule 路由规则
+ * @return $this
+ */
+ public function setCrossDomainRule(Rule $rule)
+ {
+ if (!isset($this->cross)) {
+ $this->cross = (new RuleGroup($this))->mergeRuleRegex($this->mergeRuleRegex);
+ }
+
+ $this->cross->addRuleItem($rule);
+
+ return $this;
+ }
+
+ /**
+ * 注册路由分组
+ * @access public
+ * @param string|Closure $name 分组名称或者参数
+ * @param mixed $route 分组路由
+ * @return RuleGroup
+ */
+ public function group(string | Closure $name, $route = null): RuleGroup
+ {
+ if ($name instanceof Closure) {
+ $route = $name;
+ $name = '';
+ }
+
+ return (new RuleGroup($this, $this->group, $name, $route, $this->lazy))
+ ->removeSlash($this->removeSlash)
+ ->mergeRuleRegex($this->mergeRuleRegex);
+ }
+
+ /**
+ * 注册路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function any(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, '*');
+ }
+
+ /**
+ * 注册GET路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function get(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'GET');
+ }
+
+ /**
+ * 注册POST路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function post(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'POST');
+ }
+
+ /**
+ * 注册PUT路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function put(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'PUT');
+ }
+
+ /**
+ * 注册DELETE路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function delete(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'DELETE');
+ }
+
+ /**
+ * 注册PATCH路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function patch(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'PATCH');
+ }
+
+ /**
+ * 注册HEAD路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function head(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'HEAD');
+ }
+
+ /**
+ * 注册OPTIONS路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @return RuleItem
+ */
+ public function options(string $rule, $route): RuleItem
+ {
+ return $this->rule($rule, $route, 'OPTIONS');
+ }
+
+ /**
+ * 注册资源路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param string $route 路由地址
+ * @param Closure $extend 扩展规则
+ * @return Resource|ResourceRegister
+ */
+ public function resource(string $rule, string $route, ?Closure $extend = null)
+ {
+ $resource = (new Resource($this, $this->group, $rule, $route, $this->rest))->extend($extend);
+
+ if (!$this->lazy) {
+ return new ResourceRegister($resource);
+ }
+
+ return $resource;
+ }
+
+ /**
+ * 注册视图路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param string $template 路由模板地址
+ * @param array $vars 模板变量
+ * @return RuleItem
+ */
+ public function view(string $rule, string $template = '', array $vars = []): RuleItem
+ {
+ return $this->rule($rule, function () use ($vars, $template) {
+ return Response::create($template, 'view')->assign($vars);
+ }, 'GET');
+ }
+
+ /**
+ * 注册重定向路由
+ * @access public
+ * @param string $rule 路由规则
+ * @param string $route 路由地址
+ * @param int $status 状态码
+ * @return RuleItem
+ */
+ public function redirect(string $rule, string $route = '', int $status = 301): RuleItem
+ {
+ return $this->rule($rule, function (Request $request) use ($status, $route) {
+ $search = $replace = [];
+ $matches = $request->rule()->getVars();
+
+ foreach ($matches as $key => $value) {
+ $search[] = '<' . $key . '>';
+ $replace[] = $value;
+ $search[] = '{' . $key . '}';
+ $replace[] = $value;
+ $search[] = ':' . $key;
+ $replace[] = $value;
+ }
+
+ $route = str_replace($search, $replace, $route);
+ return Response::create($route, 'redirect')->code($status);
+ }, '*');
+ }
+
+ /**
+ * rest方法定义和修改
+ * @access public
+ * @param string|array $name 方法名称
+ * @param array|bool $resource 资源
+ * @return $this
+ */
+ public function rest(string | array $name, array | bool $resource = [])
+ {
+ if (is_array($name)) {
+ $this->rest = $resource ? $name : array_merge($this->rest, $name);
+ } else {
+ $this->rest[$name] = $resource;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 获取rest方法定义的参数
+ * @access public
+ * @param string $name 方法名称
+ * @return array|null
+ */
+ public function getRest(?string $name = null)
+ {
+ if (is_null($name)) {
+ return $this->rest;
+ }
+
+ return $this->rest[$name] ?? null;
+ }
+
+ /**
+ * 注册未匹配路由规则后的处理
+ * @access public
+ * @param string|Closure $route 路由地址
+ * @param string $method 请求类型
+ * @return RuleItem
+ */
+ public function miss(string | Closure $route, string $method = '*'): RuleItem
+ {
+ return $this->group->miss($route, $method);
+ }
+
+ /**
+ * 路由调度
+ * @param Request $request
+ * @param Closure|bool $withRoute
+ * @return Response
+ */
+ public function dispatch(Request $request, Closure | bool $withRoute = true)
+ {
+ $this->request = $request;
+ $this->host = $this->request->host(true);
+ $completeMatch = (bool) $this->config['route_complete_match'];
+ $url = str_replace($this->config['pathinfo_depr'], '|', $this->path());
+
+ if ($withRoute) {
+ if ($withRoute instanceof Closure) {
+ $withRoute();
+ }
+ // 路由检测
+ $dispatch = $this->check($url, $completeMatch);
+ }
+
+ if (empty($dispatch)) {
+ // 默认URL调度
+ $dispatch = $this->checkUrlDispatch($url);
+ }
+
+ $dispatch->init($this->app);
+
+ return $this->app->middleware->pipeline('route')
+ ->send($request)
+ ->then(function () use ($dispatch) {
+ return $dispatch->run();
+ });
+ }
+
+ /**
+ * 检测URL路由
+ * @access public
+ * @param bool $completeMatch
+ * @return Dispatch|false
+ * @throws RouteNotFoundException
+ */
+ public function check(string $url, bool $completeMatch = false)
+ {
+ // 检测域名路由
+ $result = $this->checkDomain()->check($this->request, $url, $completeMatch);
+
+ if (false === $result && !empty($this->cross)) {
+ // 检测跨域路由
+ $result = $this->cross->check($this->request, $url, $completeMatch);
+ }
+
+ if (false === $result && $this->config['url_route_must']) {
+ // 开启强制路由
+ throw new RouteNotFoundException();
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取当前请求URL的pathinfo信息(不含URL后缀)
+ * @access protected
+ * @return string
+ */
+ protected function path(): string
+ {
+ $suffix = $this->config['url_html_suffix'];
+ $pathinfo = $this->request->pathinfo();
+
+ if (false === $suffix) {
+ // 禁止伪静态访问
+ $path = $pathinfo;
+ } elseif ($suffix) {
+ // 去除正常的URL后缀
+ $path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
+ } else {
+ // 允许任何后缀访问
+ $path = preg_replace('/\.' . $this->request->ext() . '$/i', '', $pathinfo);
+ }
+
+ return $path;
+ }
+
+ /**
+ * 自动多模块URL路由 如使用多模块在路由定义文件最后定义
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @param bool $middleware 自动注册中间件
+ * @return RuleItem
+ */
+ public function auto(string $rule = '[:__module__]/[:__controller__]/[:__action__]', $route = ':__module__/:__controller__/:__action__', bool $middleware = false): RuleItem
+ {
+ return $this->rule($rule, $route)
+ ->name('__think_auto_route__')
+ ->pattern([
+ '__module__' => '[A-Za-z0-9\.\_]+',
+ '__controller__' => '[A-Za-z0-9\.\_]+',
+ '__action__' => '[A-Za-z0-9\_]+',
+ ])->default([
+ '__module__' => $this->config['default_module'],
+ '__controller__' => $this->config['default_controller'],
+ '__action__' => $this->config['default_action'],
+ ])->autoMiddleware($middleware);
+ }
+
+ /**
+ * 检测默认URL解析路由
+ * @access public
+ * @param string $url URL
+ * @return Dispatch
+ */
+ protected function checkUrlDispatch(string $url): Dispatch
+ {
+ if ($this->request->method() == 'OPTIONS') {
+ // 自动响应options请求
+ return new Callback($this->request, $this->group, function () {
+ return Response::create('', 'html', 204)->header(['Allow' => 'GET, POST, PUT, DELETE']);
+ });
+ }
+
+ return $this->group->auto()->checkBind($this->request, $url);
+ }
+
+ /**
+ * 检测域名的路由规则
+ * @access protected
+ * @return Domain
+ */
+ protected function checkDomain(): Domain
+ {
+ $item = false;
+
+ if (count($this->domains) > 1) {
+ // 获取当前子域名
+ $subDomain = $this->request->subDomain();
+ $domain = $subDomain ? explode('.', $subDomain) : [];
+ $domain2 = $domain ? array_pop($domain) : '';
+
+ if ($domain) {
+ // 存在三级域名
+ $domain3 = array_pop($domain);
+ }
+
+ if (isset($this->domains[$this->host])) {
+ // 子域名配置
+ $item = $this->domains[$this->host];
+ } elseif (isset($this->domains[$subDomain])) {
+ $item = $this->domains[$subDomain];
+ } elseif (isset($this->domains['*.' . $domain2]) && !empty($domain3)) {
+ // 泛三级域名
+ $item = $this->domains['*.' . $domain2];
+ $panDomain = $domain3;
+ } elseif (isset($this->domains['*']) && !empty($domain2)) {
+ // 泛二级域名
+ if ('www' != $domain2) {
+ $item = $this->domains['*'];
+ $panDomain = $domain2;
+ }
+ }
+
+ if (isset($panDomain)) {
+ // 保存当前泛域名
+ $this->request->setPanDomain($panDomain);
+ }
+ }
+
+ if (false === $item) {
+ // 检测全局域名规则
+ $item = $this->domains['-'];
+ }
+
+ if (is_string($item)) {
+ $item = $this->domains[$item];
+ }
+
+ return $item;
+ }
+
+ /**
+ * URL生成 支持路由反射
+ * @access public
+ * @param string $url 路由地址
+ * @param array $vars 参数 ['a'=>'val1', 'b'=>'val2']
+ * @return UrlBuild
+ */
+ public function buildUrl(string $url = '', array $vars = []): UrlBuild
+ {
+ return $this->app->make(UrlBuild::class, [$this, $this->app, $url, $vars], true);
+ }
+
+ /**
+ * 设置全局的路由分组参数
+ * @access public
+ * @param string $method 方法名
+ * @param array $args 调用参数
+ * @return RuleGroup
+ */
+ public function __call($method, $args)
+ {
+ return call_user_func_array([$this->group, $method], $args);
+ }
+}
diff --git a/src/think/Service.php b/src/think/Service.php
new file mode 100644
index 0000000..988f735
--- /dev/null
+++ b/src/think/Service.php
@@ -0,0 +1,63 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use Closure;
+use think\event\RouteLoaded;
+
+/**
+ * 系统服务基础类
+ * @method void register()
+ * @method void boot()
+ */
+abstract class Service
+{
+ public function __construct(protected App $app)
+ {
+ }
+
+ /**
+ * 加载路由
+ * @access protected
+ * @param string $path 路由路径
+ */
+ protected function loadRoutesFrom(string $path)
+ {
+ $this->registerRoutes(function () use ($path) {
+ include $path;
+ });
+ }
+
+ /**
+ * 注册路由
+ * @param Closure $closure
+ */
+ protected function registerRoutes(Closure $closure)
+ {
+ $this->app->event->listen(RouteLoaded::class, $closure);
+ }
+
+ /**
+ * 添加指令
+ * @access protected
+ * @param array|string $commands 指令
+ */
+ protected function commands($commands)
+ {
+ $commands = is_array($commands) ? $commands : func_get_args();
+
+ Console::starting(function (Console $console) use ($commands) {
+ $console->addCommands($commands);
+ });
+ }
+}
diff --git a/src/think/Session.php b/src/think/Session.php
new file mode 100644
index 0000000..b3532e6
--- /dev/null
+++ b/src/think/Session.php
@@ -0,0 +1,65 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use think\helper\Arr;
+use think\session\Store;
+
+/**
+ * Session管理类
+ * @package think
+ * @mixin Store
+ */
+class Session extends Manager
+{
+ protected $namespace = '\\think\\session\\driver\\';
+
+ protected function createDriver(string $name)
+ {
+ $handler = parent::createDriver($name);
+
+ return new Store($this->getConfig('name') ?: 'PHPSESSID', $handler, $this->getConfig('serialize'));
+ }
+
+ /**
+ * 获取Session配置
+ * @access public
+ * @param null|string $name 名称
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getConfig(?string $name = null, $default = null)
+ {
+ if (!is_null($name)) {
+ return $this->app->config->get('session.' . $name, $default);
+ }
+
+ return $this->app->config->get('session');
+ }
+
+ protected function resolveConfig(string $name)
+ {
+ $config = $this->app->config->get('session', []);
+ Arr::forget($config, 'type');
+ return $config;
+ }
+
+ /**
+ * 默认驱动
+ * @return string|null
+ */
+ public function getDefaultDriver()
+ {
+ return $this->app->config->get('session.type', 'file');
+ }
+}
diff --git a/src/think/View.php b/src/think/View.php
new file mode 100644
index 0000000..4ce4607
--- /dev/null
+++ b/src/think/View.php
@@ -0,0 +1,187 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use think\contract\TemplateHandlerInterface;
+use think\helper\Arr;
+
+/**
+ * 视图类
+ * @package think
+ */
+class View extends Manager
+{
+
+ protected $namespace = '\\think\\view\\driver\\';
+
+ /**
+ * 模板变量
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 内容过滤
+ * @var mixed
+ */
+ protected $filter;
+
+ /**
+ * 获取模板引擎
+ * @access public
+ * @param string $type 模板引擎类型
+ * @return TemplateHandlerInterface
+ */
+ public function engine(?string $type = null)
+ {
+ return $this->driver($type);
+ }
+
+ /**
+ * 模板变量赋值
+ * @access public
+ * @param string|array $name 模板变量
+ * @param mixed $value 变量值
+ * @return $this
+ */
+ public function assign(string|array $name, $value = null)
+ {
+ if (is_array($name)) {
+ $this->data = array_merge($this->data, $name);
+ } else {
+ $this->data[$name] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 视图过滤
+ * @access public
+ * @param Callable $filter 过滤方法或闭包
+ * @return $this
+ */
+ public function filter(?callable $filter = null)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * 解析和获取模板内容 用于输出
+ * @access public
+ * @param string $template 模板文件名或者内容
+ * @param array $vars 模板变量
+ * @return string
+ * @throws \Exception
+ */
+ public function fetch(string $template = '', array $vars = []): string
+ {
+ return $this->getContent(function () use ($vars, $template) {
+ $this->engine()->fetch($template, array_merge($this->data, $vars));
+ });
+ }
+
+ /**
+ * 渲染内容输出
+ * @access public
+ * @param string $content 内容
+ * @param array $vars 模板变量
+ * @return string
+ */
+ public function display(string $content, array $vars = []): string
+ {
+ return $this->getContent(function () use ($vars, $content) {
+ $this->engine()->display($content, array_merge($this->data, $vars));
+ });
+ }
+
+ /**
+ * 获取模板引擎渲染内容
+ * @param $callback
+ * @return string
+ * @throws \Exception
+ */
+ protected function getContent($callback): string
+ {
+ // 页面缓存
+ ob_start();
+ ob_implicit_flush(false);
+
+ // 渲染输出
+ try {
+ $callback();
+ } catch (\Exception $e) {
+ ob_end_clean();
+ throw $e;
+ }
+
+ // 获取并清空缓存
+ $content = ob_get_clean();
+
+ if ($this->filter) {
+ $content = call_user_func_array($this->filter, [$content]);
+ }
+
+ return $content;
+ }
+
+ /**
+ * 模板变量赋值
+ * @access public
+ * @param string $name 变量名
+ * @param mixed $value 变量值
+ */
+ public function __set($name, $value)
+ {
+ $this->data[$name] = $value;
+ }
+
+ /**
+ * 取得模板显示变量的值
+ * @access protected
+ * @param string $name 模板变量
+ * @return mixed
+ */
+ public function __get($name)
+ {
+ return $this->data[$name];
+ }
+
+ /**
+ * 检测模板变量是否设置
+ * @access public
+ * @param string $name 模板变量名
+ * @return bool
+ */
+ public function __isset($name)
+ {
+ return isset($this->data[$name]);
+ }
+
+ protected function resolveConfig(string $name)
+ {
+ $config = $this->app->config->get('view', []);
+ Arr::forget($config, 'type');
+ return $config;
+ }
+
+ /**
+ * 默认驱动
+ * @return string|null
+ */
+ public function getDefaultDriver()
+ {
+ return $this->app->config->get('view.type', 'php');
+ }
+}
diff --git a/src/think/cache/Driver.php b/src/think/cache/Driver.php
new file mode 100644
index 0000000..c37850e
--- /dev/null
+++ b/src/think/cache/Driver.php
@@ -0,0 +1,385 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache;
+
+use Closure;
+use DateInterval;
+use DateTime;
+use DateTimeInterface;
+use Exception;
+use think\Container;
+use think\contract\CacheHandlerInterface;
+use think\exception\InvalidArgumentException;
+use think\exception\InvalidCacheException;
+use Throwable;
+
+/**
+ * 缓存基础类
+ */
+abstract class Driver implements CacheHandlerInterface
+{
+ /**
+ * 驱动句柄
+ * @var object
+ */
+ protected $handler = null;
+
+ /**
+ * 缓存读取次数
+ * @var integer
+ */
+ protected $readTimes = 0;
+
+ /**
+ * 缓存写入次数
+ * @var integer
+ */
+ protected $writeTimes = 0;
+
+ /**
+ * 缓存参数
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * 缓存标签
+ * @var array
+ */
+ protected $tag = [];
+
+ /**
+ * 获取有效期
+ * @access protected
+ * @param integer|DateInterval|DateTimeInterface $expire 有效期
+ * @return int
+ */
+ protected function getExpireTime(int | DateInterval | DateTimeInterface $expire): int
+ {
+ if ($expire instanceof DateTimeInterface) {
+ $expire = $expire->getTimestamp() - time();
+ } elseif ($expire instanceof DateInterval) {
+ $expire = DateTime::createFromFormat('U', (string) time())
+ ->add($expire)
+ ->format('U') - time();
+ }
+
+ return $expire;
+ }
+
+ /**
+ * 获取实际的缓存标识
+ * @access public
+ * @param string $name 缓存名
+ * @return string
+ */
+ public function getCacheKey(string $name): string
+ {
+ return $this->options['prefix'] . $name;
+ }
+
+ /**
+ * 读取缓存并删除
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function pull($name, $default = null)
+ {
+ if ($this->has($name)) {
+ $result = $this->get($name, $default);
+ $this->delete($name);
+ return $result;
+ }
+ return $this->getDefaultValue($name, $default);
+ }
+
+ /**
+ * 追加(数组)缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @return void
+ */
+ public function push($name, $value): void
+ {
+ $item = $this->get($name, []);
+
+ if (!is_array($item)) {
+ throw new InvalidArgumentException('only array cache can be push');
+ }
+
+ $item[] = $value;
+
+ if (count($item) > 1000) {
+ array_shift($item);
+ }
+
+ $item = array_unique($item);
+
+ $this->set($name, $item);
+ }
+
+ /**
+ * 追加TagSet数据
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @return void
+ */
+ public function append($name, $value): void
+ {
+ $this->push($name, $value);
+ }
+
+ /**
+ * 如果不存在则写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int|DateInterval|DateTimeInterface $expire 有效时间 0为永久
+ * @return mixed
+ */
+ public function remember($name, $value, $expire = null)
+ {
+ if ($this->has($name)) {
+ if (($hit = $this->get($name)) !== null) {
+ return $hit;
+ }
+ }
+
+ $time = time();
+
+ while ($time + 5 > time() && $this->has($name . '_lock')) {
+ // 存在锁定则等待
+ usleep(200000);
+ }
+
+ try {
+ // 锁定
+ $this->set($name . '_lock', true);
+
+ if ($value instanceof Closure) {
+ // 获取缓存数据
+ $value = Container::getInstance()->invokeFunction($value);
+ }
+
+ // 缓存数据
+ $this->set($name, $value, $expire);
+
+ // 解锁
+ $this->delete($name . '_lock');
+ } catch (Exception | Throwable $e) {
+ $this->delete($name . '_lock');
+ throw $e;
+ }
+
+ return $value;
+ }
+
+ /**
+ * 缓存标签
+ * @access public
+ * @param string|array $name 标签名
+ * @return TagSet
+ */
+ public function tag($name)
+ {
+ $name = (array) $name;
+ $key = implode('-', $name);
+
+ if (!isset($this->tag[$key])) {
+ $this->tag[$key] = new TagSet($name, $this);
+ }
+
+ return $this->tag[$key];
+ }
+
+ /**
+ * 获取标签包含的缓存标识
+ * @access public
+ * @param string $tag 标签标识
+ * @return array
+ */
+ public function getTagItems(string $tag): array
+ {
+ $name = $this->getTagKey($tag);
+ return $this->get($name, []);
+ }
+
+ /**
+ * 获取实际标签名
+ * @access public
+ * @param string $tag 标签名
+ * @return string
+ */
+ public function getTagKey(string $tag): string
+ {
+ return $this->options['tag_prefix'] . md5($tag);
+ }
+
+ /**
+ * 序列化数据
+ * @access protected
+ * @param mixed $data 缓存数据
+ * @return string
+ */
+ protected function serialize($data)
+ {
+ if (is_numeric($data)) {
+ return $data;
+ }
+
+ $serialize = $this->options['serialize'][0] ?? "serialize";
+
+ return $serialize($data);
+ }
+
+ /**
+ * 反序列化数据
+ * @access protected
+ * @param string $data 缓存数据
+ * @return mixed
+ */
+ protected function unserialize($data)
+ {
+ if (is_numeric($data)) {
+ return $data;
+ }
+ try {
+ $unserialize = $this->options['serialize'][1] ?? "unserialize";
+ $content = $unserialize($data);
+ if (is_null($content)) {
+ throw new InvalidCacheException;
+ } else {
+ return $content;
+ }
+ } catch (Exception | Throwable $e) {
+ throw new InvalidCacheException;
+ }
+ }
+
+ /**
+ * 获取默认值
+ * @access protected
+ * @param string $name 缓存标识
+ * @param mixed $default 默认值
+ * @param bool $fail 是否有异常
+ * @return mixed
+ */
+ protected function getDefaultValue($name, $default, $fail = false)
+ {
+ if ($fail && $this->options['fail_delete']) {
+ $this->delete($name);
+ }
+ return $default instanceof Closure ? $default() : $default;
+ }
+
+ /**
+ * 返回句柄对象,可执行其它高级方法
+ *
+ * @access public
+ * @return object
+ */
+ public function handler()
+ {
+ return $this->handler;
+ }
+
+ /**
+ * 返回缓存读取次数
+ * @return int
+ * @deprecated
+ * @access public
+ */
+ public function getReadTimes(): int
+ {
+ return $this->readTimes;
+ }
+
+ /**
+ * 返回缓存写入次数
+ * @return int
+ * @deprecated
+ * @access public
+ */
+ public function getWriteTimes(): int
+ {
+ return $this->writeTimes;
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param iterable $keys 缓存变量名
+ * @param mixed $default 默认值
+ * @return iterable
+ * @throws InvalidArgumentException
+ */
+ public function getMultiple($keys, $default = null): iterable
+ {
+ $result = [];
+
+ foreach ($keys as $key) {
+ $result[$key] = $this->get($key, $default);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param iterable $values 缓存数据
+ * @param null|int|\DateInterval|DateTimeInterface $ttl 有效时间 0为永久
+ * @return bool
+ */
+ public function setMultiple($values, $ttl = null): bool
+ {
+ foreach ($values as $key => $val) {
+ $result = $this->set($key, $val, $ttl);
+
+ if (false === $result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param iterable $keys 缓存变量名
+ * @return bool
+ * @throws InvalidArgumentException
+ */
+ public function deleteMultiple($keys): bool
+ {
+ foreach ($keys as $key) {
+ $result = $this->delete($key);
+
+ if (false === $result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function __call($method, $args)
+ {
+ return call_user_func_array([$this->handler, $method], $args);
+ }
+}
diff --git a/src/think/cache/TagSet.php b/src/think/cache/TagSet.php
new file mode 100644
index 0000000..5c42c1b
--- /dev/null
+++ b/src/think/cache/TagSet.php
@@ -0,0 +1,121 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types = 1);
+
+namespace think\cache;
+
+use DateInterval;
+use DateTimeInterface;
+
+/**
+ * 标签集合
+ */
+class TagSet
+{
+ /**
+ * 架构函数
+ * @access public
+ * @param array $tag 缓存标签
+ * @param Driver $handler 缓存对象
+ */
+ public function __construct(protected array $tag, protected Driver $handler)
+ {
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param integer|DateInterval|DateTimeInterface $expire 有效时间(秒)
+ * @return bool
+ */
+ public function set($name, $value, $expire = null): bool
+ {
+ $this->handler->set($name, $value, $expire);
+
+ $this->append($name);
+
+ return true;
+ }
+
+ /**
+ * 追加缓存标识到标签
+ * @access public
+ * @param string $name 缓存变量名
+ * @return void
+ */
+ public function append(string $name): void
+ {
+ $name = $this->handler->getCacheKey($name);
+
+ foreach ($this->tag as $tag) {
+ $key = $this->handler->getTagKey($tag);
+ $this->handler->append($key, $name);
+ }
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param iterable $values 缓存数据
+ * @param null|int|DateInterval|DateTimeInterface $ttl 有效时间 0为永久
+ * @return bool
+ */
+ public function setMultiple($values, $ttl = null): bool
+ {
+ foreach ($values as $key => $val) {
+ $result = $this->set($key, $val, $ttl);
+
+ if (false === $result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 如果不存在则写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int $expire 有效时间 0为永久
+ * @return mixed
+ */
+ public function remember($name, $value, $expire = null)
+ {
+ $result = $this->handler->remember($name, $value, $expire);
+
+ $this->append($name);
+
+ return $result;
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ // 指定标签清除
+ foreach ($this->tag as $tag) {
+ $keys = $this->handler->getTagItems($tag);
+ if (!empty($keys)) $this->handler->clearTag($keys);
+
+ $key = $this->handler->getTagKey($tag);
+ $this->handler->delete($key);
+ }
+
+ return true;
+ }
+}
diff --git a/src/think/cache/driver/File.php b/src/think/cache/driver/File.php
new file mode 100644
index 0000000..628c699
--- /dev/null
+++ b/src/think/cache/driver/File.php
@@ -0,0 +1,308 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use DateTimeInterface;
+use FilesystemIterator;
+use think\App;
+use think\cache\Driver;
+use think\exception\InvalidCacheException;
+
+/**
+ * 文件缓存类
+ */
+class File extends Driver
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'expire' => 0,
+ 'cache_subdir' => true,
+ 'prefix' => '',
+ 'path' => '',
+ 'hash_type' => 'md5',
+ 'data_compress' => false,
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ 'fail_delete' => false,
+ ];
+
+ /**
+ * 架构函数
+ * @param App $app
+ * @param array $options 参数
+ */
+ public function __construct(App $app, array $options = [])
+ {
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+
+ if (empty($this->options['path'])) {
+ $this->options['path'] = $app->getRuntimePath() . 'cache';
+ }
+
+ if (!str_ends_with($this->options['path'], DIRECTORY_SEPARATOR)) {
+ $this->options['path'] .= DIRECTORY_SEPARATOR;
+ }
+ }
+
+ /**
+ * 取得变量的存储文件名
+ * @access public
+ * @param string $name 缓存变量名
+ * @return string
+ */
+ public function getCacheKey(string $name): string
+ {
+ $name = hash($this->options['hash_type'], $name);
+
+ if ($this->options['cache_subdir']) {
+ // 使用子目录
+ $name = substr($name, 0, 2) . DIRECTORY_SEPARATOR . substr($name, 2);
+ }
+
+ if ($this->options['prefix']) {
+ $name = $this->options['prefix'] . DIRECTORY_SEPARATOR . $name;
+ }
+
+ return $this->options['path'] . $name . '.php';
+ }
+
+ /**
+ * 获取缓存数据
+ * @param string $name 缓存标识名
+ * @return array|null
+ */
+ protected function getRaw(string $name)
+ {
+ $filename = $this->getCacheKey($name);
+
+ if (!is_file($filename)) {
+ return;
+ }
+
+ $content = @file_get_contents($filename);
+
+ if (false !== $content) {
+ $expire = (int) substr($content, 8, 12);
+ if (0 != $expire && time() - $expire > filemtime($filename)) {
+ //缓存过期删除缓存文件
+ $this->unlink($filename);
+ return;
+ }
+
+ $content = substr($content, 32);
+
+ if ($this->options['data_compress'] && function_exists('gzcompress')) {
+ //启用数据压缩
+ $content = gzuncompress($content);
+ }
+
+ return is_string($content) ? ['content' => (string) $content, 'expire' => $expire] : null;
+ }
+ }
+
+ /**
+ * 判断缓存是否存在
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ return $this->getRaw($name) !== null;
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null): mixed
+ {
+ $raw = $this->getRaw($name);
+
+ try {
+ return is_null($raw) ? $this->getDefaultValue($name, $default) : $this->unserialize($raw['content']);
+ } catch (InvalidCacheException $e) {
+ return $this->getDefaultValue($name, $default, true);
+ }
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int|\DateInterval|DateTimeInterface|null $expire 有效时间 0为永久
+ * @return bool
+ */
+ public function set($name, $value, $expire = null): bool
+ {
+ if (is_null($expire)) {
+ $expire = $this->options['expire'];
+ }
+
+ $expire = $this->getExpireTime($expire);
+ $filename = $this->getCacheKey($name);
+
+ $dir = dirname($filename);
+
+ if (!is_dir($dir)) {
+ try {
+ mkdir($dir, 0755, true);
+ } catch (\Exception $e) {
+ // 创建失败
+ }
+ }
+
+ $data = $this->serialize($value);
+
+ if ($this->options['data_compress'] && function_exists('gzcompress')) {
+ //数据压缩
+ $data = gzcompress($data, 3);
+ }
+
+ $data = "\n" . $data;
+
+ if (str_contains($filename, '://') && !str_starts_with($filename, 'file://')) {
+ //虚拟文件不加锁
+ $result = file_put_contents($filename, $data);
+ } else {
+ $result = file_put_contents($filename, $data, LOCK_EX);
+ }
+
+ if ($result) {
+ clearstatcache();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc($name, $step = 1)
+ {
+ if ($raw = $this->getRaw($name)) {
+ $value = $this->unserialize($raw['content']) + $step;
+ $expire = $raw['expire'];
+ } else {
+ $value = $step;
+ $expire = 0;
+ }
+
+ return $this->set($name, $value, $expire) ? $value : false;
+ }
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec($name, $step = 1)
+ {
+ return $this->inc($name, -$step);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function delete($name): bool
+ {
+ return $this->unlink($this->getCacheKey($name));
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ $dirname = $this->options['path'] . $this->options['prefix'];
+
+ $this->rmdir($dirname);
+
+ return true;
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag($keys): void
+ {
+ foreach ($keys as $key) {
+ $this->unlink($key);
+ }
+ }
+
+ /**
+ * 判断文件是否存在后,删除
+ * @access private
+ * @param string $path
+ * @return bool
+ */
+ private function unlink(string $path): bool
+ {
+ try {
+ return is_file($path) && unlink($path);
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * 删除文件夹
+ * @param $dirname
+ * @return bool
+ */
+ private function rmdir($dirname)
+ {
+ if (!is_dir($dirname)) {
+ return false;
+ }
+
+ $items = new FilesystemIterator($dirname);
+
+ foreach ($items as $item) {
+ if ($item->isDir() && !$item->isLink()) {
+ $this->rmdir($item->getPathname());
+ } else {
+ $this->unlink($item->getPathname());
+ }
+ }
+
+ @rmdir($dirname);
+
+ return true;
+ }
+}
diff --git a/src/think/cache/driver/Memcache.php b/src/think/cache/driver/Memcache.php
new file mode 100644
index 0000000..59eeccc
--- /dev/null
+++ b/src/think/cache/driver/Memcache.php
@@ -0,0 +1,204 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use DateInterval;
+use DateTimeInterface;
+use think\cache\Driver;
+use think\exception\InvalidCacheException;
+
+/**
+ * Memcache缓存类
+ */
+class Memcache extends Driver
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'host' => '127.0.0.1',
+ 'port' => 11211,
+ 'expire' => 0,
+ 'timeout' => 0,
+ 'persistent' => true,
+ 'prefix' => '',
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ 'fail_delete' => false,
+ ];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $options 缓存参数
+ * @throws \BadFunctionCallException
+ */
+ public function __construct(array $options = [])
+ {
+ if (!extension_loaded('memcache')) {
+ throw new \BadFunctionCallException('not support: memcache');
+ }
+
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+
+ $this->handler = new \Memcache;
+
+ // 支持集群
+ $hosts = (array) $this->options['host'];
+ $ports = (array) $this->options['port'];
+
+ if (empty($ports[0])) {
+ $ports[0] = 11211;
+ }
+
+ // 建立连接
+ foreach ($hosts as $i => $host) {
+ $port = $ports[$i] ?? $ports[0];
+ $this->options['timeout'] > 0 ?
+ $this->handler->addServer($host, (int) $port, $this->options['persistent'], 1, (int) $this->options['timeout']) :
+ $this->handler->addServer($host, (int) $port, $this->options['persistent'], 1);
+ }
+ }
+
+ /**
+ * 判断缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ $key = $this->getCacheKey($name);
+
+ return false !== $this->handler->get($key);
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null): mixed
+ {
+ $result = $this->handler->get($this->getCacheKey($name));
+
+ try {
+ return false !== $result ? $this->unserialize($result) : $this->getDefaultValue($name, $default);
+ } catch (InvalidCacheException $e) {
+ return $this->getDefaultValue($name, $default, true);
+ }
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int|DateTimeInterface|DateInterval $expire 有效时间(秒)
+ * @return bool
+ */
+ public function set($name, $value, $expire = null): bool
+ {
+ if (is_null($expire)) {
+ $expire = $this->options['expire'];
+ }
+
+ $key = $this->getCacheKey($name);
+ $expire = $this->getExpireTime($expire);
+ $value = $this->serialize($value);
+
+ if ($this->handler->set($key, $value, 0, $expire)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc($name, $step = 1)
+ {
+ $key = $this->getCacheKey($name);
+
+ if ($this->handler->get($key)) {
+ return $this->handler->increment($key, $step);
+ }
+
+ return $this->handler->set($key, $step);
+ }
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec($name, $step = 1)
+ {
+ $key = $this->getCacheKey($name);
+ $value = $this->handler->get($key) - $step;
+ $res = $this->handler->set($key, $value);
+
+ return !$res ? false : $value;
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param bool|false $ttl
+ * @return bool
+ */
+ public function delete($name, $ttl = false): bool
+ {
+ $key = $this->getCacheKey($name);
+
+ return false === $ttl ?
+ $this->handler->delete($key) :
+ $this->handler->delete($key, $ttl);
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ return $this->handler->flush();
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag($keys): void
+ {
+ foreach ($keys as $key) {
+ $this->handler->delete($key);
+ }
+ }
+}
diff --git a/src/think/cache/driver/Memcached.php b/src/think/cache/driver/Memcached.php
new file mode 100644
index 0000000..5d519ae
--- /dev/null
+++ b/src/think/cache/driver/Memcached.php
@@ -0,0 +1,215 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use DateInterval;
+use DateTimeInterface;
+use think\cache\Driver;
+use think\exception\InvalidCacheException;
+
+/**
+ * Memcached缓存类
+ */
+class Memcached extends Driver
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'host' => '127.0.0.1',
+ 'port' => 11211,
+ 'expire' => 0,
+ 'timeout' => 0,
+ 'prefix' => '',
+ 'option' => [],
+ 'username' => '',
+ 'password' => '',
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ 'fail_delete' => false,
+ ];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $options 缓存参数
+ */
+ public function __construct(array $options = [])
+ {
+ if (!extension_loaded('memcached')) {
+ throw new \BadFunctionCallException('not support: memcached');
+ }
+
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+
+ $this->handler = new \Memcached;
+
+ if (!empty($this->options['option'])) {
+ $this->handler->setOptions($this->options['option']);
+ }
+
+ // 设置连接超时时间(单位:毫秒)
+ if ($this->options['timeout'] > 0) {
+ $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->options['timeout']);
+ }
+
+ // 支持集群
+ $hosts = (array) $this->options['host'];
+ $ports = (array) $this->options['port'];
+ if (empty($ports[0])) {
+ $ports[0] = 11211;
+ }
+
+ // 建立连接
+ $servers = [];
+ foreach ($hosts as $i => $host) {
+ $servers[] = [$host, $ports[$i] ?? $ports[0], 1];
+ }
+
+ $this->handler->addServers($servers);
+
+ if ('' != $this->options['username']) {
+ $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
+ $this->handler->setSaslAuthData($this->options['username'], $this->options['password']);
+ }
+ }
+
+ /**
+ * 判断缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ $key = $this->getCacheKey($name);
+
+ return $this->handler->get($key) ? true : false;
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null): mixed
+ {
+ $result = $this->handler->get($this->getCacheKey($name));
+ try {
+ return false !== $result ? $this->unserialize($result) : $this->getDefaultValue($name, $default);
+ } catch (InvalidCacheException $e) {
+ return $this->getDefaultValue($name, $default, true);
+ }
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param integer|DateInterval|DateTimeInterface $expire 有效时间(秒)
+ * @return bool
+ */
+ public function set($name, $value, $expire = null): bool
+ {
+ if (is_null($expire)) {
+ $expire = $this->options['expire'];
+ }
+
+ $key = $this->getCacheKey($name);
+ $expire = $this->getExpireTime($expire);
+ $value = $this->serialize($value);
+
+ if ($this->handler->set($key, $value, $expire)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc($name, $step = 1)
+ {
+ $key = $this->getCacheKey($name);
+
+ if ($this->handler->get($key)) {
+ return $this->handler->increment($key, $step);
+ }
+
+ return $this->handler->set($key, $step);
+ }
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec($name, $step = 1)
+ {
+ $key = $this->getCacheKey($name);
+ $value = $this->handler->get($key) - $step;
+ $res = $this->handler->set($key, $value);
+
+ return !$res ? false : $value;
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param bool|false $ttl
+ * @return bool
+ */
+ public function delete($name, $ttl = false): bool
+ {
+ $key = $this->getCacheKey($name);
+
+ return false === $ttl ?
+ $this->handler->delete($key) :
+ $this->handler->delete($key, $ttl);
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ return $this->handler->flush();
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag($keys): void
+ {
+ $this->handler->deleteMulti($keys);
+ }
+}
diff --git a/src/think/cache/driver/Redis.php b/src/think/cache/driver/Redis.php
new file mode 100644
index 0000000..fd0abd4
--- /dev/null
+++ b/src/think/cache/driver/Redis.php
@@ -0,0 +1,254 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use DateInterval;
+use DateTimeInterface;
+use think\cache\Driver;
+use think\exception\InvalidCacheException;
+
+class Redis extends Driver
+{
+ /** @var \Predis\Client|\Redis */
+ protected $handler;
+
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'host' => '127.0.0.1',
+ 'port' => 6379,
+ 'password' => '',
+ 'select' => 0,
+ 'timeout' => 0,
+ 'expire' => 0,
+ 'persistent' => false,
+ 'prefix' => '',
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ 'fail_delete' => false,
+ ];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $options 缓存参数
+ */
+ public function __construct(array $options = [])
+ {
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+ }
+
+ public function handler()
+ {
+ if (!$this->handler) {
+ if (extension_loaded('redis')) {
+ $this->handler = new \Redis;
+
+ if ($this->options['persistent']) {
+ $this->handler->pconnect($this->options['host'], (int) $this->options['port'], (int) $this->options['timeout'], 'persistent_id_' . $this->options['select']);
+ } else {
+ $this->handler->connect($this->options['host'], (int) $this->options['port'], (int) $this->options['timeout']);
+ }
+
+ if ('' != $this->options['password']) {
+ $this->handler->auth($this->options['password']);
+ }
+ } elseif (class_exists('\Predis\Client')) {
+ $params = [];
+ foreach ($this->options as $key => $val) {
+ if (in_array($key, ['aggregate', 'cluster', 'connections', 'exceptions', 'prefix', 'profile', 'replication', 'parameters'])) {
+ $params[$key] = $val;
+ unset($this->options[$key]);
+ }
+ }
+
+ if ('' == $this->options['password']) {
+ unset($this->options['password']);
+ }
+
+ $this->handler = new \Predis\Client($this->options, $params);
+
+ $this->options['prefix'] = '';
+ } else {
+ throw new \BadFunctionCallException('not support: redis');
+ }
+
+ if (0 != $this->options['select']) {
+ $this->handler->select((int) $this->options['select']);
+ }
+ }
+
+ return $this->handler;
+ }
+
+ /**
+ * 判断缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ return $this->handler()->exists($this->getCacheKey($name)) ? true : false;
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null): mixed
+ {
+ $key = $this->getCacheKey($name);
+ $value = $this->handler()->get($key);
+
+ if (false === $value || is_null($value)) {
+ return $this->getDefaultValue($name, $default);
+ }
+
+ try {
+ return $this->unserialize($value);
+ } catch (InvalidCacheException $e) {
+ return $this->getDefaultValue($name, $default, true);
+
+ }
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param integer|DateInterval|DateTimeInterface $expire 有效时间(秒)
+ * @return bool
+ */
+ public function set($name, $value, $expire = null): bool
+ {
+ if (is_null($expire)) {
+ $expire = $this->options['expire'];
+ }
+
+ $key = $this->getCacheKey($name);
+ $expire = $this->getExpireTime($expire);
+ $value = $this->serialize($value);
+
+ if ($expire) {
+ $this->handler()->setex($key, $expire, $value);
+ } else {
+ $this->handler()->set($key, $value);
+ }
+
+ return true;
+ }
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc($name, $step = 1)
+ {
+ $key = $this->getCacheKey($name);
+
+ return $this->handler()->incrby($key, $step);
+ }
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec($name, $step = 1)
+ {
+ $key = $this->getCacheKey($name);
+
+ return $this->handler()->decrby($key, $step);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function delete($name): bool
+ {
+ $key = $this->getCacheKey($name);
+ $result = $this->handler()->del($key);
+ return $result > 0;
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ $this->handler()->flushDB();
+ return true;
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag($keys): void
+ {
+ // 指定标签清除
+ $this->handler()->del($keys);
+ }
+
+ /**
+ * 追加TagSet数据
+ * @access public
+ * @param string $name 缓存标识
+ * @param mixed $value 数据
+ * @return void
+ */
+ public function append($name, $value): void
+ {
+ $key = $this->getCacheKey($name);
+ $this->handler()->sAdd($key, $value);
+ }
+
+ /**
+ * 获取标签包含的缓存标识
+ * @access public
+ * @param string $tag 缓存标签
+ * @return array
+ */
+ public function getTagItems(string $tag): array
+ {
+ $name = $this->getTagKey($tag);
+ $key = $this->getCacheKey($name);
+ return $this->handler()->sMembers($key);
+ }
+
+ public function __call($method, $args)
+ {
+ return call_user_func_array([$this->handler(), $method], $args);
+ }
+}
diff --git a/src/think/cache/driver/Wincache.php b/src/think/cache/driver/Wincache.php
new file mode 100644
index 0000000..255e006
--- /dev/null
+++ b/src/think/cache/driver/Wincache.php
@@ -0,0 +1,170 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\cache\driver;
+
+use DateInterval;
+use DateTimeInterface;
+use think\cache\Driver;
+use think\exception\InvalidCacheException;
+
+/**
+ * Wincache缓存驱动
+ */
+class Wincache extends Driver
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $options = [
+ 'prefix' => '',
+ 'expire' => 0,
+ 'tag_prefix' => 'tag:',
+ 'serialize' => [],
+ 'fail_delete' => false,
+ ];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $options 缓存参数
+ * @throws \BadFunctionCallException
+ */
+ public function __construct(array $options = [])
+ {
+ if (!function_exists('wincache_ucache_info')) {
+ throw new \BadFunctionCallException('not support: WinCache');
+ }
+
+ if (!empty($options)) {
+ $this->options = array_merge($this->options, $options);
+ }
+ }
+
+ /**
+ * 判断缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function has($name): bool
+ {
+ $this->readTimes++;
+
+ $key = $this->getCacheKey($name);
+
+ return wincache_ucache_exists($key);
+ }
+
+ /**
+ * 读取缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get($name, $default = null): mixed
+ {
+ $key = $this->getCacheKey($name);
+ try {
+ return wincache_ucache_exists($key) ? $this->unserialize(wincache_ucache_get($key)) : $this->getDefaultValue($name, $default);
+ } catch (InvalidCacheException $e) {
+ return $this->getDefaultValue($name, $default, true);
+ }
+ }
+
+ /**
+ * 写入缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param integer|DateInterval|DateTimeInterface $expire 有效时间(秒)
+ * @return bool
+ */
+ public function set($name, $value, $expire = null): bool
+ {
+ if (is_null($expire)) {
+ $expire = $this->options['expire'];
+ }
+
+ $key = $this->getCacheKey($name);
+ $expire = $this->getExpireTime($expire);
+ $value = $this->serialize($value);
+
+ if (wincache_ucache_set($key, $value, $expire)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc($name, $step = 1)
+ {
+ $key = $this->getCacheKey($name);
+
+ return wincache_ucache_inc($key, $step);
+ }
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @access public
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec($name, $step = 1)
+ {
+ $key = $this->getCacheKey($name);
+
+ return wincache_ucache_dec($key, $step);
+ }
+
+ /**
+ * 删除缓存
+ * @access public
+ * @param string $name 缓存变量名
+ * @return bool
+ */
+ public function delete($name): bool
+ {
+ return wincache_ucache_delete($this->getCacheKey($name));
+ }
+
+ /**
+ * 清除缓存
+ * @access public
+ * @return bool
+ */
+ public function clear(): bool
+ {
+ return wincache_ucache_clear();
+ }
+
+ /**
+ * 删除缓存标签
+ * @access public
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag($keys): void
+ {
+ wincache_ucache_delete($keys);
+ }
+}
diff --git a/src/think/console/Command.php b/src/think/console/Command.php
new file mode 100644
index 0000000..d4aa322
--- /dev/null
+++ b/src/think/console/Command.php
@@ -0,0 +1,504 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console;
+
+use Exception;
+use InvalidArgumentException;
+use LogicException;
+use think\App;
+use think\Console;
+use think\console\input\Argument;
+use think\console\input\Definition;
+use think\console\input\Option;
+
+abstract class Command
+{
+
+ /** @var Console */
+ private $console;
+ private $name;
+ private $processTitle;
+ private $aliases = [];
+ private $definition;
+ private $help;
+ private $description;
+ private $ignoreValidationErrors = false;
+ private $consoleDefinitionMerged = false;
+ private $consoleDefinitionMergedWithArgs = false;
+ private $synopsis = [];
+ private $usages = [];
+
+ /** @var Input */
+ protected $input;
+
+ /** @var Output */
+ protected $output;
+
+ /** @var App */
+ protected $app;
+
+ /**
+ * 构造方法
+ * @throws LogicException
+ * @api
+ */
+ public function __construct()
+ {
+ $this->definition = new Definition();
+
+ $this->configure();
+
+ if (!$this->name) {
+ throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this)));
+ }
+ }
+
+ /**
+ * 忽略验证错误
+ */
+ public function ignoreValidationErrors(): void
+ {
+ $this->ignoreValidationErrors = true;
+ }
+
+ /**
+ * 设置控制台
+ * @param Console $console
+ */
+ public function setConsole(?Console $console = null): void
+ {
+ $this->console = $console;
+ }
+
+ /**
+ * 获取控制台
+ * @return Console
+ * @api
+ */
+ public function getConsole(): Console
+ {
+ return $this->console;
+ }
+
+ /**
+ * 设置app
+ * @param App $app
+ */
+ public function setApp(App $app)
+ {
+ $this->app = $app;
+ }
+
+ /**
+ * 获取app
+ * @return App
+ */
+ public function getApp()
+ {
+ return $this->app;
+ }
+
+ /**
+ * 是否有效
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return true;
+ }
+
+ /**
+ * 配置指令
+ */
+ protected function configure()
+ {
+ }
+
+ /**
+ * 执行指令
+ * @param Input $input
+ * @param Output $output
+ * @return null|int
+ * @throws LogicException
+ * @see setCode()
+ */
+ protected function execute(Input $input, Output $output)
+ {
+ return $this->app->invoke([$this, 'handle']);
+ }
+
+ /**
+ * 用户验证
+ * @param Input $input
+ * @param Output $output
+ */
+ protected function interact(Input $input, Output $output)
+ {
+ }
+
+ /**
+ * 初始化
+ * @param Input $input An InputInterface instance
+ * @param Output $output An OutputInterface instance
+ */
+ protected function initialize(Input $input, Output $output)
+ {
+ }
+
+ /**
+ * 执行
+ * @param Input $input
+ * @param Output $output
+ * @return int
+ * @throws Exception
+ * @see setCode()
+ * @see execute()
+ */
+ public function run(Input $input, Output $output): int
+ {
+ $this->input = $input;
+ $this->output = $output;
+
+ $this->getSynopsis(true);
+ $this->getSynopsis(false);
+
+ $this->mergeConsoleDefinition();
+
+ try {
+ $input->bind($this->definition);
+ } catch (Exception $e) {
+ if (!$this->ignoreValidationErrors) {
+ throw $e;
+ }
+ }
+
+ $this->initialize($input, $output);
+
+ if (null !== $this->processTitle) {
+ if (function_exists('cli_set_process_title')) {
+ if (false === @cli_set_process_title($this->processTitle)) {
+ if ('Darwin' === PHP_OS) {
+ $output->writeln('Running "cli_get_process_title" as an unprivileged user is not supported on MacOS.');
+ } else {
+ $error = error_get_last();
+ trigger_error($error['message'], E_USER_WARNING);
+ }
+ }
+ } elseif (function_exists('setproctitle')) {
+ setproctitle($this->processTitle);
+ } elseif (Output::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
+ $output->writeln('Install the proctitle PECL to be able to change the process title.');
+ }
+ }
+
+ if ($input->isInteractive()) {
+ $this->interact($input, $output);
+ }
+
+ $input->validate();
+
+ $statusCode = $this->execute($input, $output);
+
+ return is_numeric($statusCode) ? (int) $statusCode : 0;
+ }
+
+ /**
+ * 合并参数定义
+ * @param bool $mergeArgs
+ */
+ public function mergeConsoleDefinition(bool $mergeArgs = true)
+ {
+ if (null === $this->console
+ || (true === $this->consoleDefinitionMerged
+ && ($this->consoleDefinitionMergedWithArgs || !$mergeArgs))
+ ) {
+ return;
+ }
+
+ if ($mergeArgs) {
+ $currentArguments = $this->definition->getArguments();
+ $this->definition->setArguments($this->console->getDefinition()->getArguments());
+ $this->definition->addArguments($currentArguments);
+ }
+
+ $this->definition->addOptions($this->console->getDefinition()->getOptions());
+
+ $this->consoleDefinitionMerged = true;
+ if ($mergeArgs) {
+ $this->consoleDefinitionMergedWithArgs = true;
+ }
+ }
+
+ /**
+ * 设置参数定义
+ * @param array|Definition $definition
+ * @return Command
+ * @api
+ */
+ public function setDefinition($definition)
+ {
+ if ($definition instanceof Definition) {
+ $this->definition = $definition;
+ } else {
+ $this->definition->setDefinition($definition);
+ }
+
+ $this->consoleDefinitionMerged = false;
+
+ return $this;
+ }
+
+ /**
+ * 获取参数定义
+ * @return Definition
+ * @api
+ */
+ public function getDefinition(): Definition
+ {
+ return $this->definition;
+ }
+
+ /**
+ * 获取当前指令的参数定义
+ * @return Definition
+ */
+ public function getNativeDefinition(): Definition
+ {
+ return $this->getDefinition();
+ }
+
+ /**
+ * 添加参数
+ * @param string $name 名称
+ * @param int $mode 类型
+ * @param string $description 描述
+ * @param mixed $default 默认值
+ * @return Command
+ */
+ public function addArgument(string $name, ?int $mode = null, string $description = '', $default = null)
+ {
+ $this->definition->addArgument(new Argument($name, $mode, $description, $default));
+
+ return $this;
+ }
+
+ /**
+ * 添加选项
+ * @param string $name 选项名称
+ * @param string $shortcut 别名
+ * @param int $mode 类型
+ * @param string $description 描述
+ * @param mixed $default 默认值
+ * @return Command
+ */
+ public function addOption(string $name, ?string $shortcut = null, ?int $mode = null, string $description = '', $default = null)
+ {
+ $this->definition->addOption(new Option($name, $shortcut, $mode, $description, $default));
+
+ return $this;
+ }
+
+ /**
+ * 设置指令名称
+ * @param string $name
+ * @return Command
+ * @throws InvalidArgumentException
+ */
+ public function setName(string $name)
+ {
+ $this->validateName($name);
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * 设置进程名称
+ *
+ * PHP 5.5+ or the proctitle PECL library is required
+ *
+ * @param string $title The process title
+ *
+ * @return $this
+ */
+ public function setProcessTitle($title)
+ {
+ $this->processTitle = $title;
+
+ return $this;
+ }
+
+ /**
+ * 获取指令名称
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name ?: '';
+ }
+
+ /**
+ * 设置描述
+ * @param string $description
+ * @return Command
+ */
+ public function setDescription(string $description)
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * 获取描述
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return $this->description ?: '';
+ }
+
+ /**
+ * 设置帮助信息
+ * @param string $help
+ * @return Command
+ */
+ public function setHelp(string $help)
+ {
+ $this->help = $help;
+
+ return $this;
+ }
+
+ /**
+ * 获取帮助信息
+ * @return string
+ */
+ public function getHelp(): string
+ {
+ return $this->help ?: '';
+ }
+
+ /**
+ * 描述信息
+ * @return string
+ */
+ public function getProcessedHelp(): string
+ {
+ $name = $this->name;
+
+ $placeholders = [
+ '%command.name%',
+ '%command.full_name%',
+ ];
+ $replacements = [
+ $name,
+ $_SERVER['PHP_SELF'] . ' ' . $name,
+ ];
+
+ return str_replace($placeholders, $replacements, $this->getHelp());
+ }
+
+ /**
+ * 设置别名
+ * @param string[] $aliases
+ * @return Command
+ * @throws InvalidArgumentException
+ */
+ public function setAliases(iterable $aliases)
+ {
+ foreach ($aliases as $alias) {
+ $this->validateName($alias);
+ }
+
+ $this->aliases = $aliases;
+
+ return $this;
+ }
+
+ /**
+ * 获取别名
+ * @return array
+ */
+ public function getAliases(): array
+ {
+ return $this->aliases;
+ }
+
+ /**
+ * 获取简介
+ * @param bool $short 是否简单的
+ * @return string
+ */
+ public function getSynopsis(bool $short = false): string
+ {
+ $key = $short ? 'short' : 'long';
+
+ if (!isset($this->synopsis[$key])) {
+ $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
+ }
+
+ return $this->synopsis[$key];
+ }
+
+ /**
+ * 添加用法介绍
+ * @param string $usage
+ * @return $this
+ */
+ public function addUsage(string $usage)
+ {
+ if (!str_starts_with($usage, $this->name)) {
+ $usage = sprintf('%s %s', $this->name, $usage);
+ }
+
+ $this->usages[] = $usage;
+
+ return $this;
+ }
+
+ /**
+ * 获取用法介绍
+ * @return array
+ */
+ public function getUsages(): array
+ {
+ return $this->usages;
+ }
+
+ /**
+ * 验证指令名称
+ * @param string $name
+ * @throws InvalidArgumentException
+ */
+ private function validateName(string $name)
+ {
+ if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
+ throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
+ }
+ }
+
+ /**
+ * 输出表格
+ * @param Table $table
+ * @return string
+ */
+ protected function table(Table $table): string
+ {
+ $content = $table->render();
+ $this->output->writeln($content);
+ return $content;
+ }
+
+}
diff --git a/src/think/console/Input.php b/src/think/console/Input.php
new file mode 100644
index 0000000..533cbf5
--- /dev/null
+++ b/src/think/console/Input.php
@@ -0,0 +1,465 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console;
+
+use think\console\input\Argument;
+use think\console\input\Definition;
+use think\console\input\Option;
+
+class Input
+{
+
+ /**
+ * @var Definition
+ */
+ protected $definition;
+
+ /**
+ * @var Option[]
+ */
+ protected $options = [];
+
+ /**
+ * @var Argument[]
+ */
+ protected $arguments = [];
+
+ protected $interactive = true;
+
+ private $tokens;
+ private $parsed;
+
+ public function __construct($argv = null)
+ {
+ if (null === $argv) {
+ $argv = $_SERVER['argv'];
+ // 去除命令名
+ array_shift($argv);
+ }
+
+ $this->tokens = $argv;
+
+ $this->definition = new Definition();
+ }
+
+ protected function setTokens(array $tokens)
+ {
+ $this->tokens = $tokens;
+ }
+
+ /**
+ * 绑定实例
+ * @param Definition $definition A InputDefinition instance
+ */
+ public function bind(Definition $definition): void
+ {
+ $this->arguments = [];
+ $this->options = [];
+ $this->definition = $definition;
+
+ $this->parse();
+ }
+
+ /**
+ * 解析参数
+ */
+ protected function parse(): void
+ {
+ $parseOptions = true;
+ $this->parsed = $this->tokens;
+ while (null !== $token = array_shift($this->parsed)) {
+ if ($parseOptions && '' == $token) {
+ $this->parseArgument($token);
+ } elseif ($parseOptions && '--' == $token) {
+ $parseOptions = false;
+ } elseif ($parseOptions && str_starts_with($token, '--')) {
+ $this->parseLongOption($token);
+ } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
+ $this->parseShortOption($token);
+ } else {
+ $this->parseArgument($token);
+ }
+ }
+ }
+
+ /**
+ * 解析短选项
+ * @param string $token 当前的指令.
+ */
+ private function parseShortOption(string $token): void
+ {
+ $name = substr($token, 1);
+
+ if (strlen($name) > 1) {
+ if ($this->definition->hasShortcut($name[0])
+ && $this->definition->getOptionForShortcut($name[0])->acceptValue()
+ ) {
+ $this->addShortOption($name[0], substr($name, 1));
+ } else {
+ $this->parseShortOptionSet($name);
+ }
+ } else {
+ $this->addShortOption($name, null);
+ }
+ }
+
+ /**
+ * 解析短选项
+ * @param string $name 当前指令
+ * @throws \RuntimeException
+ */
+ private function parseShortOptionSet(string $name): void
+ {
+ $len = strlen($name);
+ for ($i = 0; $i < $len; ++$i) {
+ if (!$this->definition->hasShortcut($name[$i])) {
+ throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i]));
+ }
+
+ $option = $this->definition->getOptionForShortcut($name[$i]);
+ if ($option->acceptValue()) {
+ $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
+
+ break;
+ } else {
+ $this->addLongOption($option->getName(), null);
+ }
+ }
+ }
+
+ /**
+ * 解析完整选项
+ * @param string $token 当前指令
+ */
+ private function parseLongOption(string $token): void
+ {
+ $name = substr($token, 2);
+
+ if (false !== $pos = strpos($name, '=')) {
+ $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1));
+ } else {
+ $this->addLongOption($name, null);
+ }
+ }
+
+ /**
+ * 解析参数
+ * @param string $token 当前指令
+ * @throws \RuntimeException
+ */
+ private function parseArgument(string $token): void
+ {
+ $c = count($this->arguments);
+
+ if ($this->definition->hasArgument($c)) {
+ $arg = $this->definition->getArgument($c);
+
+ $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token;
+
+ } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) {
+ $arg = $this->definition->getArgument($c - 1);
+
+ $this->arguments[$arg->getName()][] = $token;
+ } else {
+ throw new \RuntimeException('Too many arguments.');
+ }
+ }
+
+ /**
+ * 添加一个短选项的值
+ * @param string $shortcut 短名称
+ * @param mixed $value 值
+ * @throws \RuntimeException
+ */
+ private function addShortOption(string $shortcut, $value): void
+ {
+ if (!$this->definition->hasShortcut($shortcut)) {
+ throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
+ }
+
+ $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
+ }
+
+ /**
+ * 添加一个完整选项的值
+ * @param string $name 选项名
+ * @param mixed $value 值
+ * @throws \RuntimeException
+ */
+ private function addLongOption(string $name, $value): void
+ {
+ if (!$this->definition->hasOption($name)) {
+ throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name));
+ }
+
+ $option = $this->definition->getOption($name);
+
+ if (false === $value) {
+ $value = null;
+ }
+
+ if (null !== $value && !$option->acceptValue()) {
+ throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value));
+ }
+
+ if (null === $value && $option->acceptValue() && count($this->parsed)) {
+ $next = array_shift($this->parsed);
+ if (isset($next[0]) && '-' !== $next[0]) {
+ $value = $next;
+ } elseif (empty($next)) {
+ $value = '';
+ } else {
+ array_unshift($this->parsed, $next);
+ }
+ }
+
+ if (null === $value) {
+ if ($option->isValueRequired()) {
+ throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name));
+ }
+
+ if (!$option->isArray()) {
+ $value = $option->isValueOptional() ? $option->getDefault() : true;
+ }
+ }
+
+ if ($option->isArray()) {
+ $this->options[$name][] = $value;
+ } else {
+ $this->options[$name] = $value;
+ }
+ }
+
+ /**
+ * 获取第一个参数
+ * @return string|null
+ */
+ public function getFirstArgument()
+ {
+ foreach ($this->tokens as $token) {
+ if ($token && '-' === $token[0]) {
+ continue;
+ }
+
+ return $token;
+ }
+ return;
+ }
+
+ /**
+ * 检查原始参数是否包含某个值
+ * @param string|array $values 需要检查的值
+ * @return bool
+ */
+ public function hasParameterOption($values): bool
+ {
+ $values = (array) $values;
+
+ foreach ($this->tokens as $token) {
+ foreach ($values as $value) {
+ if ($token === $value || str_starts_with($token, $value . '=')) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取原始选项的值
+ * @param string|array $values 需要检查的值
+ * @param mixed $default 默认值
+ * @return mixed The option value
+ */
+ public function getParameterOption($values, $default = false)
+ {
+ $values = (array) $values;
+ $tokens = $this->tokens;
+
+ while (0 < count($tokens)) {
+ $token = array_shift($tokens);
+
+ foreach ($values as $value) {
+ if ($token === $value || str_starts_with($token, $value . '=')) {
+ if (false !== $pos = strpos($token, '=')) {
+ return substr($token, $pos + 1);
+ }
+
+ return array_shift($tokens);
+ }
+ }
+ }
+
+ return $default;
+ }
+
+ /**
+ * 验证输入
+ * @throws \RuntimeException
+ */
+ public function validate()
+ {
+ if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) {
+ throw new \RuntimeException('Not enough arguments.');
+ }
+ }
+
+ /**
+ * 检查输入是否是交互的
+ * @return bool
+ */
+ public function isInteractive(): bool
+ {
+ return $this->interactive;
+ }
+
+ /**
+ * 设置输入的交互
+ * @param bool
+ */
+ public function setInteractive(bool $interactive): void
+ {
+ $this->interactive = $interactive;
+ }
+
+ /**
+ * 获取所有的参数
+ * @return Argument[]
+ */
+ public function getArguments(): array
+ {
+ return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
+ }
+
+ /**
+ * 根据名称获取参数
+ * @param string $name 参数名
+ * @return mixed
+ * @throws \InvalidArgumentException
+ */
+ public function getArgument(string $name)
+ {
+ if (!$this->definition->hasArgument($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ }
+
+ return $this->arguments[$name] ?? $this->definition->getArgument($name)
+ ->getDefault();
+ }
+
+ /**
+ * 设置参数的值
+ * @param string $name 参数名
+ * @param string $value 值
+ * @throws \InvalidArgumentException
+ */
+ public function setArgument(string $name, $value)
+ {
+ if (!$this->definition->hasArgument($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ }
+
+ $this->arguments[$name] = $value;
+ }
+
+ /**
+ * 检查是否存在某个参数
+ * @param string|int $name 参数名或位置
+ * @return bool
+ */
+ public function hasArgument(string|int $name): bool
+ {
+ return $this->definition->hasArgument($name);
+ }
+
+ /**
+ * 获取所有的选项
+ * @return Option[]
+ */
+ public function getOptions(): array
+ {
+ return array_merge($this->definition->getOptionDefaults(), $this->options);
+ }
+
+ /**
+ * 获取选项值
+ * @param string $name 选项名称
+ * @return mixed
+ * @throws \InvalidArgumentException
+ */
+ public function getOption(string $name)
+ {
+ if (!$this->definition->hasOption($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+ }
+
+ return $this->options[$name] ?? $this->definition->getOption($name)->getDefault();
+ }
+
+ /**
+ * 设置选项值
+ * @param string $name 选项名
+ * @param string|bool $value 值
+ * @throws \InvalidArgumentException
+ */
+ public function setOption(string $name, $value): void
+ {
+ if (!$this->definition->hasOption($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+ }
+
+ $this->options[$name] = $value;
+ }
+
+ /**
+ * 是否有某个选项
+ * @param string $name 选项名
+ * @return bool
+ */
+ public function hasOption(string $name): bool
+ {
+ return $this->definition->hasOption($name) && isset($this->options[$name]);
+ }
+
+ /**
+ * 转义指令
+ * @param string $token
+ * @return string
+ */
+ public function escapeToken(string $token): string
+ {
+ return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token);
+ }
+
+ /**
+ * 返回传递给命令的参数的字符串
+ * @return string
+ */
+ public function __toString()
+ {
+ $tokens = array_map(function ($token) {
+ if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) {
+ return $match[1] . $this->escapeToken($match[2]);
+ }
+
+ if ($token && '-' !== $token[0]) {
+ return $this->escapeToken($token);
+ }
+
+ return $token;
+ }, $this->tokens);
+
+ return implode(' ', $tokens);
+ }
+}
diff --git a/src/think/console/LICENSE b/src/think/console/LICENSE
new file mode 100644
index 0000000..0abe056
--- /dev/null
+++ b/src/think/console/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2004-2016 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/src/think/console/Output.php b/src/think/console/Output.php
new file mode 100644
index 0000000..da41523
--- /dev/null
+++ b/src/think/console/Output.php
@@ -0,0 +1,231 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console;
+
+use Exception;
+use think\console\output\Ask;
+use think\console\output\Descriptor;
+use think\console\output\driver\Buffer;
+use think\console\output\driver\Console;
+use think\console\output\driver\Nothing;
+use think\console\output\Question;
+use think\console\output\question\Choice;
+use think\console\output\question\Confirmation;
+use Throwable;
+
+/**
+ * Class Output
+ * @package think\console
+ *
+ * @see \think\console\output\driver\Console::setDecorated
+ * @method void setDecorated($decorated)
+ *
+ * @see \think\console\output\driver\Buffer::fetch
+ * @method string fetch()
+ *
+ * @method void info($message)
+ * @method void error($message)
+ * @method void comment($message)
+ * @method void warning($message)
+ * @method void highlight($message)
+ * @method void question($message)
+ */
+class Output
+{
+ // 不显示信息(静默)
+ const VERBOSITY_QUIET = 0;
+ // 正常信息
+ const VERBOSITY_NORMAL = 1;
+ // 详细信息
+ const VERBOSITY_VERBOSE = 2;
+ // 非常详细的信息
+ const VERBOSITY_VERY_VERBOSE = 3;
+ // 调试信息
+ const VERBOSITY_DEBUG = 4;
+
+ const OUTPUT_NORMAL = 0;
+ const OUTPUT_RAW = 1;
+ const OUTPUT_PLAIN = 2;
+
+ // 输出信息级别
+ private $verbosity = self::VERBOSITY_NORMAL;
+
+ /** @var Buffer|Console|Nothing */
+ private $handle = null;
+
+ protected $styles = [
+ 'info',
+ 'error',
+ 'comment',
+ 'question',
+ 'highlight',
+ 'warning',
+ ];
+
+ public function __construct($driver = 'console')
+ {
+ $class = '\\think\\console\\output\\driver\\' . ucwords($driver);
+
+ $this->handle = new $class($this);
+ }
+
+ public function ask(Input $input, $question, $default = null, $validator = null)
+ {
+ $question = new Question($question, $default);
+ $question->setValidator($validator);
+
+ return $this->askQuestion($input, $question);
+ }
+
+ public function askHidden(Input $input, $question, $validator = null)
+ {
+ $question = new Question($question);
+
+ $question->setHidden(true);
+ $question->setValidator($validator);
+
+ return $this->askQuestion($input, $question);
+ }
+
+ public function confirm(Input $input, $question, $default = true)
+ {
+ return $this->askQuestion($input, new Confirmation($question, $default));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function choice(Input $input, $question, array $choices, $default = null)
+ {
+ if (null !== $default) {
+ $values = array_flip($choices);
+ $default = $values[$default];
+ }
+
+ return $this->askQuestion($input, new Choice($question, $choices, $default));
+ }
+
+ protected function askQuestion(Input $input, Question $question)
+ {
+ $ask = new Ask($input, $this, $question);
+ $answer = $ask->run();
+
+ if ($input->isInteractive()) {
+ $this->newLine();
+ }
+
+ return $answer;
+ }
+
+ protected function block(string $style, string $message): void
+ {
+ $this->writeln("<{$style}>{$message}$style>");
+ }
+
+ /**
+ * 输出空行
+ * @param int $count
+ */
+ public function newLine(int $count = 1): void
+ {
+ $this->write(str_repeat(PHP_EOL, $count));
+ }
+
+ /**
+ * 输出信息并换行
+ * @param string $messages
+ * @param int $type
+ */
+ public function writeln(string $messages, int $type = 0): void
+ {
+ $this->write($messages, true, $type);
+ }
+
+ /**
+ * 输出信息
+ * @param string $messages
+ * @param bool $newline
+ * @param int $type
+ */
+ public function write(string $messages, bool $newline = false, int $type = 0): void
+ {
+ $this->handle->write($messages, $newline, $type);
+ }
+
+ public function renderException(Throwable $e): void
+ {
+ $this->handle->renderException($e);
+ }
+
+ /**
+ * 设置输出信息级别
+ * @param int $level 输出信息级别
+ */
+ public function setVerbosity(int $level)
+ {
+ $this->verbosity = $level;
+ }
+
+ /**
+ * 获取输出信息级别
+ * @return int
+ */
+ public function getVerbosity(): int
+ {
+ return $this->verbosity;
+ }
+
+ public function isQuiet(): bool
+ {
+ return self::VERBOSITY_QUIET === $this->verbosity;
+ }
+
+ public function isVerbose(): bool
+ {
+ return self::VERBOSITY_VERBOSE <= $this->verbosity;
+ }
+
+ public function isVeryVerbose(): bool
+ {
+ return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity;
+ }
+
+ public function isDebug(): bool
+ {
+ return self::VERBOSITY_DEBUG <= $this->verbosity;
+ }
+
+ public function describe($object, array $options = []): void
+ {
+ $descriptor = new Descriptor();
+ $options = array_merge([
+ 'raw_text' => false,
+ ], $options);
+
+ $descriptor->describe($this, $object, $options);
+ }
+
+ public function __call($method, $args)
+ {
+ if (in_array($method, $this->styles)) {
+ array_unshift($args, $method);
+ return call_user_func_array([$this, 'block'], $args);
+ }
+
+ if ($this->handle && method_exists($this->handle, $method)) {
+ return call_user_func_array([$this->handle, $method], $args);
+ } else {
+ throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
+ }
+ }
+}
diff --git a/src/think/console/Table.php b/src/think/console/Table.php
new file mode 100644
index 0000000..aa86fb4
--- /dev/null
+++ b/src/think/console/Table.php
@@ -0,0 +1,300 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console;
+
+class Table
+{
+ const ALIGN_LEFT = 1;
+ const ALIGN_RIGHT = 0;
+ const ALIGN_CENTER = 2;
+
+ /**
+ * 头信息数据
+ * @var array
+ */
+ protected $header = [];
+
+ /**
+ * 头部对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @var int
+ */
+ protected $headerAlign = 1;
+
+ /**
+ * 表格数据(二维数组)
+ * @var array
+ */
+ protected $rows = [];
+
+ /**
+ * 单元格对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @var int
+ */
+ protected $cellAlign = 1;
+
+ /**
+ * 单元格宽度信息
+ * @var array
+ */
+ protected $colWidth = [];
+
+ /**
+ * 表格输出样式
+ * @var string
+ */
+ protected $style = 'default';
+
+ /**
+ * 表格样式定义
+ * @var array
+ */
+ protected $format = [
+ 'compact' => [],
+ 'default' => [
+ 'top' => ['+', '-', '+', '+'],
+ 'cell' => ['|', ' ', '|', '|'],
+ 'middle' => ['+', '-', '+', '+'],
+ 'bottom' => ['+', '-', '+', '+'],
+ 'cross-top' => ['+', '-', '-', '+'],
+ 'cross-bottom' => ['+', '-', '-', '+'],
+ ],
+ 'markdown' => [
+ 'top' => [' ', ' ', ' ', ' '],
+ 'cell' => ['|', ' ', '|', '|'],
+ 'middle' => ['|', '-', '|', '|'],
+ 'bottom' => [' ', ' ', ' ', ' '],
+ 'cross-top' => ['|', ' ', ' ', '|'],
+ 'cross-bottom' => ['|', ' ', ' ', '|'],
+ ],
+ 'borderless' => [
+ 'top' => ['=', '=', ' ', '='],
+ 'cell' => [' ', ' ', ' ', ' '],
+ 'middle' => ['=', '=', ' ', '='],
+ 'bottom' => ['=', '=', ' ', '='],
+ 'cross-top' => ['=', '=', ' ', '='],
+ 'cross-bottom' => ['=', '=', ' ', '='],
+ ],
+ 'box' => [
+ 'top' => ['┌', '─', '┬', '┐'],
+ 'cell' => ['│', ' ', '│', '│'],
+ 'middle' => ['├', '─', '┼', '┤'],
+ 'bottom' => ['└', '─', '┴', '┘'],
+ 'cross-top' => ['├', '─', '┴', '┤'],
+ 'cross-bottom' => ['├', '─', '┬', '┤'],
+ ],
+ 'box-double' => [
+ 'top' => ['╔', '═', '╤', '╗'],
+ 'cell' => ['║', ' ', '│', '║'],
+ 'middle' => ['╠', '─', '╪', '╣'],
+ 'bottom' => ['╚', '═', '╧', '╝'],
+ 'cross-top' => ['╠', '═', '╧', '╣'],
+ 'cross-bottom' => ['╠', '═', '╤', '╣'],
+ ],
+ ];
+
+ /**
+ * 设置表格头信息 以及对齐方式
+ * @access public
+ * @param array $header 要输出的Header信息
+ * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @return void
+ */
+ public function setHeader(array $header, int $align = 1): void
+ {
+ $this->header = $header;
+ $this->headerAlign = $align;
+
+ $this->checkColWidth($header);
+ }
+
+ /**
+ * 设置输出表格数据 及对齐方式
+ * @access public
+ * @param array $rows 要输出的表格数据(二维数组)
+ * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @return void
+ */
+ public function setRows(array $rows, int $align = 1): void
+ {
+ $this->rows = $rows;
+ $this->cellAlign = $align;
+
+ foreach ($rows as $row) {
+ $this->checkColWidth($row);
+ }
+ }
+
+ /**
+ * 设置全局单元格对齐方式
+ * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
+ * @return $this
+ */
+ public function setCellAlign(int $align = 1)
+ {
+ $this->cellAlign = $align;
+ return $this;
+ }
+
+ /**
+ * 检查列数据的显示宽度
+ * @access public
+ * @param mixed $row 行数据
+ * @return void
+ */
+ protected function checkColWidth($row): void
+ {
+ if (is_array($row)) {
+ foreach ($row as $key => $cell) {
+ $width = mb_strwidth((string) $cell);
+ if (!isset($this->colWidth[$key]) || $width > $this->colWidth[$key]) {
+ $this->colWidth[$key] = $width;
+ }
+ }
+ }
+ }
+
+ /**
+ * 增加一行表格数据
+ * @access public
+ * @param mixed $row 行数据
+ * @param bool $first 是否在开头插入
+ * @return void
+ */
+ public function addRow($row, bool $first = false): void
+ {
+ if ($first) {
+ array_unshift($this->rows, $row);
+ } else {
+ $this->rows[] = $row;
+ }
+
+ $this->checkColWidth($row);
+ }
+
+ /**
+ * 设置输出表格的样式
+ * @access public
+ * @param string $style 样式名
+ * @return void
+ */
+ public function setStyle(string $style): void
+ {
+ $this->style = isset($this->format[$style]) ? $style : 'default';
+ }
+
+ /**
+ * 输出分隔行
+ * @access public
+ * @param string $pos 位置
+ * @return string
+ */
+ protected function renderSeparator(string $pos): string
+ {
+ $style = $this->getStyle($pos);
+ $array = [];
+
+ foreach ($this->colWidth as $width) {
+ $array[] = str_repeat($style[1], $width + 2);
+ }
+
+ return $style[0] . implode($style[2], $array) . $style[3] . PHP_EOL;
+ }
+
+ /**
+ * 输出表格头部
+ * @access public
+ * @return string
+ */
+ protected function renderHeader(): string
+ {
+ $style = $this->getStyle('cell');
+ $content = $this->renderSeparator('top');
+
+ foreach ($this->header as $key => $header) {
+ $array[] = ' ' . str_pad($header, $this->colWidth[$key], $style[1], $this->headerAlign);
+ }
+
+ if (!empty($array)) {
+ $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL;
+
+ if (!empty($this->rows)) {
+ $content .= $this->renderSeparator('middle');
+ }
+ }
+
+ return $content;
+ }
+
+ protected function getStyle(string $style): array
+ {
+ if ($this->format[$this->style]) {
+ $style = $this->format[$this->style][$style];
+ } else {
+ $style = [' ', ' ', ' ', ' '];
+ }
+
+ return $style;
+ }
+
+ /**
+ * 输出表格
+ * @access public
+ * @param array $dataList 表格数据
+ * @return string
+ */
+ public function render(array $dataList = []): string
+ {
+ if (!empty($dataList)) {
+ $this->setRows($dataList);
+ }
+
+ // 输出头部
+ $content = $this->renderHeader();
+ $style = $this->getStyle('cell');
+
+ if (!empty($this->rows)) {
+ foreach ($this->rows as $row) {
+ if (is_string($row) && '-' === $row) {
+ $content .= $this->renderSeparator('middle');
+ } elseif (is_scalar($row)) {
+ $content .= $this->renderSeparator('cross-top');
+ $width = 3 * (count($this->colWidth) - 1) + array_reduce($this->colWidth, function ($a, $b) {
+ return $a + $b;
+ });
+ $array = str_pad($row, $width);
+
+ $content .= $style[0] . ' ' . $array . ' ' . $style[3] . PHP_EOL;
+ $content .= $this->renderSeparator('cross-bottom');
+ } else {
+ $array = [];
+
+ foreach ($row as $key => $val) {
+ $width = $this->colWidth[$key];
+ // form https://github.com/symfony/console/blob/20c9821c8d1c2189f287dcee709b2f86353ea08f/Helper/Table.php#L467
+ // str_pad won't work properly with multi-byte strings, we need to fix the padding
+ if (false !== $encoding = mb_detect_encoding((string) $val, null, true)) {
+ $width += strlen((string) $val) - mb_strwidth((string) $val, $encoding);
+ }
+ $array[] = ' ' . str_pad((string) $val, $width, ' ', $this->cellAlign);
+ }
+
+ $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL;
+ }
+ }
+ }
+
+ $content .= $this->renderSeparator('bottom');
+
+ return $content;
+ }
+}
diff --git a/src/think/console/bin/README.md b/src/think/console/bin/README.md
new file mode 100644
index 0000000..9acc52f
--- /dev/null
+++ b/src/think/console/bin/README.md
@@ -0,0 +1 @@
+console 工具使用 hiddeninput.exe 在 windows 上隐藏密码输入,该二进制文件由第三方提供,相关源码和其他细节可以在 [Hidden Input](https://github.com/Seldaek/hidden-input) 找到。
diff --git a/src/think/console/bin/hiddeninput.exe b/src/think/console/bin/hiddeninput.exe
new file mode 100644
index 0000000..c8cf65e
Binary files /dev/null and b/src/think/console/bin/hiddeninput.exe differ
diff --git a/src/think/console/command/Clear.php b/src/think/console/command/Clear.php
new file mode 100644
index 0000000..a359a97
--- /dev/null
+++ b/src/think/console/command/Clear.php
@@ -0,0 +1,85 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+
+class Clear extends Command
+{
+ protected function configure()
+ {
+ // 指令配置
+ $this->setName('clear')
+ ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null)
+ ->addOption('cache', 'c', Option::VALUE_NONE, 'clear cache file')
+ ->addOption('log', 'l', Option::VALUE_NONE, 'clear log file')
+ ->addOption('dir', 'r', Option::VALUE_NONE, 'clear empty dir')
+ ->addOption('expire', 'e', Option::VALUE_NONE, 'clear cache file if cache has expired')
+ ->setDescription('Clear runtime file');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $runtimePath = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR;
+
+ if ($input->getOption('cache')) {
+ $path = $runtimePath . 'cache';
+ } elseif ($input->getOption('log')) {
+ $path = $runtimePath . 'log';
+ } else {
+ $path = $input->getOption('path') ?: $runtimePath;
+ }
+
+ $rmdir = $input->getOption('dir') ? true : false;
+ // --expire 仅当 --cache 时生效
+ $cache_expire = $input->getOption('expire') && $input->getOption('cache') ? true : false;
+ $this->clear(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $rmdir, $cache_expire);
+
+ $output->writeln("Clear Successed");
+ }
+
+ protected function clear(string $path, bool $rmdir, bool $cache_expire): void
+ {
+ $files = is_dir($path) ? scandir($path) : [];
+
+ foreach ($files as $file) {
+ if ('.' != $file && '..' != $file && is_dir($path . $file)) {
+ $this->clear($path . $file . DIRECTORY_SEPARATOR, $rmdir, $cache_expire);
+ if ($rmdir) {
+ @rmdir($path . $file);
+ }
+ } elseif ('.gitignore' != $file && is_file($path . $file)) {
+ if ($cache_expire) {
+ if ($this->cacheHasExpired($path . $file)) {
+ unlink($path . $file);
+ }
+ } else {
+ unlink($path . $file);
+ }
+ }
+ }
+ }
+
+ /**
+ * 缓存文件是否已过期
+ * @param $filename string 文件路径
+ * @return bool
+ */
+ protected function cacheHasExpired($filename) {
+ $content = file_get_contents($filename);
+ $expire = (int) substr($content, 8, 12);
+ return 0 != $expire && time() - $expire > filemtime($filename);
+ }
+
+}
diff --git a/src/think/console/command/Help.php b/src/think/console/command/Help.php
new file mode 100644
index 0000000..097a98f
--- /dev/null
+++ b/src/think/console/command/Help.php
@@ -0,0 +1,70 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+
+class Help extends Command
+{
+
+ private $command;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->ignoreValidationErrors();
+
+ $this->setName('help')->setDefinition([
+ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'),
+ new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
+ ])->setDescription('Displays help for a command')->setHelp(
+ <<%command.name% command displays help for a given command:
+
+ php %command.full_name% list
+
+To display the list of available commands, please use the list command.
+EOF
+ );
+ }
+
+ /**
+ * Sets the command.
+ * @param Command $command The command to set
+ */
+ public function setCommand(Command $command): void
+ {
+ $this->command = $command;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(Input $input, Output $output)
+ {
+ if (null === $this->command) {
+ $this->command = $this->getConsole()->find($input->getArgument('command_name'));
+ }
+
+ $output->describe($this->command, [
+ 'raw_text' => $input->getOption('raw'),
+ ]);
+
+ $this->command = null;
+ }
+}
diff --git a/src/think/console/command/Lists.php b/src/think/console/command/Lists.php
new file mode 100644
index 0000000..b8368e9
--- /dev/null
+++ b/src/think/console/command/Lists.php
@@ -0,0 +1,74 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Definition as InputDefinition;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+
+class Lists extends Command
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp(
+ <<%command.name% command lists all commands:
+
+ php %command.full_name%
+
+You can also display the commands for a specific namespace:
+
+ php %command.full_name% test
+
+It's also possible to get raw list of commands (useful for embedding command runner):
+
+ php %command.full_name% --raw
+EOF
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNativeDefinition(): InputDefinition
+ {
+ return $this->createDefinition();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(Input $input, Output $output)
+ {
+ $output->describe($this->getConsole(), [
+ 'raw_text' => $input->getOption('raw'),
+ 'namespace' => $input->getArgument('namespace'),
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private function createDefinition(): InputDefinition
+ {
+ return new InputDefinition([
+ new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
+ new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
+ ]);
+ }
+}
diff --git a/src/think/console/command/Make.php b/src/think/console/command/Make.php
new file mode 100644
index 0000000..ac2453d
--- /dev/null
+++ b/src/think/console/command/Make.php
@@ -0,0 +1,99 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+
+abstract class Make extends Command
+{
+ protected $type;
+
+ abstract protected function getStub();
+
+ protected function configure()
+ {
+ $this->addArgument('name', Argument::REQUIRED, "The name of the class");
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $name = trim($input->getArgument('name'));
+
+ $classname = $this->getClassName($name);
+
+ $pathname = $this->getPathName($classname);
+
+ if (is_file($pathname)) {
+ $output->writeln('' . $this->type . ':' . $classname . ' already exists!');
+ return false;
+ }
+
+ if (!is_dir(dirname($pathname))) {
+ mkdir(dirname($pathname), 0755, true);
+ }
+
+ file_put_contents($pathname, $this->buildClass($classname));
+
+ $output->writeln('' . $this->type . ':' . $classname . ' created successfully.');
+ }
+
+ protected function buildClass(string $name)
+ {
+ $stub = file_get_contents($this->getStub());
+
+ $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
+
+ $class = str_replace($namespace . '\\', '', $name);
+
+ return str_replace(['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'], [
+ $class,
+ $this->app->config->get('route.action_suffix'),
+ $namespace,
+ $this->app->getNamespace(),
+ ], $stub);
+ }
+
+ protected function getPathName(string $name): string
+ {
+ $name = substr($name, 4);
+
+ return $this->app->getBasePath() . ltrim(str_replace('\\', '/', $name), '/') . '.php';
+ }
+
+ protected function getClassName(string $name): string
+ {
+ if (str_contains($name, '\\')) {
+ return $name;
+ }
+
+ if (str_contains($name, '@')) {
+ [$app, $name] = explode('@', $name);
+ } else {
+ $app = '';
+ }
+
+ if (str_contains($name, '/')) {
+ $name = str_replace('/', '\\', $name);
+ }
+
+ return $this->getNamespace($app) . '\\' . $name;
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return 'app' . ($app ? '\\' . $app : '');
+ }
+
+}
diff --git a/src/think/console/command/RouteList.php b/src/think/console/command/RouteList.php
new file mode 100644
index 0000000..4c5c41a
--- /dev/null
+++ b/src/think/console/command/RouteList.php
@@ -0,0 +1,141 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command;
+
+use DirectoryIterator;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+use think\console\Table;
+use think\event\RouteLoaded;
+
+class RouteList extends Command
+{
+ protected $sortBy = [
+ 'rule' => 0,
+ 'route' => 1,
+ 'method' => 2,
+ 'name' => 3,
+ 'domain' => 4,
+ ];
+
+ protected function configure()
+ {
+ $this->setName('route:list')
+ ->addArgument('style', Argument::OPTIONAL, "the style of the table.", 'default')
+ ->addOption('sort', 's', Option::VALUE_OPTIONAL, 'order by rule name.', 0)
+ ->addOption('more', 'm', Option::VALUE_NONE, 'show route options.')
+ ->setDescription('show route list.');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $filename = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . 'route_list.php';
+
+ if (is_file($filename)) {
+ unlink($filename);
+ } elseif (!is_dir(dirname($filename))) {
+ mkdir(dirname($filename), 0755);
+ }
+
+ $content = $this->getRouteList();
+ file_put_contents($filename, 'Route List' . PHP_EOL . $content);
+ }
+
+ protected function scanRoute($path, $root, $autoGroup)
+ {
+ $iterator = new DirectoryIterator($path);
+ foreach ($iterator as $fileinfo) {
+ if ($fileinfo->isDot()) {
+ continue;
+ }
+
+ if ($fileinfo->getType() == 'file' && $fileinfo->getExtension() == 'php') {
+ $groupName = str_replace('\\', '/', substr_replace($fileinfo->getPath(), '', 0, strlen($root)));
+ if ($groupName) {
+ $this->app->route->group($groupName, function() use ($fileinfo) {
+ include $fileinfo->getRealPath();
+ });
+ } else {
+ include $fileinfo->getRealPath();
+ }
+ } elseif ($autoGroup && $fileinfo->isDir()) {
+ $this->scanRoute($fileinfo->getPathname(), $root, $autoGroup);
+ }
+ }
+ }
+
+ protected function getRouteList(?string $dir = null): string
+ {
+ $this->app->route->clear();
+ $this->app->route->lazy(false);
+ $autoGroup = $this->app->route->config('route_auto_group');
+ $path = $this->app->getRootPath() . 'route' . DIRECTORY_SEPARATOR;
+
+ $this->scanRoute($path, $path, $autoGroup);
+
+ //触发路由载入完成事件
+ $this->app->event->trigger(RouteLoaded::class);
+
+ $table = new Table();
+
+ if ($this->input->hasOption('more')) {
+ $header = ['Rule', 'Route', 'Method', 'Name', 'Domain', 'Option', 'Pattern'];
+ } else {
+ $header = ['Rule', 'Route', 'Method', 'Name'];
+ }
+
+ $table->setHeader($header);
+
+ $routeList = $this->app->route->getRuleList();
+ $rows = [];
+
+ foreach ($routeList as $item) {
+ if (is_array($item['route'])) {
+ $item['route'] = '[' . $item['route'][0] .' , ' . $item['route'][1] . ']';
+ } else {
+ $item['route'] = $item['route'] instanceof \Closure ? '' : $item['route'];
+ }
+ $row = [$item['rule'], $item['route'], $item['method'], $item['name']];
+
+ if ($this->input->hasOption('more')) {
+ array_push($row, $item['domain'], json_encode($item['option']), json_encode($item['pattern']));
+ }
+ $rows[] = $row;
+ }
+
+ if ($this->input->getOption('sort')) {
+ $sort = strtolower($this->input->getOption('sort'));
+
+ if (isset($this->sortBy[$sort])) {
+ $sort = $this->sortBy[$sort];
+ }
+
+ uasort($rows, function ($a, $b) use ($sort) {
+ $itemA = $a[$sort] ?? null;
+ $itemB = $b[$sort] ?? null;
+ return strcasecmp($itemA, $itemB);
+ });
+ }
+
+ $table->setRows($rows);
+
+ if ($this->input->getArgument('style')) {
+ $style = $this->input->getArgument('style');
+ $table->setStyle($style);
+ }
+
+ return $this->table($table);
+ }
+
+}
diff --git a/src/think/console/command/RunServer.php b/src/think/console/command/RunServer.php
new file mode 100644
index 0000000..92bdd3d
--- /dev/null
+++ b/src/think/console/command/RunServer.php
@@ -0,0 +1,72 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+
+class RunServer extends Command
+{
+ public function configure()
+ {
+ $this->setName('run')
+ ->addOption(
+ 'host',
+ 'H',
+ Option::VALUE_OPTIONAL,
+ 'The host to server the application on',
+ '0.0.0.0'
+ )
+ ->addOption(
+ 'port',
+ 'p',
+ Option::VALUE_OPTIONAL,
+ 'The port to server the application on',
+ 8000
+ )
+ ->addOption(
+ 'root',
+ 'r',
+ Option::VALUE_OPTIONAL,
+ 'The document root of the application',
+ ''
+ )
+ ->setDescription('PHP Built-in Server for ThinkPHP');
+ }
+
+ public function execute(Input $input, Output $output)
+ {
+ $host = $input->getOption('host');
+ $port = $input->getOption('port');
+ $root = $input->getOption('root');
+ if (empty($root)) {
+ $root = $this->app->getRootPath() . 'public';
+ }
+
+ $command = sprintf(
+ '"%s" -S %s:%d -t %s %s',
+ PHP_BINARY,
+ $host,
+ $port,
+ escapeshellarg($root),
+ escapeshellarg($root . DIRECTORY_SEPARATOR . 'router.php')
+ );
+
+ $output->writeln(sprintf('ThinkPHP Development server is started On ', $host, $port));
+ $output->writeln(sprintf('You can exit with `CTRL-C`'));
+ $output->writeln(sprintf('Document root is: %s', $root));
+ passthru($command);
+ }
+}
diff --git a/src/think/console/command/ServiceDiscover.php b/src/think/console/command/ServiceDiscover.php
new file mode 100644
index 0000000..05cff55
--- /dev/null
+++ b/src/think/console/command/ServiceDiscover.php
@@ -0,0 +1,52 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+
+class ServiceDiscover extends Command
+{
+ public function configure()
+ {
+ $this->setName('service:discover')
+ ->setDescription('Discover Services for ThinkPHP');
+ }
+
+ public function execute(Input $input, Output $output)
+ {
+ if (is_file($path = $this->app->getRootPath() . 'vendor/composer/installed.json')) {
+ $packages = json_decode(@file_get_contents($path), true);
+ // Compatibility with Composer 2.0
+ if (isset($packages['packages'])) {
+ $packages = $packages['packages'];
+ }
+
+ $services = [];
+ foreach ($packages as $package) {
+ if (!empty($package['extra']['think']['services'])) {
+ $services = array_merge($services, (array) $package['extra']['think']['services']);
+ }
+ }
+
+ $header = '// This file is automatically generated at:' . date('Y-m-d H:i:s') . PHP_EOL . 'declare (strict_types = 1);' . PHP_EOL;
+
+ $content = 'app->getRootPath() . 'vendor/services.php', $content);
+
+ $output->writeln('Succeed!');
+ }
+ }
+}
diff --git a/src/think/console/command/VendorPublish.php b/src/think/console/command/VendorPublish.php
new file mode 100644
index 0000000..49cec9c
--- /dev/null
+++ b/src/think/console/command/VendorPublish.php
@@ -0,0 +1,69 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\input\Option;
+
+class VendorPublish extends Command
+{
+ public function configure()
+ {
+ $this->setName('vendor:publish')
+ ->addOption('force', 'f', Option::VALUE_NONE, 'Overwrite any existing files')
+ ->setDescription('Publish any publishable assets from vendor packages');
+ }
+
+ public function handle()
+ {
+
+ $force = $this->input->getOption('force');
+
+ if (is_file($path = $this->app->getRootPath() . 'vendor/composer/installed.json')) {
+ $packages = json_decode(@file_get_contents($path), true);
+ // Compatibility with Composer 2.0
+ if (isset($packages['packages'])) {
+ $packages = $packages['packages'];
+ }
+ foreach ($packages as $package) {
+ //配置
+ $configDir = $this->app->getConfigPath();
+
+ if (!empty($package['extra']['think']['config'])) {
+
+ $installPath = $this->app->getRootPath() . 'vendor/' . $package['name'] . DIRECTORY_SEPARATOR;
+
+ foreach ((array) $package['extra']['think']['config'] as $name => $file) {
+
+ $target = $configDir . $name . '.php';
+ $source = $installPath . $file;
+
+ if (is_file($target) && !$force) {
+ $this->output->info("File {$target} exist!");
+ continue;
+ }
+
+ if (!is_file($source)) {
+ $this->output->info("File {$source} not exist!");
+ continue;
+ }
+
+ copy($source, $target);
+ }
+ }
+ }
+
+ $this->output->writeln('Succeed!');
+ }
+ }
+}
diff --git a/src/think/console/command/Version.php b/src/think/console/command/Version.php
new file mode 100644
index 0000000..bae3472
--- /dev/null
+++ b/src/think/console/command/Version.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\console\command;
+
+use Composer\InstalledVersions;
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+
+class Version extends Command
+{
+ protected function configure()
+ {
+ // 指令配置
+ $this->setName('version')
+ ->setDescription('show thinkphp framework version');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $version = InstalledVersions::getPrettyVersion('topthink/framework');
+ $output->writeln($version);
+ }
+
+}
diff --git a/src/think/console/command/make/Command.php b/src/think/console/command/make/Command.php
new file mode 100644
index 0000000..c38c482
--- /dev/null
+++ b/src/think/console/command/make/Command.php
@@ -0,0 +1,54 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+use think\console\input\Argument;
+
+class Command extends Make
+{
+ protected $type = "Command";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:command')
+ ->addArgument('commandName', Argument::OPTIONAL, "The name of the command")
+ ->setDescription('Create a new command class');
+ }
+
+ protected function buildClass(string $name): string
+ {
+ $commandName = $this->input->getArgument('commandName') ?: strtolower(basename($name));
+ $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
+
+ $class = str_replace($namespace . '\\', '', $name);
+ $stub = file_get_contents($this->getStub());
+
+ return str_replace(['{%commandName%}', '{%className%}', '{%namespace%}', '{%app_namespace%}'], [
+ $commandName,
+ $class,
+ $namespace,
+ $this->app->getNamespace(),
+ ], $stub);
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'command.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\command';
+ }
+}
diff --git a/src/think/console/command/make/Controller.php b/src/think/console/command/make/Controller.php
new file mode 100644
index 0000000..45157f2
--- /dev/null
+++ b/src/think/console/command/make/Controller.php
@@ -0,0 +1,55 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+use think\console\input\Option;
+
+class Controller extends Make
+{
+
+ protected $type = "Controller";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:controller')
+ ->addOption('api', null, Option::VALUE_NONE, 'Generate an api controller class.')
+ ->addOption('plain', null, Option::VALUE_NONE, 'Generate an empty controller class.')
+ ->setDescription('Create a new resource controller class');
+ }
+
+ protected function getStub(): string
+ {
+ $stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR;
+
+ if ($this->input->getOption('api')) {
+ return $stubPath . 'controller.api.stub';
+ }
+
+ if ($this->input->getOption('plain')) {
+ return $stubPath . 'controller.plain.stub';
+ }
+
+ return $stubPath . 'controller.stub';
+ }
+
+ protected function getClassName(string $name): string
+ {
+ return parent::getClassName($name) . ($this->app->config->get('route.controller_suffix') ? 'Controller' : '');
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\controller';
+ }
+}
diff --git a/src/think/console/command/make/Event.php b/src/think/console/command/make/Event.php
new file mode 100644
index 0000000..efc5bb1
--- /dev/null
+++ b/src/think/console/command/make/Event.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Event extends Make
+{
+ protected $type = "Event";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:event')
+ ->setDescription('Create a new event class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'event.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\event';
+ }
+}
diff --git a/src/think/console/command/make/Listener.php b/src/think/console/command/make/Listener.php
new file mode 100644
index 0000000..f5d3185
--- /dev/null
+++ b/src/think/console/command/make/Listener.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Listener extends Make
+{
+ protected $type = "Listener";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:listener')
+ ->setDescription('Create a new listener class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'listener.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\listener';
+ }
+}
diff --git a/src/think/console/command/make/Middleware.php b/src/think/console/command/make/Middleware.php
new file mode 100644
index 0000000..523f186
--- /dev/null
+++ b/src/think/console/command/make/Middleware.php
@@ -0,0 +1,36 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Middleware extends Make
+{
+ protected $type = "Middleware";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:middleware')
+ ->setDescription('Create a new middleware class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'middleware.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\middleware';
+ }
+}
diff --git a/src/think/console/command/make/Model.php b/src/think/console/command/make/Model.php
new file mode 100644
index 0000000..00b4621
--- /dev/null
+++ b/src/think/console/command/make/Model.php
@@ -0,0 +1,36 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Model extends Make
+{
+ protected $type = "Model";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:model')
+ ->setDescription('Create a new model class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'model.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\model';
+ }
+}
diff --git a/src/think/console/command/make/Service.php b/src/think/console/command/make/Service.php
new file mode 100644
index 0000000..3008b0b
--- /dev/null
+++ b/src/think/console/command/make/Service.php
@@ -0,0 +1,36 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Service extends Make
+{
+ protected $type = "Service";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:service')
+ ->setDescription('Create a new Service class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'service.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\service';
+ }
+}
diff --git a/src/think/console/command/make/Subscribe.php b/src/think/console/command/make/Subscribe.php
new file mode 100644
index 0000000..f9211f0
--- /dev/null
+++ b/src/think/console/command/make/Subscribe.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Subscribe extends Make
+{
+ protected $type = "Subscribe";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:subscribe')
+ ->setDescription('Create a new subscribe class');
+ }
+
+ protected function getStub(): string
+ {
+ return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'subscribe.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\subscribe';
+ }
+}
diff --git a/src/think/console/command/make/Validate.php b/src/think/console/command/make/Validate.php
new file mode 100644
index 0000000..798b4cf
--- /dev/null
+++ b/src/think/console/command/make/Validate.php
@@ -0,0 +1,38 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Validate extends Make
+{
+ protected $type = "Validate";
+
+ protected function configure()
+ {
+ parent::configure();
+ $this->setName('make:validate')
+ ->setDescription('Create a validate class');
+ }
+
+ protected function getStub(): string
+ {
+ $stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR;
+
+ return $stubPath . 'validate.stub';
+ }
+
+ protected function getNamespace(string $app): string
+ {
+ return parent::getNamespace($app) . '\\validate';
+ }
+}
diff --git a/src/think/console/command/make/stubs/command.stub b/src/think/console/command/make/stubs/command.stub
new file mode 100644
index 0000000..3ee2b1c
--- /dev/null
+++ b/src/think/console/command/make/stubs/command.stub
@@ -0,0 +1,26 @@
+setName('{%commandName%}')
+ ->setDescription('the {%commandName%} command');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ // 指令输出
+ $output->writeln('{%commandName%}');
+ }
+}
diff --git a/src/think/console/command/make/stubs/controller.api.stub b/src/think/console/command/make/stubs/controller.api.stub
new file mode 100644
index 0000000..5d3383d
--- /dev/null
+++ b/src/think/console/command/make/stubs/controller.api.stub
@@ -0,0 +1,64 @@
+ ['规则1','规则2'...]
+ *
+ * @var array
+ */
+ protected $rule = [];
+
+ /**
+ * 定义错误信息
+ * 格式:'字段名.规则名' => '错误信息'
+ *
+ * @var array
+ */
+ protected $message = [];
+}
diff --git a/src/think/console/command/optimize/Config.php b/src/think/console/command/optimize/Config.php
new file mode 100644
index 0000000..bee164f
--- /dev/null
+++ b/src/think/console/command/optimize/Config.php
@@ -0,0 +1,64 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+
+class Config extends Command
+{
+ protected function configure()
+ {
+ $this->setName('optimize:config')
+ ->addArgument('dir', Argument::OPTIONAL, 'dir name .')
+ ->setDescription('Build config cache.');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ // 加载配置文件
+ $dir = $input->getArgument('dir') ?: '';
+ $path = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . ($dir ? $dir . DIRECTORY_SEPARATOR : '');
+ if (!is_dir($path)) {
+ try {
+ mkdir($path, 0755, true);
+ } catch (\Exception $e) {
+ // 创建失败
+ }
+ }
+ $file = $path . 'config.php';
+ $config = $this->loadConfig($dir);
+ $content = 'writeln("Succeed!");
+ } else {
+ $output->writeln("config build fail");
+ }
+ }
+
+ public function loadConfig($dir = '')
+ {
+ $configPath = $this->app->getRootPath() . ($dir ? 'app' . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR : '') . 'config' . DIRECTORY_SEPARATOR;
+ $files = [];
+
+ if (is_dir($configPath)) {
+ $files = glob($configPath . '*' . $this->app->getConfigExt());
+ }
+
+ foreach ($files as $file) {
+ $this->app->config->load($file, pathinfo($file, PATHINFO_FILENAME));
+ }
+
+ return $this->app->config->get();
+ }
+}
\ No newline at end of file
diff --git a/src/think/console/command/optimize/Route.php b/src/think/console/command/optimize/Route.php
new file mode 100644
index 0000000..5636590
--- /dev/null
+++ b/src/think/console/command/optimize/Route.php
@@ -0,0 +1,86 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use DirectoryIterator;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+use think\event\RouteLoaded;
+
+class Route extends Command
+{
+ protected function configure()
+ {
+ $this->setName('optimize:route')
+ ->addArgument('dir', Argument::OPTIONAL, 'dir name .')
+ ->setDescription('Build app route cache.');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $dir = $input->getArgument('dir') ?: '';
+
+ $path = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . ($dir ? $dir . DIRECTORY_SEPARATOR : '');
+ if (!is_dir($path)) {
+ try {
+ mkdir($path, 0755, true);
+ } catch (\Exception $e) {
+ // 创建失败
+ }
+ }
+ file_put_contents($path . 'route.php', $this->buildRouteCache($dir));
+ $output->writeln('Succeed!');
+ }
+
+ protected function scanRoute($path, $root, $autoGroup)
+ {
+ $iterator = new DirectoryIterator($path);
+ foreach ($iterator as $fileinfo) {
+ if ($fileinfo->isDot()) {
+ continue;
+ }
+
+ if ($fileinfo->getType() == 'file' && $fileinfo->getExtension() == 'php') {
+ $groupName = str_replace('\\', '/', substr_replace($fileinfo->getPath(), '', 0, strlen($root)));
+ if ($groupName) {
+ $this->app->route->group($groupName, function() use ($fileinfo) {
+ include $fileinfo->getRealPath();
+ });
+ } else {
+ include $fileinfo->getRealPath();
+ }
+ } elseif ($autoGroup && $fileinfo->isDir()) {
+ $this->scanRoute($fileinfo->getPathname(), $root, $autoGroup);
+ }
+ }
+ }
+
+ protected function buildRouteCache(?string $dir = null): string
+ {
+ $this->app->route->clear();
+ $this->app->route->lazy(false);
+
+ // 路由检测
+ $autoGroup = $this->app->route->config('route_auto_group');
+ $path = $this->app->getRootPath() . ($dir ? 'app' . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR : '') . 'route' . DIRECTORY_SEPARATOR;
+
+ $this->scanRoute($path, $path, $autoGroup);
+
+ //触发路由载入完成事件
+ $this->app->event->trigger(RouteLoaded::class);
+ $rules = $this->app->route->getName();
+
+ return '
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use Exception;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+use think\db\PDOConnection;
+
+class Schema extends Command
+{
+ protected function configure()
+ {
+ $this->setName('optimize:schema')
+ ->addArgument('dir', Argument::OPTIONAL, 'dir name .')
+ ->addOption('connection', null, Option::VALUE_REQUIRED, 'connection name .')
+ ->addOption('table', null, Option::VALUE_REQUIRED, 'table name .')
+ ->setDescription('Build database schema cache.');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $dir = $input->getArgument('dir') ?: '';
+
+ if ($input->hasOption('table')) {
+ $connection = $this->app->db->connect($input->getOption('connection'));
+ if (!$connection instanceof PDOConnection) {
+ $output->error("only PDO connection support schema cache!");
+ return;
+ }
+ $table = $input->getOption('table');
+ if (!str_contains($table, '.')) {
+ $dbName = $connection->getConfig('database');
+ } else {
+ [$dbName, $table] = explode('.', $table);
+ }
+
+ if ($table == '*') {
+ $table = $connection->getTables($dbName);
+ }
+
+ $this->buildDataBaseSchema($connection, (array) $table, $dbName);
+ } else {
+ if ($dir) {
+ $appPath = $this->app->getBasePath() . $dir . DIRECTORY_SEPARATOR;
+ $namespace = 'app\\' . $dir;
+ } else {
+ $appPath = $this->app->getBasePath();
+ $namespace = 'app';
+ }
+
+ $path = $appPath . 'model';
+ $list = is_dir($path) ? scandir($path) : [];
+
+ foreach ($list as $file) {
+ if (str_starts_with($file, '.')) {
+ continue;
+ }
+ $class = '\\' . $namespace . '\\model\\' . pathinfo($file, PATHINFO_FILENAME);
+
+ if (!class_exists($class)) {
+ continue;
+ }
+
+ $this->buildModelSchema($class);
+ }
+ }
+
+ $output->writeln('Succeed!');
+ }
+
+ protected function buildModelSchema(string $class): void
+ {
+ $reflect = new \ReflectionClass($class);
+ if (!$reflect->isAbstract() && $reflect->isSubclassOf('\think\Model')) {
+ try {
+ /** @var \think\Model $model */
+ $model = new $class;
+ $connection = $model->db()->getConnection();
+ if ($connection instanceof PDOConnection) {
+ $table = $model->getTable();
+ //预读字段信息
+ $connection->getSchemaInfo($table, true);
+ }
+ } catch (Exception $e) {
+
+ }
+ }
+ }
+
+ protected function buildDataBaseSchema(PDOConnection $connection, array $tables, string $dbName): void
+ {
+ foreach ($tables as $table) {
+ //预读字段信息
+ $connection->getSchemaInfo("{$dbName}.{$table}", true);
+ }
+ }
+}
diff --git a/src/think/console/input/Argument.php b/src/think/console/input/Argument.php
new file mode 100644
index 0000000..97262c8
--- /dev/null
+++ b/src/think/console/input/Argument.php
@@ -0,0 +1,138 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Argument
+{
+ // 必传参数
+ const REQUIRED = 1;
+
+ // 可选参数
+ const OPTIONAL = 2;
+
+ // 数组参数
+ const IS_ARRAY = 4;
+
+ /**
+ * 参数名
+ * @var string
+ */
+ private $name;
+
+ /**
+ * 参数类型
+ * @var int
+ */
+ private $mode;
+
+ /**
+ * 参数默认值
+ * @var mixed
+ */
+ private $default;
+
+ /**
+ * 参数描述
+ * @var string
+ */
+ private $description;
+
+ /**
+ * 构造方法
+ * @param string $name 参数名
+ * @param int $mode 参数类型: self::REQUIRED 或者 self::OPTIONAL
+ * @param string $description 描述
+ * @param mixed $default 默认值 (仅 self::OPTIONAL 类型有效)
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(string $name, ?int $mode = null, string $description = '', $default = null)
+ {
+ if (null === $mode) {
+ $mode = self::OPTIONAL;
+ } elseif (!is_int($mode) || $mode > 7 || $mode < 1) {
+ throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode));
+ }
+
+ $this->name = $name;
+ $this->mode = $mode;
+ $this->description = $description;
+
+ $this->setDefault($default);
+ }
+
+ /**
+ * 获取参数名
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * 是否必须
+ * @return bool
+ */
+ public function isRequired(): bool
+ {
+ return self::REQUIRED === (self::REQUIRED & $this->mode);
+ }
+
+ /**
+ * 该参数是否接受数组
+ * @return bool
+ */
+ public function isArray(): bool
+ {
+ return self::IS_ARRAY === (self::IS_ARRAY & $this->mode);
+ }
+
+ /**
+ * 设置默认值
+ * @param mixed $default 默认值
+ * @throws \LogicException
+ */
+ public function setDefault($default = null): void
+ {
+ if (self::REQUIRED === $this->mode && null !== $default) {
+ throw new \LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.');
+ }
+
+ if ($this->isArray()) {
+ if (null === $default) {
+ $default = [];
+ } elseif (!is_array($default)) {
+ throw new \LogicException('A default value for an array argument must be an array.');
+ }
+ }
+
+ $this->default = $default;
+ }
+
+ /**
+ * 获取默认值
+ * @return mixed
+ */
+ public function getDefault()
+ {
+ return $this->default;
+ }
+
+ /**
+ * 获取描述
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+}
diff --git a/src/think/console/input/Definition.php b/src/think/console/input/Definition.php
new file mode 100644
index 0000000..ccf02a0
--- /dev/null
+++ b/src/think/console/input/Definition.php
@@ -0,0 +1,375 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Definition
+{
+
+ /**
+ * @var Argument[]
+ */
+ private $arguments;
+
+ private $requiredCount;
+ private $hasAnArrayArgument = false;
+ private $hasOptional;
+
+ /**
+ * @var Option[]
+ */
+ private $options;
+ private $shortcuts;
+
+ /**
+ * 构造方法
+ * @param array $definition
+ * @api
+ */
+ public function __construct(array $definition = [])
+ {
+ $this->setDefinition($definition);
+ }
+
+ /**
+ * 设置指令的定义
+ * @param array $definition 定义的数组
+ */
+ public function setDefinition(array $definition): void
+ {
+ $arguments = [];
+ $options = [];
+ foreach ($definition as $item) {
+ if ($item instanceof Option) {
+ $options[] = $item;
+ } else {
+ $arguments[] = $item;
+ }
+ }
+
+ $this->setArguments($arguments);
+ $this->setOptions($options);
+ }
+
+ /**
+ * 设置参数
+ * @param Argument[] $arguments 参数数组
+ */
+ public function setArguments(array $arguments = []): void
+ {
+ $this->arguments = [];
+ $this->requiredCount = 0;
+ $this->hasOptional = false;
+ $this->hasAnArrayArgument = false;
+ $this->addArguments($arguments);
+ }
+
+ /**
+ * 添加参数
+ * @param Argument[] $arguments 参数数组
+ * @api
+ */
+ public function addArguments(array $arguments = []): void
+ {
+ if (null !== $arguments) {
+ foreach ($arguments as $argument) {
+ $this->addArgument($argument);
+ }
+ }
+ }
+
+ /**
+ * 添加一个参数
+ * @param Argument $argument 参数
+ * @throws \LogicException
+ */
+ public function addArgument(Argument $argument): void
+ {
+ if (isset($this->arguments[$argument->getName()])) {
+ throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName()));
+ }
+
+ if ($this->hasAnArrayArgument) {
+ throw new \LogicException('Cannot add an argument after an array argument.');
+ }
+
+ if ($argument->isRequired() && $this->hasOptional) {
+ throw new \LogicException('Cannot add a required argument after an optional one.');
+ }
+
+ if ($argument->isArray()) {
+ $this->hasAnArrayArgument = true;
+ }
+
+ if ($argument->isRequired()) {
+ ++$this->requiredCount;
+ } else {
+ $this->hasOptional = true;
+ }
+
+ $this->arguments[$argument->getName()] = $argument;
+ }
+
+ /**
+ * 根据名称或者位置获取参数
+ * @param string|int $name 参数名或者位置
+ * @return Argument 参数
+ * @throws \InvalidArgumentException
+ */
+ public function getArgument($name): Argument
+ {
+ if (!$this->hasArgument($name)) {
+ throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ }
+
+ $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;
+
+ return $arguments[$name];
+ }
+
+ /**
+ * 根据名称或位置检查是否具有某个参数
+ * @param string|int $name 参数名或者位置
+ * @return bool
+ * @api
+ */
+ public function hasArgument($name): bool
+ {
+ $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;
+
+ return isset($arguments[$name]);
+ }
+
+ /**
+ * 获取所有的参数
+ * @return Argument[] 参数数组
+ */
+ public function getArguments(): array
+ {
+ return $this->arguments;
+ }
+
+ /**
+ * 获取参数数量
+ * @return int
+ */
+ public function getArgumentCount(): int
+ {
+ return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments);
+ }
+
+ /**
+ * 获取必填的参数的数量
+ * @return int
+ */
+ public function getArgumentRequiredCount(): int
+ {
+ return $this->requiredCount;
+ }
+
+ /**
+ * 获取参数默认值
+ * @return array
+ */
+ public function getArgumentDefaults(): array
+ {
+ $values = [];
+ foreach ($this->arguments as $argument) {
+ $values[$argument->getName()] = $argument->getDefault();
+ }
+
+ return $values;
+ }
+
+ /**
+ * 设置选项
+ * @param Option[] $options 选项数组
+ */
+ public function setOptions(array $options = []): void
+ {
+ $this->options = [];
+ $this->shortcuts = [];
+ $this->addOptions($options);
+ }
+
+ /**
+ * 添加选项
+ * @param Option[] $options 选项数组
+ * @api
+ */
+ public function addOptions(array $options = []): void
+ {
+ foreach ($options as $option) {
+ $this->addOption($option);
+ }
+ }
+
+ /**
+ * 添加一个选项
+ * @param Option $option 选项
+ * @throws \LogicException
+ * @api
+ */
+ public function addOption(Option $option): void
+ {
+ if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
+ throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
+ }
+
+ if ($option->getShortcut()) {
+ foreach (explode('|', $option->getShortcut()) as $shortcut) {
+ if (isset($this->shortcuts[$shortcut])
+ && !$option->equals($this->options[$this->shortcuts[$shortcut]])
+ ) {
+ throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut));
+ }
+ }
+ }
+
+ $this->options[$option->getName()] = $option;
+ if ($option->getShortcut()) {
+ foreach (explode('|', $option->getShortcut()) as $shortcut) {
+ $this->shortcuts[$shortcut] = $option->getName();
+ }
+ }
+ }
+
+ /**
+ * 根据名称获取选项
+ * @param string $name 选项名
+ * @return Option
+ * @throws \InvalidArgumentException
+ * @api
+ */
+ public function getOption(string $name): Option
+ {
+ if (!$this->hasOption($name)) {
+ throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name));
+ }
+
+ return $this->options[$name];
+ }
+
+ /**
+ * 根据名称检查是否有这个选项
+ * @param string $name 选项名
+ * @return bool
+ * @api
+ */
+ public function hasOption(string $name): bool
+ {
+ return isset($this->options[$name]);
+ }
+
+ /**
+ * 获取所有选项
+ * @return Option[]
+ * @api
+ */
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ /**
+ * 根据名称检查某个选项是否有短名称
+ * @param string $name 短名称
+ * @return bool
+ */
+ public function hasShortcut(string $name): bool
+ {
+ return isset($this->shortcuts[$name]);
+ }
+
+ /**
+ * 根据短名称获取选项
+ * @param string $shortcut 短名称
+ * @return Option
+ */
+ public function getOptionForShortcut(string $shortcut): Option
+ {
+ return $this->getOption($this->shortcutToName($shortcut));
+ }
+
+ /**
+ * 获取所有选项的默认值
+ * @return array
+ */
+ public function getOptionDefaults(): array
+ {
+ $values = [];
+ foreach ($this->options as $option) {
+ $values[$option->getName()] = $option->getDefault();
+ }
+
+ return $values;
+ }
+
+ /**
+ * 根据短名称获取选项名
+ * @param string $shortcut 短名称
+ * @return string
+ * @throws \InvalidArgumentException
+ */
+ private function shortcutToName(string $shortcut): string
+ {
+ if (!isset($this->shortcuts[$shortcut])) {
+ throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut));
+ }
+
+ return $this->shortcuts[$shortcut];
+ }
+
+ /**
+ * 获取该指令的介绍
+ * @param bool $short 是否简洁介绍
+ * @return string
+ */
+ public function getSynopsis(bool $short = false): string
+ {
+ $elements = [];
+
+ if ($short && $this->getOptions()) {
+ $elements[] = '[options]';
+ } elseif (!$short) {
+ foreach ($this->getOptions() as $option) {
+ $value = '';
+ if ($option->acceptValue()) {
+ $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : '');
+ }
+
+ $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : '';
+ $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value);
+ }
+ }
+
+ if (count($elements) && $this->getArguments()) {
+ $elements[] = '[--]';
+ }
+
+ foreach ($this->getArguments() as $argument) {
+ $element = '<' . $argument->getName() . '>';
+ if (!$argument->isRequired()) {
+ $element = '[' . $element . ']';
+ } elseif ($argument->isArray()) {
+ $element .= ' (' . $element . ')';
+ }
+
+ if ($argument->isArray()) {
+ $element .= '...';
+ }
+
+ $elements[] = $element;
+ }
+
+ return implode(' ', $elements);
+ }
+}
diff --git a/src/think/console/input/Option.php b/src/think/console/input/Option.php
new file mode 100644
index 0000000..8554087
--- /dev/null
+++ b/src/think/console/input/Option.php
@@ -0,0 +1,221 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+/**
+ * 命令行选项
+ * @package think\console\input
+ */
+class Option
+{
+ // 无需传值
+ const VALUE_NONE = 1;
+ // 必须传值
+ const VALUE_REQUIRED = 2;
+ // 可选传值
+ const VALUE_OPTIONAL = 4;
+ // 传数组值
+ const VALUE_IS_ARRAY = 8;
+
+ /**
+ * 选项名
+ * @var string
+ */
+ private $name = '';
+
+ /**
+ * 选项短名称
+ * @var string
+ */
+ private $shortcut = '';
+
+ /**
+ * 选项类型
+ * @var int
+ */
+ private $mode;
+
+ /**
+ * 选项默认值
+ * @var mixed
+ */
+ private $default;
+
+ /**
+ * 选项描述
+ * @var string
+ */
+ private $description = '';
+
+ /**
+ * 构造方法
+ * @param string $name 选项名
+ * @param string|array $shortcut 短名称,多个用|隔开或者使用数组
+ * @param int $mode 选项类型(可选类型为 self::VALUE_*)
+ * @param string $description 描述
+ * @param mixed $default 默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null)
+ * @throws \InvalidArgumentException
+ */
+ public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null)
+ {
+ if (str_starts_with($name, '--')) {
+ $name = substr($name, 2);
+ }
+
+ if (empty($name)) {
+ throw new \InvalidArgumentException('An option name cannot be empty.');
+ }
+
+ if (empty($shortcut)) {
+ $shortcut = '';
+ }
+
+ if ('' !== $shortcut) {
+ if (is_array($shortcut)) {
+ $shortcut = implode('|', $shortcut);
+ }
+ $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-'));
+ $shortcuts = array_filter($shortcuts);
+ $shortcut = implode('|', $shortcuts);
+
+ if (empty($shortcut)) {
+ throw new \InvalidArgumentException('An option shortcut cannot be empty.');
+ }
+ }
+
+ if (null === $mode) {
+ $mode = self::VALUE_NONE;
+ } elseif (!is_int($mode) || $mode > 15 || $mode < 1) {
+ throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode));
+ }
+
+ $this->name = $name;
+ $this->shortcut = $shortcut;
+ $this->mode = $mode;
+ $this->description = $description;
+
+ if ($this->isArray() && !$this->acceptValue()) {
+ throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.');
+ }
+
+ $this->setDefault($default);
+ }
+
+ /**
+ * 获取短名称
+ * @return string
+ */
+ public function getShortcut(): string
+ {
+ return $this->shortcut;
+ }
+
+ /**
+ * 获取选项名
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * 是否可以设置值
+ * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false
+ */
+ public function acceptValue(): bool
+ {
+ return $this->isValueRequired() || $this->isValueOptional();
+ }
+
+ /**
+ * 是否必须
+ * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false
+ */
+ public function isValueRequired(): bool
+ {
+ return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode);
+ }
+
+ /**
+ * 是否可选
+ * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false
+ */
+ public function isValueOptional(): bool
+ {
+ return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode);
+ }
+
+ /**
+ * 选项值是否接受数组
+ * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false
+ */
+ public function isArray(): bool
+ {
+ return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
+ }
+
+ /**
+ * 设置默认值
+ * @param mixed $default 默认值
+ * @throws \LogicException
+ */
+ public function setDefault($default = null)
+ {
+ if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) {
+ throw new \LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.');
+ }
+
+ if ($this->isArray()) {
+ if (null === $default) {
+ $default = [];
+ } elseif (!is_array($default)) {
+ throw new \LogicException('A default value for an array option must be an array.');
+ }
+ }
+
+ $this->default = $this->acceptValue() ? $default : false;
+ }
+
+ /**
+ * 获取默认值
+ * @return mixed
+ */
+ public function getDefault()
+ {
+ return $this->default;
+ }
+
+ /**
+ * 获取描述文字
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ /**
+ * 检查所给选项是否是当前这个
+ * @param Option $option
+ * @return bool
+ */
+ public function equals(Option $option): bool
+ {
+ return $option->getName() === $this->getName()
+ && $option->getShortcut() === $this->getShortcut()
+ && $option->getDefault() === $this->getDefault()
+ && $option->isArray() === $this->isArray()
+ && $option->isValueRequired() === $this->isValueRequired()
+ && $option->isValueOptional() === $this->isValueOptional();
+ }
+}
diff --git a/src/think/console/output/Ask.php b/src/think/console/output/Ask.php
new file mode 100644
index 0000000..01bc219
--- /dev/null
+++ b/src/think/console/output/Ask.php
@@ -0,0 +1,336 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+use think\console\Input;
+use think\console\Output;
+use think\console\output\question\Choice;
+use think\console\output\question\Confirmation;
+
+class Ask
+{
+ private static $stty;
+
+ private static $shell;
+
+ /** @var Input */
+ protected $input;
+
+ /** @var Output */
+ protected $output;
+
+ /** @var Question */
+ protected $question;
+
+ public function __construct(Input $input, Output $output, Question $question)
+ {
+ $this->input = $input;
+ $this->output = $output;
+ $this->question = $question;
+ }
+
+ public function run()
+ {
+ if (!$this->input->isInteractive()) {
+ return $this->question->getDefault();
+ }
+
+ if (!$this->question->getValidator()) {
+ return $this->doAsk();
+ }
+
+ $that = $this;
+
+ $interviewer = function () use ($that) {
+ return $that->doAsk();
+ };
+
+ return $this->validateAttempts($interviewer);
+ }
+
+ protected function doAsk()
+ {
+ $this->writePrompt();
+
+ $inputStream = STDIN;
+ $autocomplete = $this->question->getAutocompleterValues();
+
+ if (null === $autocomplete || !$this->hasSttyAvailable()) {
+ $ret = false;
+ if ($this->question->isHidden()) {
+ try {
+ $ret = trim($this->getHiddenResponse($inputStream));
+ } catch (\RuntimeException $e) {
+ if (!$this->question->isHiddenFallback()) {
+ throw $e;
+ }
+ }
+ }
+
+ if (false === $ret) {
+ $ret = fgets($inputStream, 4096);
+ if (false === $ret) {
+ throw new \RuntimeException('Aborted');
+ }
+ $ret = trim($ret);
+ }
+ } else {
+ $ret = trim($this->autocomplete($inputStream));
+ }
+
+ $ret = strlen($ret) > 0 ? $ret : $this->question->getDefault();
+
+ if ($normalizer = $this->question->getNormalizer()) {
+ return $normalizer($ret);
+ }
+
+ return $ret;
+ }
+
+ private function autocomplete($inputStream)
+ {
+ $autocomplete = $this->question->getAutocompleterValues();
+ $ret = '';
+
+ $i = 0;
+ $ofs = -1;
+ $matches = $autocomplete;
+ $numMatches = count($matches);
+
+ $sttyMode = shell_exec('stty -g');
+
+ shell_exec('stty -icanon -echo');
+
+ while (!feof($inputStream)) {
+ $c = fread($inputStream, 1);
+
+ if ("\177" === $c) {
+ if (0 === $numMatches && 0 !== $i) {
+ --$i;
+ $this->output->write("\033[1D");
+ }
+
+ if ($i === 0) {
+ $ofs = -1;
+ $matches = $autocomplete;
+ $numMatches = count($matches);
+ } else {
+ $numMatches = 0;
+ }
+
+ $ret = substr($ret, 0, $i);
+ } elseif ("\033" === $c) {
+ $c .= fread($inputStream, 2);
+
+ if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
+ if ('A' === $c[2] && -1 === $ofs) {
+ $ofs = 0;
+ }
+
+ if (0 === $numMatches) {
+ continue;
+ }
+
+ $ofs += ('A' === $c[2]) ? -1 : 1;
+ $ofs = ($numMatches + $ofs) % $numMatches;
+ }
+ } elseif (ord($c) < 32) {
+ if ("\t" === $c || "\n" === $c) {
+ if ($numMatches > 0 && -1 !== $ofs) {
+ $ret = $matches[$ofs];
+ $this->output->write(substr($ret, $i));
+ $i = strlen($ret);
+ }
+
+ if ("\n" === $c) {
+ $this->output->write($c);
+ break;
+ }
+
+ $numMatches = 0;
+ }
+
+ continue;
+ } else {
+ $this->output->write($c);
+ $ret .= $c;
+ ++$i;
+
+ $numMatches = 0;
+ $ofs = 0;
+
+ foreach ($autocomplete as $value) {
+ if (str_starts_with($value, $ret) && $i !== strlen($value)) {
+ $matches[$numMatches++] = $value;
+ }
+ }
+ }
+
+ $this->output->write("\033[K");
+
+ if ($numMatches > 0 && -1 !== $ofs) {
+ $this->output->write("\0337");
+ $this->output->highlight(substr($matches[$ofs], $i));
+ $this->output->write("\0338");
+ }
+ }
+
+ shell_exec(sprintf('stty %s', $sttyMode));
+
+ return $ret;
+ }
+
+ protected function getHiddenResponse($inputStream)
+ {
+ if ('\\' === DIRECTORY_SEPARATOR) {
+ $exe = __DIR__ . '/../bin/hiddeninput.exe';
+
+ $value = rtrim(shell_exec($exe));
+ $this->output->writeln('');
+
+ return $value;
+ }
+
+ if ($this->hasSttyAvailable()) {
+ $sttyMode = shell_exec('stty -g');
+
+ shell_exec('stty -echo');
+ $value = fgets($inputStream, 4096);
+ shell_exec(sprintf('stty %s', $sttyMode));
+
+ if (false === $value) {
+ throw new \RuntimeException('Aborted');
+ }
+
+ $value = trim($value);
+ $this->output->writeln('');
+
+ return $value;
+ }
+
+ if (false !== $shell = $this->getShell()) {
+ $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
+ $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
+ $value = rtrim(shell_exec($command));
+ $this->output->writeln('');
+
+ return $value;
+ }
+
+ throw new \RuntimeException('Unable to hide the response.');
+ }
+
+ protected function validateAttempts($interviewer)
+ {
+ /** @var \Exception $error */
+ $error = null;
+ $attempts = $this->question->getMaxAttempts();
+ while (null === $attempts || $attempts--) {
+ if (null !== $error) {
+ $this->output->error($error->getMessage());
+ }
+
+ try {
+ return call_user_func($this->question->getValidator(), $interviewer());
+ } catch (\Exception $error) {
+ }
+ }
+
+ throw $error;
+ }
+
+ /**
+ * 显示问题的提示信息
+ */
+ protected function writePrompt()
+ {
+ $text = $this->question->getQuestion();
+ $default = $this->question->getDefault();
+
+ switch (true) {
+ case null === $default:
+ $text = sprintf(' %s:', $text);
+
+ break;
+
+ case $this->question instanceof Confirmation:
+ $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no');
+
+ break;
+
+ case $this->question instanceof Choice && $this->question->isMultiselect():
+ $choices = $this->question->getChoices();
+ $default = explode(',', $default);
+
+ foreach ($default as $key => $value) {
+ $default[$key] = $choices[trim($value)];
+ }
+
+ $text = sprintf(' %s [%s]:', $text, implode(', ', $default));
+
+ break;
+
+ case $this->question instanceof Choice:
+ $choices = $this->question->getChoices();
+ $text = sprintf(' %s [%s]:', $text, $choices[$default]);
+
+ break;
+
+ default:
+ $text = sprintf(' %s [%s]:', $text, $default);
+ }
+
+ $this->output->writeln($text);
+
+ if ($this->question instanceof Choice) {
+ $width = max(array_map('strlen', array_keys($this->question->getChoices())));
+
+ foreach ($this->question->getChoices() as $key => $value) {
+ $this->output->writeln(sprintf(" [%-{$width}s] %s", $key, $value));
+ }
+ }
+
+ $this->output->write(' > ');
+ }
+
+ private function getShell()
+ {
+ if (null !== self::$shell) {
+ return self::$shell;
+ }
+
+ self::$shell = false;
+
+ if (file_exists('/usr/bin/env')) {
+ $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
+ foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) {
+ if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
+ self::$shell = $sh;
+ break;
+ }
+ }
+ }
+
+ return self::$shell;
+ }
+
+ private function hasSttyAvailable()
+ {
+ if (null !== self::$stty) {
+ return self::$stty;
+ }
+
+ exec('stty 2>&1', $output, $exitcode);
+
+ return self::$stty = $exitcode === 0;
+ }
+}
diff --git a/src/think/console/output/Descriptor.php b/src/think/console/output/Descriptor.php
new file mode 100644
index 0000000..2985ddb
--- /dev/null
+++ b/src/think/console/output/Descriptor.php
@@ -0,0 +1,323 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+use think\Console;
+use think\console\Command;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Definition as InputDefinition;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+use think\console\output\descriptor\Console as ConsoleDescription;
+
+class Descriptor
+{
+
+ /**
+ * @var Output
+ */
+ protected $output;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function describe(Output $output, $object, array $options = [])
+ {
+ $this->output = $output;
+
+ switch (true) {
+ case $object instanceof InputArgument:
+ $this->describeInputArgument($object, $options);
+ break;
+ case $object instanceof InputOption:
+ $this->describeInputOption($object, $options);
+ break;
+ case $object instanceof InputDefinition:
+ $this->describeInputDefinition($object, $options);
+ break;
+ case $object instanceof Command:
+ $this->describeCommand($object, $options);
+ break;
+ case $object instanceof Console:
+ $this->describeConsole($object, $options);
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', $object::class));
+ }
+ }
+
+ /**
+ * 输出内容
+ * @param string $content
+ * @param bool $decorated
+ */
+ protected function write($content, $decorated = false)
+ {
+ $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW);
+ }
+
+ /**
+ * 描述参数
+ * @param InputArgument $argument
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeInputArgument(InputArgument $argument, array $options = [])
+ {
+ if (null !== $argument->getDefault()
+ && (!is_array($argument->getDefault())
+ || count($argument->getDefault()))
+ ) {
+ $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault()));
+ } else {
+ $default = '';
+ }
+
+ $totalWidth = $options['total_width'] ?? strlen($argument->getName());
+ $spacingWidth = $totalWidth - strlen($argument->getName()) + 2;
+
+ $this->writeText(sprintf(" %s%s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces
+ preg_replace('/\s*[\r\n]\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options);
+ }
+
+ /**
+ * 描述选项
+ * @param InputOption $option
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeInputOption(InputOption $option, array $options = [])
+ {
+ if ($option->acceptValue() && null !== $option->getDefault()
+ && (!is_array($option->getDefault())
+ || count($option->getDefault()))
+ ) {
+ $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault()));
+ } else {
+ $default = '';
+ }
+
+ $value = '';
+ if ($option->acceptValue()) {
+ $value = '=' . strtoupper($option->getName());
+
+ if ($option->isValueOptional()) {
+ $value = '[' . $value . ']';
+ }
+ }
+
+ $totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
+ $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value));
+
+ $spacingWidth = $totalWidth - strlen($synopsis) + 2;
+
+ $this->writeText(sprintf(" %s%s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces
+ preg_replace('/\s*[\r\n]\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : ''), $options);
+ }
+
+ /**
+ * 描述输入
+ * @param InputDefinition $definition
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeInputDefinition(InputDefinition $definition, array $options = [])
+ {
+ $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
+ foreach ($definition->getArguments() as $argument) {
+ $totalWidth = max($totalWidth, strlen($argument->getName()));
+ }
+
+ if ($definition->getArguments()) {
+ $this->writeText('Arguments:', $options);
+ $this->writeText("\n");
+ foreach ($definition->getArguments() as $argument) {
+ $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
+ $this->writeText("\n");
+ }
+ }
+
+ if ($definition->getArguments() && $definition->getOptions()) {
+ $this->writeText("\n");
+ }
+
+ if ($definition->getOptions()) {
+ $laterOptions = [];
+
+ $this->writeText('Options:', $options);
+ foreach ($definition->getOptions() as $option) {
+ if (strlen($option->getShortcut()) > 1) {
+ $laterOptions[] = $option;
+ continue;
+ }
+ $this->writeText("\n");
+ $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
+ }
+ foreach ($laterOptions as $option) {
+ $this->writeText("\n");
+ $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
+ }
+ }
+ }
+
+ /**
+ * 描述指令
+ * @param Command $command
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeCommand(Command $command, array $options = [])
+ {
+ $command->getSynopsis(true);
+ $command->getSynopsis(false);
+ $command->mergeConsoleDefinition(false);
+
+ $this->writeText('Usage:', $options);
+ foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
+ $this->writeText("\n");
+ $this->writeText(' ' . $usage, $options);
+ }
+ $this->writeText("\n");
+
+ $definition = $command->getNativeDefinition();
+ if ($definition->getOptions() || $definition->getArguments()) {
+ $this->writeText("\n");
+ $this->describeInputDefinition($definition, $options);
+ $this->writeText("\n");
+ }
+
+ if ($help = $command->getProcessedHelp()) {
+ $this->writeText("\n");
+ $this->writeText('Help:', $options);
+ $this->writeText("\n");
+ $this->writeText(' ' . str_replace("\n", "\n ", $help), $options);
+ $this->writeText("\n");
+ }
+ }
+
+ /**
+ * 描述控制台
+ * @param Console $console
+ * @param array $options
+ * @return string|mixed
+ */
+ protected function describeConsole(Console $console, array $options = [])
+ {
+ $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
+ $description = new ConsoleDescription($console, $describedNamespace);
+
+ if (isset($options['raw_text']) && $options['raw_text']) {
+ $width = $this->getColumnWidth($description->getNamespaces());
+
+ foreach ($description->getCommands() as $command) {
+ $this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
+ $this->writeText("\n");
+ }
+ } else {
+ if ('' != $help = $console->getHelp()) {
+ $this->writeText("$help\n\n", $options);
+ }
+
+ $this->writeText("Usage:\n", $options);
+ $this->writeText(" command [options] [arguments]\n\n", $options);
+
+ $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options);
+
+ $this->writeText("\n");
+ $this->writeText("\n");
+
+ $width = $this->getColumnWidth($description->getNamespaces());
+
+ if ($describedNamespace) {
+ $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options);
+ } else {
+ $this->writeText('Available commands:', $options);
+ }
+
+ // add commands by namespace
+ foreach ($description->getNamespaces() as $namespace) {
+ if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
+ $this->writeText("\n");
+ $this->writeText(' ' . $namespace['id'] . '', $options);
+ }
+
+ foreach ($namespace['commands'] as $name) {
+ $this->writeText("\n");
+ $spacingWidth = $width - strlen($name);
+ $this->writeText(sprintf(" %s%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name)
+ ->getDescription()), $options);
+ }
+ }
+
+ $this->writeText("\n");
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private function writeText($content, array $options = [])
+ {
+ $this->write(isset($options['raw_text'])
+ && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true);
+ }
+
+ /**
+ * 格式化
+ * @param mixed $default
+ * @return string
+ */
+ private function formatDefaultValue($default)
+ {
+ return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ }
+
+ /**
+ * @param Namespaces[] $namespaces
+ * @return int
+ */
+ private function getColumnWidth(array $namespaces)
+ {
+ $width = 0;
+ foreach ($namespaces as $namespace) {
+ foreach ($namespace['commands'] as $name) {
+ if (strlen($name) > $width) {
+ $width = strlen($name);
+ }
+ }
+ }
+
+ return $width + 2;
+ }
+
+ /**
+ * @param InputOption[] $options
+ * @return int
+ */
+ private function calculateTotalWidthForOptions($options)
+ {
+ $totalWidth = 0;
+ foreach ($options as $option) {
+ $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + --
+
+ if ($option->acceptValue()) {
+ $valueLength = 1 + strlen($option->getName()); // = + value
+ $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
+
+ $nameLength += $valueLength;
+ }
+ $totalWidth = max($totalWidth, $nameLength);
+ }
+
+ return $totalWidth;
+ }
+}
diff --git a/src/think/console/output/Formatter.php b/src/think/console/output/Formatter.php
new file mode 100644
index 0000000..1b97ca3
--- /dev/null
+++ b/src/think/console/output/Formatter.php
@@ -0,0 +1,198 @@
+
+// +----------------------------------------------------------------------
+namespace think\console\output;
+
+use think\console\output\formatter\Stack as StyleStack;
+use think\console\output\formatter\Style;
+
+class Formatter
+{
+
+ private $decorated = false;
+ private $styles = [];
+ private $styleStack;
+
+ /**
+ * 转义
+ * @param string $text
+ * @return string
+ */
+ public static function escape($text)
+ {
+ return preg_replace('/([^\\\\]?)setStyle('error', new Style('white', 'red'));
+ $this->setStyle('info', new Style('green'));
+ $this->setStyle('comment', new Style('yellow'));
+ $this->setStyle('question', new Style('black', 'cyan'));
+ $this->setStyle('highlight', new Style('red'));
+ $this->setStyle('warning', new Style('black', 'yellow'));
+
+ $this->styleStack = new StyleStack();
+ }
+
+ /**
+ * 设置外观标识
+ * @param bool $decorated 是否美化文字
+ */
+ public function setDecorated($decorated)
+ {
+ $this->decorated = (bool) $decorated;
+ }
+
+ /**
+ * 获取外观标识
+ * @return bool
+ */
+ public function isDecorated()
+ {
+ return $this->decorated;
+ }
+
+ /**
+ * 添加一个新样式
+ * @param string $name 样式名
+ * @param Style $style 样式实例
+ */
+ public function setStyle($name, Style $style)
+ {
+ $this->styles[strtolower($name)] = $style;
+ }
+
+ /**
+ * 是否有这个样式
+ * @param string $name
+ * @return bool
+ */
+ public function hasStyle($name)
+ {
+ return isset($this->styles[strtolower($name)]);
+ }
+
+ /**
+ * 获取样式
+ * @param string $name
+ * @return Style
+ * @throws \InvalidArgumentException
+ */
+ public function getStyle($name)
+ {
+ if (!$this->hasStyle($name)) {
+ throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name));
+ }
+
+ return $this->styles[strtolower($name)];
+ }
+
+ /**
+ * 使用所给的样式格式化文字
+ * @param string $message 文字
+ * @return string
+ */
+ public function format($message)
+ {
+ $offset = 0;
+ $output = '';
+ $tagRegex = '[a-z][a-z0-9_=;-]*';
+ preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE);
+ foreach ($matches[0] as $i => $match) {
+ $pos = $match[1];
+ $text = $match[0];
+
+ if (0 != $pos && '\\' == $message[$pos - 1]) {
+ continue;
+ }
+
+ $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset));
+ $offset = $pos + strlen($text);
+
+ if ($open = '/' != $text[1]) {
+ $tag = $matches[1][$i][0];
+ } else {
+ $tag = $matches[3][$i][0] ?? '';
+ }
+
+ if (!$open && !$tag) {
+ // >
+ $this->styleStack->pop();
+ } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) {
+ $output .= $this->applyCurrentStyle($text);
+ } elseif ($open) {
+ $this->styleStack->push($style);
+ } else {
+ $this->styleStack->pop($style);
+ }
+ }
+
+ $output .= $this->applyCurrentStyle(substr($message, $offset));
+
+ return str_replace('\\<', '<', $output);
+ }
+
+ /**
+ * @return StyleStack
+ */
+ public function getStyleStack()
+ {
+ return $this->styleStack;
+ }
+
+ /**
+ * 根据字符串创建新的样式实例
+ * @param string $string
+ * @return Style|bool
+ */
+ private function createStyleFromString($string)
+ {
+ if (isset($this->styles[$string])) {
+ return $this->styles[$string];
+ }
+
+ if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) {
+ return false;
+ }
+
+ $style = new Style();
+ foreach ($matches as $match) {
+ array_shift($match);
+
+ if ('fg' == $match[0]) {
+ $style->setForeground($match[1]);
+ } elseif ('bg' == $match[0]) {
+ $style->setBackground($match[1]);
+ } else {
+ try {
+ $style->setOption($match[1]);
+ } catch (\InvalidArgumentException $e) {
+ return false;
+ }
+ }
+ }
+
+ return $style;
+ }
+
+ /**
+ * 从堆栈应用样式到文字
+ * @param string $text 文字
+ * @return string
+ */
+ private function applyCurrentStyle($text)
+ {
+ return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text;
+ }
+}
diff --git a/src/think/console/output/Question.php b/src/think/console/output/Question.php
new file mode 100644
index 0000000..03975f2
--- /dev/null
+++ b/src/think/console/output/Question.php
@@ -0,0 +1,211 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+class Question
+{
+
+ private $question;
+ private $attempts;
+ private $hidden = false;
+ private $hiddenFallback = true;
+ private $autocompleterValues;
+ private $validator;
+ private $default;
+ private $normalizer;
+
+ /**
+ * 构造方法
+ * @param string $question 问题
+ * @param mixed $default 默认答案
+ */
+ public function __construct($question, $default = null)
+ {
+ $this->question = $question;
+ $this->default = $default;
+ }
+
+ /**
+ * 获取问题
+ * @return string
+ */
+ public function getQuestion()
+ {
+ return $this->question;
+ }
+
+ /**
+ * 获取默认答案
+ * @return mixed
+ */
+ public function getDefault()
+ {
+ return $this->default;
+ }
+
+ /**
+ * 是否隐藏答案
+ * @return bool
+ */
+ public function isHidden()
+ {
+ return $this->hidden;
+ }
+
+ /**
+ * 隐藏答案
+ * @param bool $hidden
+ * @return Question
+ */
+ public function setHidden($hidden)
+ {
+ if ($this->autocompleterValues) {
+ throw new \LogicException('A hidden question cannot use the autocompleter.');
+ }
+
+ $this->hidden = (bool) $hidden;
+
+ return $this;
+ }
+
+ /**
+ * 不能被隐藏是否撤销
+ * @return bool
+ */
+ public function isHiddenFallback()
+ {
+ return $this->hiddenFallback;
+ }
+
+ /**
+ * 设置不能被隐藏的时候的操作
+ * @param bool $fallback
+ * @return Question
+ */
+ public function setHiddenFallback($fallback)
+ {
+ $this->hiddenFallback = (bool) $fallback;
+
+ return $this;
+ }
+
+ /**
+ * 获取自动完成
+ * @return null|array|\Traversable
+ */
+ public function getAutocompleterValues()
+ {
+ return $this->autocompleterValues;
+ }
+
+ /**
+ * 设置自动完成的值
+ * @param null|array|\Traversable $values
+ * @return Question
+ * @throws \InvalidArgumentException
+ * @throws \LogicException
+ */
+ public function setAutocompleterValues($values)
+ {
+ if (is_array($values) && $this->isAssoc($values)) {
+ $values = array_merge(array_keys($values), array_values($values));
+ }
+
+ if (null !== $values && !is_array($values)) {
+ if (!$values instanceof \Traversable || $values instanceof \Countable) {
+ throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.');
+ }
+ }
+
+ if ($this->hidden) {
+ throw new \LogicException('A hidden question cannot use the autocompleter.');
+ }
+
+ $this->autocompleterValues = $values;
+
+ return $this;
+ }
+
+ /**
+ * 设置答案的验证器
+ * @param null|callable $validator
+ * @return Question The current instance
+ */
+ public function setValidator($validator)
+ {
+ $this->validator = $validator;
+
+ return $this;
+ }
+
+ /**
+ * 获取验证器
+ * @return null|callable
+ */
+ public function getValidator()
+ {
+ return $this->validator;
+ }
+
+ /**
+ * 设置最大重试次数
+ * @param null|int $attempts
+ * @return Question
+ * @throws \InvalidArgumentException
+ */
+ public function setMaxAttempts($attempts)
+ {
+ if (null !== $attempts && $attempts < 1) {
+ throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.');
+ }
+
+ $this->attempts = $attempts;
+
+ return $this;
+ }
+
+ /**
+ * 获取最大重试次数
+ * @return null|int
+ */
+ public function getMaxAttempts()
+ {
+ return $this->attempts;
+ }
+
+ /**
+ * 设置响应的回调
+ * @param string|\Closure $normalizer
+ * @return Question
+ */
+ public function setNormalizer($normalizer)
+ {
+ $this->normalizer = $normalizer;
+
+ return $this;
+ }
+
+ /**
+ * 获取响应回调
+ * The normalizer can ba a callable (a string), a closure or a class implementing __invoke.
+ * @return string|\Closure
+ */
+ public function getNormalizer()
+ {
+ return $this->normalizer;
+ }
+
+ protected function isAssoc($array)
+ {
+ return (bool) count(array_filter(array_keys($array), 'is_string'));
+ }
+}
diff --git a/src/think/console/output/descriptor/Console.php b/src/think/console/output/descriptor/Console.php
new file mode 100644
index 0000000..ff9f464
--- /dev/null
+++ b/src/think/console/output/descriptor/Console.php
@@ -0,0 +1,153 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\descriptor;
+
+use think\Console as ThinkConsole;
+use think\console\Command;
+
+class Console
+{
+
+ const GLOBAL_NAMESPACE = '_global';
+
+ /**
+ * @var ThinkConsole
+ */
+ private $console;
+
+ /**
+ * @var null|string
+ */
+ private $namespace;
+
+ /**
+ * @var array
+ */
+ private $namespaces;
+
+ /**
+ * @var Command[]
+ */
+ private $commands;
+
+ /**
+ * @var Command[]
+ */
+ private $aliases;
+
+ /**
+ * 构造方法
+ * @param ThinkConsole $console
+ * @param string|null $namespace
+ */
+ public function __construct(ThinkConsole $console, $namespace = null)
+ {
+ $this->console = $console;
+ $this->namespace = $namespace;
+ }
+
+ /**
+ * @return array
+ */
+ public function getNamespaces(): array
+ {
+ if (null === $this->namespaces) {
+ $this->inspectConsole();
+ }
+
+ return $this->namespaces;
+ }
+
+ /**
+ * @return Command[]
+ */
+ public function getCommands(): array
+ {
+ if (null === $this->commands) {
+ $this->inspectConsole();
+ }
+
+ return $this->commands;
+ }
+
+ /**
+ * @param string $name
+ * @return Command
+ * @throws \InvalidArgumentException
+ */
+ public function getCommand(string $name): Command
+ {
+ if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
+ throw new \InvalidArgumentException(sprintf('Command %s does not exist.', $name));
+ }
+
+ return $this->commands[$name] ?? $this->aliases[$name];
+ }
+
+ private function inspectConsole(): void
+ {
+ $this->commands = [];
+ $this->namespaces = [];
+
+ $all = $this->console->all($this->namespace ? $this->console->findNamespace($this->namespace) : null);
+ foreach ($this->sortCommands($all) as $namespace => $commands) {
+ $names = [];
+
+ /** @var Command $command */
+ foreach ($commands as $name => $command) {
+ if (is_string($command)) {
+ $command = new $command();
+ }
+
+ if (!$command->getName()) {
+ continue;
+ }
+
+ if ($command->getName() === $name) {
+ $this->commands[$name] = $command;
+ } else {
+ $this->aliases[$name] = $command;
+ }
+
+ $names[] = $name;
+ }
+
+ $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names];
+ }
+ }
+
+ /**
+ * @param array $commands
+ * @return array
+ */
+ private function sortCommands(array $commands): array
+ {
+ $namespacedCommands = [];
+ foreach ($commands as $name => $command) {
+ $key = $this->console->extractNamespace($name, 1);
+ if (!$key) {
+ $key = self::GLOBAL_NAMESPACE;
+ }
+
+ $namespacedCommands[$key][$name] = $command;
+ }
+ ksort($namespacedCommands);
+
+ foreach ($namespacedCommands as &$commandsSet) {
+ ksort($commandsSet);
+ }
+ // unset reference to keep scope clear
+ unset($commandsSet);
+
+ return $namespacedCommands;
+ }
+}
diff --git a/src/think/console/output/driver/Buffer.php b/src/think/console/output/driver/Buffer.php
new file mode 100644
index 0000000..576f31a
--- /dev/null
+++ b/src/think/console/output/driver/Buffer.php
@@ -0,0 +1,52 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\driver;
+
+use think\console\Output;
+
+class Buffer
+{
+ /**
+ * @var string
+ */
+ private $buffer = '';
+
+ public function __construct(Output $output)
+ {
+ // do nothing
+ }
+
+ public function fetch()
+ {
+ $content = $this->buffer;
+ $this->buffer = '';
+ return $content;
+ }
+
+ public function write($messages, bool $newline = false, int $options = 0)
+ {
+ $messages = (array) $messages;
+
+ foreach ($messages as $message) {
+ $this->buffer .= $message;
+ }
+ if ($newline) {
+ $this->buffer .= "\n";
+ }
+ }
+
+ public function renderException(\Throwable $e)
+ {
+ // do nothing
+ }
+
+}
diff --git a/src/think/console/output/driver/Console.php b/src/think/console/output/driver/Console.php
new file mode 100644
index 0000000..5c0a9ef
--- /dev/null
+++ b/src/think/console/output/driver/Console.php
@@ -0,0 +1,369 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\driver;
+
+use think\console\Output;
+use think\console\output\Formatter;
+
+class Console
+{
+
+ /** @var Resource */
+ private $stdout;
+
+ /** @var Formatter */
+ private $formatter;
+
+ private $terminalDimensions;
+
+ /** @var Output */
+ private $output;
+
+ public function __construct(Output $output)
+ {
+ $this->output = $output;
+ $this->formatter = new Formatter();
+ $this->stdout = $this->openOutputStream();
+ $decorated = $this->hasColorSupport($this->stdout);
+ $this->formatter->setDecorated($decorated);
+ }
+
+ public function setDecorated($decorated)
+ {
+ $this->formatter->setDecorated($decorated);
+ }
+
+ public function write($messages, bool $newline = false, int $type = 0, $stream = null)
+ {
+ if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) {
+ return;
+ }
+
+ $messages = (array) $messages;
+
+ foreach ($messages as $message) {
+ switch ($type) {
+ case Output::OUTPUT_NORMAL:
+ $message = $this->formatter->format($message);
+ break;
+ case Output::OUTPUT_RAW:
+ break;
+ case Output::OUTPUT_PLAIN:
+ $message = strip_tags($this->formatter->format($message));
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type));
+ }
+
+ $this->doWrite($message, $newline, $stream);
+ }
+ }
+
+ public function renderException(\Throwable $e)
+ {
+ $stderr = $this->openErrorStream();
+ $decorated = $this->hasColorSupport($stderr);
+ $this->formatter->setDecorated($decorated);
+
+ do {
+ $title = sprintf(' [%s] ', $e::class);
+
+ $len = $this->stringWidth($title);
+
+ $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX;
+
+ if (defined('HHVM_VERSION') && $width > 1 << 31) {
+ $width = 1 << 31;
+ }
+ $lines = [];
+ foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) {
+ foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
+
+ $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4;
+ $lines[] = [$line, $lineLength];
+
+ $len = max($lineLength, $len);
+ }
+ }
+
+ $messages = ['', ''];
+ $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len));
+ $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title))));
+ foreach ($lines as $line) {
+ $messages[] = sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1]));
+ }
+ $messages[] = $emptyLine;
+ $messages[] = '';
+ $messages[] = '';
+
+ $this->write($messages, true, Output::OUTPUT_NORMAL, $stderr);
+
+ if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) {
+ $this->write('Exception trace:', true, Output::OUTPUT_NORMAL, $stderr);
+
+ // exception related properties
+ $trace = $e->getTrace();
+ array_unshift($trace, [
+ 'function' => '',
+ 'file' => $e->getFile() !== null ? $e->getFile() : 'n/a',
+ 'line' => $e->getLine() !== null ? $e->getLine() : 'n/a',
+ 'args' => [],
+ ]);
+
+ for ($i = 0, $count = count($trace); $i < $count; ++$i) {
+ $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
+ $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
+ $function = $trace[$i]['function'];
+ $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
+ $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
+
+ $this->write(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr);
+ }
+
+ $this->write('', true, Output::OUTPUT_NORMAL, $stderr);
+ $this->write('', true, Output::OUTPUT_NORMAL, $stderr);
+ }
+ } while ($e = $e->getPrevious());
+
+ }
+
+ /**
+ * 获取终端宽度
+ * @return int|null
+ */
+ protected function getTerminalWidth()
+ {
+ $dimensions = $this->getTerminalDimensions();
+
+ return $dimensions[0];
+ }
+
+ /**
+ * 获取终端高度
+ * @return int|null
+ */
+ protected function getTerminalHeight()
+ {
+ $dimensions = $this->getTerminalDimensions();
+
+ return $dimensions[1];
+ }
+
+ /**
+ * 获取当前终端的尺寸
+ * @return array
+ */
+ public function getTerminalDimensions(): array
+ {
+ if ($this->terminalDimensions) {
+ return $this->terminalDimensions;
+ }
+
+ if ('\\' === DIRECTORY_SEPARATOR) {
+ if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) {
+ return [(int) $matches[1], (int) $matches[2]];
+ }
+ if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) {
+ return [(int) $matches[1], (int) $matches[2]];
+ }
+ }
+
+ if ($sttyString = $this->getSttyColumns()) {
+ if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
+ return [(int) $matches[2], (int) $matches[1]];
+ }
+ if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
+ return [(int) $matches[2], (int) $matches[1]];
+ }
+ }
+
+ return [null, null];
+ }
+
+ /**
+ * 获取stty列数
+ * @return string
+ */
+ private function getSttyColumns()
+ {
+ if (!function_exists('proc_open')) {
+ return;
+ }
+
+ $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
+ $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
+ if (is_resource($process)) {
+ $info = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ proc_close($process);
+
+ return $info;
+ }
+ return;
+ }
+
+ /**
+ * 获取终端模式
+ * @return string x
+ */
+ private function getMode()
+ {
+ if (!function_exists('proc_open')) {
+ return '';
+ }
+
+ $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
+ $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
+ if (is_resource($process)) {
+ $info = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ proc_close($process);
+
+ if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
+ return $matches[2] . 'x' . $matches[1];
+ }
+ }
+
+ return '';
+ }
+
+ private function stringWidth(string $string): int
+ {
+ if (!function_exists('mb_strwidth')) {
+ return strlen($string);
+ }
+
+ if (false === $encoding = mb_detect_encoding($string)) {
+ return strlen($string);
+ }
+
+ return mb_strwidth($string, $encoding);
+ }
+
+ private function splitStringByWidth(string $string, int $width): array
+ {
+ if (!function_exists('mb_strwidth')) {
+ return str_split($string, $width);
+ }
+
+ if (false === $encoding = mb_detect_encoding($string)) {
+ return str_split($string, $width);
+ }
+
+ $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
+ $lines = [];
+ $line = '';
+ foreach (preg_split('//u', $utf8String) as $char) {
+ if (mb_strwidth($line . $char, 'utf8') <= $width) {
+ $line .= $char;
+ continue;
+ }
+ $lines[] = str_pad($line, $width);
+ $line = $char;
+ }
+ if (strlen($line)) {
+ $lines[] = count($lines) ? str_pad($line, $width) : $line;
+ }
+
+ mb_convert_variables($encoding, 'utf8', $lines);
+
+ return $lines;
+ }
+
+ private function isRunningOS400(): bool
+ {
+ $checks = [
+ function_exists('php_uname') ? php_uname('s') : '',
+ getenv('OSTYPE'),
+ PHP_OS,
+ ];
+ return false !== stripos(implode(';', $checks), 'OS400');
+ }
+
+ /**
+ * 当前环境是否支持写入控制台输出到stdout.
+ *
+ * @return bool
+ */
+ protected function hasStdoutSupport(): bool
+ {
+ return false === $this->isRunningOS400();
+ }
+
+ /**
+ * 当前环境是否支持写入控制台输出到stderr.
+ *
+ * @return bool
+ */
+ protected function hasStderrSupport(): bool
+ {
+ return false === $this->isRunningOS400();
+ }
+
+ /**
+ * @return resource
+ */
+ private function openOutputStream()
+ {
+ if (!$this->hasStdoutSupport()) {
+ return fopen('php://output', 'w');
+ }
+ return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
+ }
+
+ /**
+ * @return resource
+ */
+ private function openErrorStream()
+ {
+ return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w');
+ }
+
+ /**
+ * 将消息写入到输出。
+ * @param string $message 消息
+ * @param bool $newline 是否另起一行
+ * @param null $stream
+ */
+ protected function doWrite($message, $newline, $stream = null)
+ {
+ if (null === $stream) {
+ $stream = $this->stdout;
+ }
+ if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) {
+ throw new \RuntimeException('Unable to write output.');
+ }
+
+ fflush($stream);
+ }
+
+ /**
+ * 是否支持着色
+ * @param $stream
+ * @return bool
+ */
+ protected function hasColorSupport($stream): bool
+ {
+ if (DIRECTORY_SEPARATOR === '\\') {
+ return
+ '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD
+ || false !== getenv('ANSICON')
+ || 'ON' === getenv('ConEmuANSI')
+ || 'xterm' === getenv('TERM');
+ }
+
+ return function_exists('posix_isatty') && @posix_isatty($stream);
+ }
+
+}
diff --git a/src/think/console/output/driver/Nothing.php b/src/think/console/output/driver/Nothing.php
new file mode 100644
index 0000000..a7cc49e
--- /dev/null
+++ b/src/think/console/output/driver/Nothing.php
@@ -0,0 +1,33 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\driver;
+
+use think\console\Output;
+
+class Nothing
+{
+
+ public function __construct(Output $output)
+ {
+ // do nothing
+ }
+
+ public function write($messages, bool $newline = false, int $options = 0)
+ {
+ // do nothing
+ }
+
+ public function renderException(\Throwable $e)
+ {
+ // do nothing
+ }
+}
diff --git a/src/think/console/output/formatter/Stack.php b/src/think/console/output/formatter/Stack.php
new file mode 100644
index 0000000..8a212ba
--- /dev/null
+++ b/src/think/console/output/formatter/Stack.php
@@ -0,0 +1,116 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\formatter;
+
+class Stack
+{
+
+ /**
+ * @var Style[]
+ */
+ private $styles;
+
+ /**
+ * @var Style
+ */
+ private $emptyStyle;
+
+ /**
+ * 构造方法
+ * @param Style|null $emptyStyle
+ */
+ public function __construct(?Style $emptyStyle = null)
+ {
+ $this->emptyStyle = $emptyStyle ?: new Style();
+ $this->reset();
+ }
+
+ /**
+ * 重置堆栈
+ */
+ public function reset(): void
+ {
+ $this->styles = [];
+ }
+
+ /**
+ * 推一个样式进入堆栈
+ * @param Style $style
+ */
+ public function push(Style $style): void
+ {
+ $this->styles[] = $style;
+ }
+
+ /**
+ * 从堆栈中弹出一个样式
+ * @param Style|null $style
+ * @return Style
+ * @throws \InvalidArgumentException
+ */
+ public function pop(?Style $style = null): Style
+ {
+ if (empty($this->styles)) {
+ return $this->emptyStyle;
+ }
+
+ if (null === $style) {
+ return array_pop($this->styles);
+ }
+
+ /**
+ * @var int $index
+ * @var Style $stackedStyle
+ */
+ foreach (array_reverse($this->styles, true) as $index => $stackedStyle) {
+ if ($style->apply('') === $stackedStyle->apply('')) {
+ $this->styles = array_slice($this->styles, 0, $index);
+
+ return $stackedStyle;
+ }
+ }
+
+ throw new \InvalidArgumentException('Incorrectly nested style tag found.');
+ }
+
+ /**
+ * 计算堆栈的当前样式。
+ * @return Style
+ */
+ public function getCurrent(): Style
+ {
+ if (empty($this->styles)) {
+ return $this->emptyStyle;
+ }
+
+ return $this->styles[count($this->styles) - 1];
+ }
+
+ /**
+ * @param Style $emptyStyle
+ * @return Stack
+ */
+ public function setEmptyStyle(Style $emptyStyle)
+ {
+ $this->emptyStyle = $emptyStyle;
+
+ return $this;
+ }
+
+ /**
+ * @return Style
+ */
+ public function getEmptyStyle(): Style
+ {
+ return $this->emptyStyle;
+ }
+}
diff --git a/src/think/console/output/formatter/Style.php b/src/think/console/output/formatter/Style.php
new file mode 100644
index 0000000..2aae768
--- /dev/null
+++ b/src/think/console/output/formatter/Style.php
@@ -0,0 +1,190 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\formatter;
+
+class Style
+{
+ protected static $availableForegroundColors = [
+ 'black' => ['set' => 30, 'unset' => 39],
+ 'red' => ['set' => 31, 'unset' => 39],
+ 'green' => ['set' => 32, 'unset' => 39],
+ 'yellow' => ['set' => 33, 'unset' => 39],
+ 'blue' => ['set' => 34, 'unset' => 39],
+ 'magenta' => ['set' => 35, 'unset' => 39],
+ 'cyan' => ['set' => 36, 'unset' => 39],
+ 'white' => ['set' => 37, 'unset' => 39],
+ ];
+
+ protected static $availableBackgroundColors = [
+ 'black' => ['set' => 40, 'unset' => 49],
+ 'red' => ['set' => 41, 'unset' => 49],
+ 'green' => ['set' => 42, 'unset' => 49],
+ 'yellow' => ['set' => 43, 'unset' => 49],
+ 'blue' => ['set' => 44, 'unset' => 49],
+ 'magenta' => ['set' => 45, 'unset' => 49],
+ 'cyan' => ['set' => 46, 'unset' => 49],
+ 'white' => ['set' => 47, 'unset' => 49],
+ ];
+
+ protected static $availableOptions = [
+ 'bold' => ['set' => 1, 'unset' => 22],
+ 'underscore' => ['set' => 4, 'unset' => 24],
+ 'blink' => ['set' => 5, 'unset' => 25],
+ 'reverse' => ['set' => 7, 'unset' => 27],
+ 'conceal' => ['set' => 8, 'unset' => 28],
+ ];
+
+ private $foreground;
+ private $background;
+ private $options = [];
+
+ /**
+ * 初始化输出的样式
+ * @param string|null $foreground 字体颜色
+ * @param string|null $background 背景色
+ * @param array $options 格式
+ * @api
+ */
+ public function __construct($foreground = null, $background = null, array $options = [])
+ {
+ if (null !== $foreground) {
+ $this->setForeground($foreground);
+ }
+ if (null !== $background) {
+ $this->setBackground($background);
+ }
+ if (count($options)) {
+ $this->setOptions($options);
+ }
+ }
+
+ /**
+ * 设置字体颜色
+ * @param string|null $color 颜色名
+ * @throws \InvalidArgumentException
+ * @api
+ */
+ public function setForeground($color = null)
+ {
+ if (null === $color) {
+ $this->foreground = null;
+
+ return;
+ }
+
+ if (!isset(static::$availableForegroundColors[$color])) {
+ throw new \InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors))));
+ }
+
+ $this->foreground = static::$availableForegroundColors[$color];
+ }
+
+ /**
+ * 设置背景色
+ * @param string|null $color 颜色名
+ * @throws \InvalidArgumentException
+ * @api
+ */
+ public function setBackground($color = null)
+ {
+ if (null === $color) {
+ $this->background = null;
+
+ return;
+ }
+
+ if (!isset(static::$availableBackgroundColors[$color])) {
+ throw new \InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors))));
+ }
+
+ $this->background = static::$availableBackgroundColors[$color];
+ }
+
+ /**
+ * 设置字体格式
+ * @param string $option 格式名
+ * @throws \InvalidArgumentException When the option name isn't defined
+ * @api
+ */
+ public function setOption(string $option): void
+ {
+ if (!isset(static::$availableOptions[$option])) {
+ throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
+ }
+
+ if (!in_array(static::$availableOptions[$option], $this->options)) {
+ $this->options[] = static::$availableOptions[$option];
+ }
+ }
+
+ /**
+ * 重置字体格式
+ * @param string $option 格式名
+ * @throws \InvalidArgumentException
+ */
+ public function unsetOption(string $option): void
+ {
+ if (!isset(static::$availableOptions[$option])) {
+ throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
+ }
+
+ $pos = array_search(static::$availableOptions[$option], $this->options);
+ if (false !== $pos) {
+ unset($this->options[$pos]);
+ }
+ }
+
+ /**
+ * 批量设置字体格式
+ * @param array $options
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = [];
+
+ foreach ($options as $option) {
+ $this->setOption($option);
+ }
+ }
+
+ /**
+ * 应用样式到文字
+ * @param string $text 文字
+ * @return string
+ */
+ public function apply(string $text): string
+ {
+ $setCodes = [];
+ $unsetCodes = [];
+
+ if (null !== $this->foreground) {
+ $setCodes[] = $this->foreground['set'];
+ $unsetCodes[] = $this->foreground['unset'];
+ }
+ if (null !== $this->background) {
+ $setCodes[] = $this->background['set'];
+ $unsetCodes[] = $this->background['unset'];
+ }
+ if (count($this->options)) {
+ foreach ($this->options as $option) {
+ $setCodes[] = $option['set'];
+ $unsetCodes[] = $option['unset'];
+ }
+ }
+
+ if (0 === count($setCodes)) {
+ return $text;
+ }
+
+ return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes));
+ }
+}
diff --git a/src/think/console/output/question/Choice.php b/src/think/console/output/question/Choice.php
new file mode 100644
index 0000000..1da1750
--- /dev/null
+++ b/src/think/console/output/question/Choice.php
@@ -0,0 +1,163 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\question;
+
+use think\console\output\Question;
+
+class Choice extends Question
+{
+
+ private $choices;
+ private $multiselect = false;
+ private $prompt = ' > ';
+ private $errorMessage = 'Value "%s" is invalid';
+
+ /**
+ * 构造方法
+ * @param string $question 问题
+ * @param array $choices 选项
+ * @param mixed $default 默认答案
+ */
+ public function __construct($question, array $choices, $default = null)
+ {
+ parent::__construct($question, $default);
+
+ $this->choices = $choices;
+ $this->setValidator($this->getDefaultValidator());
+ $this->setAutocompleterValues($choices);
+ }
+
+ /**
+ * 可选项
+ * @return array
+ */
+ public function getChoices(): array
+ {
+ return $this->choices;
+ }
+
+ /**
+ * 设置可否多选
+ * @param bool $multiselect
+ * @return self
+ */
+ public function setMultiselect(bool $multiselect)
+ {
+ $this->multiselect = $multiselect;
+ $this->setValidator($this->getDefaultValidator());
+
+ return $this;
+ }
+
+ public function isMultiselect(): bool
+ {
+ return $this->multiselect;
+ }
+
+ /**
+ * 获取提示
+ * @return string
+ */
+ public function getPrompt(): string
+ {
+ return $this->prompt;
+ }
+
+ /**
+ * 设置提示
+ * @param string $prompt
+ * @return self
+ */
+ public function setPrompt(string $prompt)
+ {
+ $this->prompt = $prompt;
+
+ return $this;
+ }
+
+ /**
+ * 设置错误提示信息
+ * @param string $errorMessage
+ * @return self
+ */
+ public function setErrorMessage(string $errorMessage)
+ {
+ $this->errorMessage = $errorMessage;
+ $this->setValidator($this->getDefaultValidator());
+
+ return $this;
+ }
+
+ /**
+ * 获取默认的验证方法
+ * @return callable
+ */
+ private function getDefaultValidator()
+ {
+ $choices = $this->choices;
+ $errorMessage = $this->errorMessage;
+ $multiselect = $this->multiselect;
+ $isAssoc = $this->isAssoc($choices);
+
+ return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) {
+ // Collapse all spaces.
+ $selectedChoices = str_replace(' ', '', $selected);
+
+ if ($multiselect) {
+ // Check for a separated comma values
+ if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) {
+ throw new \InvalidArgumentException(sprintf($errorMessage, $selected));
+ }
+ $selectedChoices = explode(',', $selectedChoices);
+ } else {
+ $selectedChoices = [$selected];
+ }
+
+ $multiselectChoices = [];
+ foreach ($selectedChoices as $value) {
+ $results = [];
+ foreach ($choices as $key => $choice) {
+ if ($choice === $value) {
+ $results[] = $key;
+ }
+ }
+
+ if (count($results) > 1) {
+ throw new \InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results)));
+ }
+
+ $result = array_search($value, $choices);
+
+ if (!$isAssoc) {
+ if (!empty($result)) {
+ $result = $choices[$result];
+ } elseif (isset($choices[$value])) {
+ $result = $choices[$value];
+ }
+ } elseif (empty($result) && array_key_exists($value, $choices)) {
+ $result = $value;
+ }
+
+ if (false === $result) {
+ throw new \InvalidArgumentException(sprintf($errorMessage, $value));
+ }
+ array_push($multiselectChoices, $result);
+ }
+
+ if ($multiselect) {
+ return $multiselectChoices;
+ }
+
+ return current($multiselectChoices);
+ };
+ }
+}
diff --git a/src/think/console/output/question/Confirmation.php b/src/think/console/output/question/Confirmation.php
new file mode 100644
index 0000000..bf71b5d
--- /dev/null
+++ b/src/think/console/output/question/Confirmation.php
@@ -0,0 +1,57 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\console\output\question;
+
+use think\console\output\Question;
+
+class Confirmation extends Question
+{
+
+ private $trueAnswerRegex;
+
+ /**
+ * 构造方法
+ * @param string $question 问题
+ * @param bool $default 默认答案
+ * @param string $trueAnswerRegex 验证正则
+ */
+ public function __construct(string $question, bool $default = true, string $trueAnswerRegex = '/^y/i')
+ {
+ parent::__construct($question, (bool) $default);
+
+ $this->trueAnswerRegex = $trueAnswerRegex;
+ $this->setNormalizer($this->getDefaultNormalizer());
+ }
+
+ /**
+ * 获取默认的答案回调
+ * @return callable
+ */
+ private function getDefaultNormalizer()
+ {
+ $default = $this->getDefault();
+ $regex = $this->trueAnswerRegex;
+
+ return function ($answer) use ($default, $regex) {
+ if (is_bool($answer)) {
+ return $answer;
+ }
+
+ $answerIsTrue = (bool) preg_match($regex, $answer);
+ if (false === $default) {
+ return $answer && $answerIsTrue;
+ }
+
+ return !$answer || $answerIsTrue;
+ };
+ }
+}
diff --git a/src/think/contract/CacheHandlerInterface.php b/src/think/contract/CacheHandlerInterface.php
new file mode 100644
index 0000000..aa09d47
--- /dev/null
+++ b/src/think/contract/CacheHandlerInterface.php
@@ -0,0 +1,71 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types = 1);
+
+namespace think\contract;
+
+use DateInterval;
+use DateTimeInterface;
+use Psr\SimpleCache\CacheInterface;
+use think\cache\TagSet;
+
+/**
+ * 缓存驱动接口
+ */
+interface CacheHandlerInterface extends CacheInterface
+{
+
+ /**
+ * 自增缓存(针对数值缓存)
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function inc($name, $step = 1);
+
+ /**
+ * 自减缓存(针对数值缓存)
+ * @param string $name 缓存变量名
+ * @param int $step 步长
+ * @return false|int
+ */
+ public function dec($name, $step = 1);
+
+ /**
+ * 读取缓存并删除
+ * @param string $name 缓存变量名
+ * @return mixed
+ */
+ public function pull($name);
+
+ /**
+ * 如果不存在则写入缓存
+ * @param string $name 缓存变量名
+ * @param mixed $value 存储数据
+ * @param int|DateInterval|DateTimeInterface $expire 有效时间 0为永久
+ * @return mixed
+ */
+ public function remember($name, $value, $expire = null);
+
+ /**
+ * 缓存标签
+ * @param string|array $name 标签名
+ * @return TagSet
+ */
+ public function tag($name);
+
+ /**
+ * 删除缓存标签
+ * @param array $keys 缓存标识列表
+ * @return void
+ */
+ public function clearTag($keys);
+}
diff --git a/src/think/contract/LogHandlerInterface.php b/src/think/contract/LogHandlerInterface.php
new file mode 100644
index 0000000..161fb93
--- /dev/null
+++ b/src/think/contract/LogHandlerInterface.php
@@ -0,0 +1,28 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\contract;
+
+/**
+ * 日志驱动接口
+ */
+interface LogHandlerInterface
+{
+ /**
+ * 日志写入接口
+ * @access public
+ * @param array $log 日志信息
+ * @return bool
+ */
+ public function save(array $log): bool;
+
+}
diff --git a/src/think/contract/ModelRelationInterface.php b/src/think/contract/ModelRelationInterface.php
new file mode 100644
index 0000000..66aa204
--- /dev/null
+++ b/src/think/contract/ModelRelationInterface.php
@@ -0,0 +1,98 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\contract;
+
+use Closure;
+use think\db\BaseQuery as Query;
+use think\Model;
+
+/**
+ * 模型关联接口
+ */
+interface ModelRelationInterface
+{
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param array $subRelation 子关联
+ * @param Closure $closure 闭包查询条件
+ * @return mixed
+ */
+ public function getRelation(array $subRelation = [], ?Closure $closure = null);
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包条件
+ * @return void
+ */
+ public function eagerlyResultSet(array &$resultSet, string $relation, array $subRelation, ?Closure $closure = null): void;
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param array $subRelation 子关联名
+ * @param Closure $closure 闭包条件
+ * @return void
+ */
+ public function eagerlyResult(Model $result, string $relation, array $subRelation = [], ?Closure $closure = null): void;
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 模型对象
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return integer
+ */
+ public function relationCount(Model $result, Closure $closure, string $aggregate = 'count', string $field = '*', ?string &$name = null);
+
+ /**
+ * 创建关联统计子查询
+ * @access public
+ * @param Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @param string $name 统计字段别名
+ * @return string
+ */
+ public function getRelationCountQuery(?Closure $closure = null, string $aggregate = 'count', string $field = '*', ?string &$name = null): string;
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has(string $operator = '>=', int $count = 1, string $id = '*', string $joinType = 'INNER', ?Query $query = null): Query;
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null, string $joinType = ''): Query;
+}
diff --git a/src/think/contract/SessionHandlerInterface.php b/src/think/contract/SessionHandlerInterface.php
new file mode 100644
index 0000000..6e5d044
--- /dev/null
+++ b/src/think/contract/SessionHandlerInterface.php
@@ -0,0 +1,23 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\contract;
+
+/**
+ * Session驱动接口
+ */
+interface SessionHandlerInterface
+{
+ public function read(string $sessionId): string;
+ public function delete(string $sessionId): bool;
+ public function write(string $sessionId, string $data): bool;
+}
diff --git a/src/think/contract/TemplateHandlerInterface.php b/src/think/contract/TemplateHandlerInterface.php
new file mode 100644
index 0000000..7ab6cfa
--- /dev/null
+++ b/src/think/contract/TemplateHandlerInterface.php
@@ -0,0 +1,56 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\contract;
+
+/**
+ * 视图驱动接口
+ */
+interface TemplateHandlerInterface
+{
+ /**
+ * 检测是否存在模板文件
+ * @param string $template 模板文件或者模板规则
+ * @return bool
+ */
+ public function exists(string $template): bool;
+
+ /**
+ * 渲染模板文件
+ * @param string $template 模板文件
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function fetch(string $template, array $data = []): void;
+
+ /**
+ * 渲染模板内容
+ * @param string $content 模板内容
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function display(string $content, array $data = []): void;
+
+ /**
+ * 配置模板引擎
+ * @param array $config 参数
+ * @return void
+ */
+ public function config(array $config): void;
+
+ /**
+ * 获取模板引擎配置
+ * @param string $name 参数名
+ * @return mixed
+ */
+ public function getConfig(string $name);
+}
diff --git a/src/think/event/AppInit.php b/src/think/event/AppInit.php
new file mode 100644
index 0000000..3e49c42
--- /dev/null
+++ b/src/think/event/AppInit.php
@@ -0,0 +1,20 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\event;
+
+/**
+ * AppInit事件类
+ */
+class AppInit
+{
+}
diff --git a/src/think/event/HttpEnd.php b/src/think/event/HttpEnd.php
new file mode 100644
index 0000000..935bf6b
--- /dev/null
+++ b/src/think/event/HttpEnd.php
@@ -0,0 +1,20 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\event;
+
+/**
+ * HttpEnd事件类
+ */
+class HttpEnd
+{
+}
diff --git a/src/think/event/HttpRun.php b/src/think/event/HttpRun.php
new file mode 100644
index 0000000..e9aa82a
--- /dev/null
+++ b/src/think/event/HttpRun.php
@@ -0,0 +1,20 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\event;
+
+/**
+ * HttpRun事件类
+ */
+class HttpRun
+{
+}
diff --git a/src/think/event/LogRecord.php b/src/think/event/LogRecord.php
new file mode 100644
index 0000000..51a15a4
--- /dev/null
+++ b/src/think/event/LogRecord.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+namespace think\event;
+
+use DateTimeImmutable;
+
+/**
+ * LogRecord事件类
+ */
+class LogRecord
+{
+ /** @var string */
+ public string $type;
+
+ /** @var string|array */
+ public $message;
+
+ /** @var DateTimeImmutable */
+ public DateTimeImmutable $time;
+
+ public function __construct($type, $message)
+ {
+ $this->type = $type;
+ $this->message = $message;
+ $this->time = new DateTimeImmutable();
+ }
+}
diff --git a/src/think/event/LogWrite.php b/src/think/event/LogWrite.php
new file mode 100644
index 0000000..a9c4fd5
--- /dev/null
+++ b/src/think/event/LogWrite.php
@@ -0,0 +1,23 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\event;
+
+/**
+ * LogWrite事件类
+ */
+class LogWrite
+{
+ public function __construct(public string $channel, public array $log)
+ {
+ }
+}
diff --git a/src/think/event/RouteLoaded.php b/src/think/event/RouteLoaded.php
new file mode 100644
index 0000000..e40b980
--- /dev/null
+++ b/src/think/event/RouteLoaded.php
@@ -0,0 +1,20 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\event;
+
+/**
+ * 路由加载完成事件
+ */
+class RouteLoaded
+{
+}
diff --git a/src/think/exception/ErrorException.php b/src/think/exception/ErrorException.php
new file mode 100644
index 0000000..faf1038
--- /dev/null
+++ b/src/think/exception/ErrorException.php
@@ -0,0 +1,57 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\exception;
+
+use think\Exception;
+
+/**
+ * ThinkPHP错误异常
+ * 主要用于封装 set_error_handler 和 register_shutdown_function 得到的错误
+ * 除开从 think\Exception 继承的功能
+ * 其他和PHP系统\ErrorException功能基本一样
+ */
+class ErrorException extends Exception
+{
+ /**
+ * 用于保存错误级别
+ * @var integer
+ */
+ protected $severity;
+
+ /**
+ * 错误异常构造函数
+ * @access public
+ * @param integer $severity 错误级别
+ * @param string $message 错误详细信息
+ * @param string $file 出错文件路径
+ * @param integer $line 出错行号
+ */
+ public function __construct(int $severity, string $message, string $file, int $line)
+ {
+ $this->severity = $severity;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ $this->code = 0;
+ }
+
+ /**
+ * 获取错误级别
+ * @access public
+ * @return integer 错误级别
+ */
+ final public function getSeverity()
+ {
+ return $this->severity;
+ }
+}
diff --git a/src/think/exception/FileException.php b/src/think/exception/FileException.php
new file mode 100644
index 0000000..fdcbcd3
--- /dev/null
+++ b/src/think/exception/FileException.php
@@ -0,0 +1,17 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\exception;
+
+class FileException extends \RuntimeException
+{
+}
diff --git a/src/think/exception/Handle.php b/src/think/exception/Handle.php
new file mode 100644
index 0000000..c9db836
--- /dev/null
+++ b/src/think/exception/Handle.php
@@ -0,0 +1,371 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+use Exception;
+use think\App;
+use think\console\Output;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\ModelNotFoundException;
+use think\Request;
+use think\Response;
+use Throwable;
+
+/**
+ * 系统异常处理类
+ */
+class Handle
+{
+ protected $ignoreReport = [
+ HttpException::class,
+ HttpResponseException::class,
+ ModelNotFoundException::class,
+ DataNotFoundException::class,
+ ValidateException::class,
+ ];
+
+ protected $showErrorMsg = [
+
+ ];
+
+ public function __construct(protected App $app)
+ {
+ }
+
+ /**
+ * Report or log an exception.
+ *
+ * @access public
+ * @param Throwable $exception
+ * @return void
+ */
+ public function report(Throwable $exception): void
+ {
+ if (!$this->isIgnoreReport($exception)) {
+ // 收集异常数据
+ if ($this->app->isDebug()) {
+ $data = [
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ 'message' => $this->getMessage($exception),
+ 'code' => $this->getCode($exception),
+ ];
+ $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]";
+ } else {
+ $data = [
+ 'code' => $this->getCode($exception),
+ 'message' => $this->getMessage($exception),
+ ];
+ $log = "[{$data['code']}]{$data['message']}";
+ }
+
+ if ($this->app->config->get('log.record_trace')) {
+ $log .= PHP_EOL . $exception->getTraceAsString();
+ }
+
+ try {
+ $this->app->log->record($log, 'error');
+ } catch (Exception $e) {
+ }
+ }
+ }
+
+ protected function isIgnoreReport(Throwable $exception): bool
+ {
+ foreach ($this->ignoreReport as $class) {
+ if ($exception instanceof $class) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Render an exception into an HTTP response.
+ *
+ * @access public
+ * @param Request $request
+ * @param Throwable $e
+ * @return Response
+ */
+ public function render(Request $request, Throwable $e): Response
+ {
+ if ($e instanceof HttpResponseException) {
+ return $e->getResponse();
+ } elseif ($e instanceof HttpException) {
+ return $this->renderHttpException($request, $e);
+ } else {
+ return $this->convertExceptionToResponse($request, $e);
+ }
+ }
+
+ /**
+ * @access public
+ * @param Output $output
+ * @param Throwable $e
+ */
+ public function renderForConsole(Output $output, Throwable $e): void
+ {
+ if ($this->app->isDebug()) {
+ $output->setVerbosity(Output::VERBOSITY_DEBUG);
+ }
+
+ $output->renderException($e);
+ }
+
+ /**
+ * @access protected
+ * @param HttpException $e
+ * @return Response
+ */
+ protected function renderHttpException(Request $request, HttpException $e): Response
+ {
+ $status = $e->getStatusCode();
+ $template = $this->app->config->get('app.http_exception_template');
+
+ if (!$this->app->isDebug() && !empty($template[$status])) {
+ return Response::create($template[$status], 'view', $status)->assign(['e' => $e]);
+ } else {
+ return $this->convertExceptionToResponse($request, $e);
+ }
+ }
+
+ /**
+ * 收集异常数据
+ * @param Throwable $exception
+ * @return array
+ */
+ protected function convertExceptionToArray(Throwable $exception): array
+ {
+ return $this->app->isDebug() ? $this->getDebugMsg($exception) : $this->getDeployMsg($exception);
+ }
+
+ /**
+ * 是否显示错误信息
+ * @param \Throwable $exception
+ * @return bool
+ */
+ protected function isShowErrorMsg(Throwable $exception)
+ {
+ foreach ($this->showErrorMsg as $class) {
+ if ($exception instanceof $class) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取部署模式异常数据
+ * @access protected
+ * @param Throwable $exception
+ * @return array
+ */
+ protected function getDeployMsg(Throwable $exception): array
+ {
+ $showErrorMsg = $this->isShowErrorMsg($exception);
+ if ($showErrorMsg || $this->app->config->get('app.show_error_msg', false)) {
+ $message = $this->getMessage($exception);
+ } else {
+ // 不显示详细错误信息
+ $message = $this->app->config->get('app.error_message');
+ }
+
+ return [
+ 'code' => $this->getCode($exception),
+ 'message' => $message,
+ ];
+ }
+
+ /**
+ * 收集调试模式异常数据
+ * @access protected
+ * @param Throwable $exception
+ * @return array
+ */
+ protected function getDebugMsg(Throwable $exception): array
+ {
+ // 调试模式,获取详细的错误信息
+ $traces = [];
+ $nextException = $exception;
+
+ do {
+ $traces[] = [
+ 'name' => $nextException::class,
+ 'file' => $nextException->getFile(),
+ 'line' => $nextException->getLine(),
+ 'code' => $this->getCode($nextException),
+ 'message' => $this->getMessage($nextException),
+ 'trace' => $nextException->getTrace(),
+ 'source' => $this->getSourceCode($nextException),
+ ];
+ } while ($nextException = $nextException->getPrevious());
+
+ return [
+ 'code' => $this->getCode($exception),
+ 'message' => $this->getMessage($exception),
+ 'traces' => $traces,
+ 'datas' => $this->getExtendData($exception),
+ 'tables' => [
+ 'GET Data' => $this->app->request->get(),
+ 'POST Data' => $this->app->request->post(),
+ 'Files' => $this->app->request->file(),
+ 'Cookies' => $this->app->request->cookie(),
+ 'Session' => $this->app->exists('session') ? $this->app->session->all() : [],
+ 'Server/Request Data' => $this->app->request->server(),
+ ],
+ ];
+ }
+
+ protected function isJson(Request $request)
+ {
+ return $request->isJson();
+ }
+
+ /**
+ * @access protected
+ * @param Throwable $exception
+ * @return Response
+ */
+ protected function convertExceptionToResponse(Request $request, Throwable $exception): Response
+ {
+ if ($this->isJson($request)) {
+ $response = Response::create($this->convertExceptionToArray($exception), 'json');
+ } else {
+ $response = Response::create($this->renderExceptionContent($exception));
+ }
+
+ if ($exception instanceof HttpException) {
+ $statusCode = $exception->getStatusCode();
+ $response->header($exception->getHeaders());
+ }
+
+ return $response->code($statusCode ?? 500);
+ }
+
+ protected function renderExceptionContent(Throwable $exception): string
+ {
+ ob_start();
+ $data = $this->convertExceptionToArray($exception);
+ extract($data);
+ include $this->app->config->get('app.exception_tmpl') ?: __DIR__ . '/../../tpl/think_exception.tpl';
+
+ return ob_get_clean();
+ }
+
+ /**
+ * 获取错误编码
+ * ErrorException则使用错误级别作为错误编码
+ * @access protected
+ * @param Throwable $exception
+ * @return integer 错误编码
+ */
+ protected function getCode(Throwable $exception)
+ {
+ $code = $exception->getCode();
+
+ if (!$code && $exception instanceof ErrorException) {
+ $code = $exception->getSeverity();
+ }
+
+ return $code;
+ }
+
+ /**
+ * 获取错误信息
+ * ErrorException则使用错误级别作为错误编码
+ * @access protected
+ * @param Throwable $exception
+ * @return string 错误信息
+ */
+ protected function getMessage(Throwable $exception): string
+ {
+ $message = $exception->getMessage();
+
+ if ($this->app->runningInConsole()) {
+ return $message;
+ }
+
+ $lang = $this->app->lang;
+
+ if (str_contains($message, ':')) {
+ $name = strstr($message, ':', true);
+ $message = $lang->has($name) ? $lang->get($name) . strstr($message, ':') : $message;
+ } elseif (str_contains($message, ',')) {
+ $name = strstr($message, ',', true);
+ $message = $lang->has($name) ? $lang->get($name) . ':' . substr(strstr($message, ','), 1) : $message;
+ } elseif ($lang->has($message)) {
+ $message = $lang->get($message);
+ }
+
+ return $message;
+ }
+
+ /**
+ * 获取出错文件内容
+ * 获取错误的前9行和后9行
+ * @access protected
+ * @param Throwable $exception
+ * @return array 错误文件内容
+ */
+ protected function getSourceCode(Throwable $exception): array
+ {
+ // 读取前9行和后9行
+ $line = $exception->getLine();
+ $first = ($line - 9 > 0) ? $line - 9 : 1;
+
+ try {
+ $contents = file($exception->getFile()) ?: [];
+ $source = [
+ 'first' => $first,
+ 'source' => array_slice($contents, $first - 1, 19),
+ ];
+ } catch (Exception $e) {
+ $source = [];
+ }
+
+ return $source;
+ }
+
+ /**
+ * 获取异常扩展信息
+ * 用于非调试模式html返回类型显示
+ * @access protected
+ * @param Throwable $exception
+ * @return array 异常类定义的扩展数据
+ */
+ protected function getExtendData(Throwable $exception): array
+ {
+ $data = [];
+
+ if ($exception instanceof \think\Exception) {
+ $data = $exception->getData();
+ }
+
+ return $data;
+ }
+
+ /**
+ * 获取常量列表
+ * @access protected
+ * @return array 常量列表
+ */
+ protected function getConst(): array
+ {
+ $const = get_defined_constants(true);
+
+ return $const['user'] ?? [];
+ }
+}
diff --git a/src/think/exception/HttpException.php b/src/think/exception/HttpException.php
new file mode 100644
index 0000000..66597e4
--- /dev/null
+++ b/src/think/exception/HttpException.php
@@ -0,0 +1,36 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+use Exception;
+
+/**
+ * HTTP异常
+ */
+class HttpException extends \RuntimeException
+{
+ public function __construct(private int $statusCode, string $message = '', ?Exception $previous = null, private array $headers = [], $code = 0)
+ {
+ parent::__construct($message, $code, $previous);
+ }
+
+ public function getStatusCode()
+ {
+ return $this->statusCode;
+ }
+
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+}
diff --git a/src/think/exception/HttpResponseException.php b/src/think/exception/HttpResponseException.php
new file mode 100644
index 0000000..2ce18e3
--- /dev/null
+++ b/src/think/exception/HttpResponseException.php
@@ -0,0 +1,31 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+use think\Response;
+
+/**
+ * HTTP响应异常
+ */
+class HttpResponseException extends \RuntimeException
+{
+ public function __construct(protected Response $response)
+ {
+ }
+
+ public function getResponse()
+ {
+ return $this->response;
+ }
+
+}
diff --git a/src/think/exception/InvalidArgumentException.php b/src/think/exception/InvalidArgumentException.php
new file mode 100644
index 0000000..8c21efb
--- /dev/null
+++ b/src/think/exception/InvalidArgumentException.php
@@ -0,0 +1,21 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+namespace think\exception;
+
+use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInvalidArgumentInterface;
+
+/**
+ * 非法数据异常
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements SimpleCacheInvalidArgumentInterface
+{
+}
diff --git a/src/think/exception/InvalidCacheException.php b/src/think/exception/InvalidCacheException.php
new file mode 100644
index 0000000..afe2d04
--- /dev/null
+++ b/src/think/exception/InvalidCacheException.php
@@ -0,0 +1,19 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+namespace think\exception;
+
+/**
+ * 非法缓存数据异常
+ */
+class InvalidCacheException extends InvalidArgumentException
+{
+}
diff --git a/src/think/exception/RouteNotFoundException.php b/src/think/exception/RouteNotFoundException.php
new file mode 100644
index 0000000..7cd0206
--- /dev/null
+++ b/src/think/exception/RouteNotFoundException.php
@@ -0,0 +1,26 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\exception;
+
+/**
+ * 路由未定义异常
+ */
+class RouteNotFoundException extends HttpException
+{
+
+ public function __construct()
+ {
+ parent::__construct(404, 'Route Not Found');
+ }
+
+}
diff --git a/src/think/facade/App.php b/src/think/facade/App.php
new file mode 100644
index 0000000..f99d1f4
--- /dev/null
+++ b/src/think/facade/App.php
@@ -0,0 +1,59 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\App
+ * @package think\facade
+ * @mixin \think\App
+ * @method static \think\Service|null register(\think\Service|string $service, bool $force = false) 注册服务
+ * @method static mixed bootService(\think\Service $service) 执行服务
+ * @method static \think\Service|null getService(string|\think\Service $service) 获取服务
+ * @method static \think\App debug(bool $debug = true) 开启应用调试模式
+ * @method static bool isDebug() 是否为调试模式
+ * @method static \think\App setNamespace(string $namespace) 设置应用命名空间
+ * @method static string getNamespace() 获取应用类库命名空间
+ * @method static string version() 获取框架版本
+ * @method static string getRootPath() 获取应用根目录
+ * @method static string getBasePath() 获取应用基础目录
+ * @method static string getAppPath() 获取当前应用目录
+ * @method static mixed setAppPath(string $path) 设置应用目录
+ * @method static string getRuntimePath() 获取应用运行时目录
+ * @method static void setRuntimePath(string $path) 设置runtime目录
+ * @method static string getThinkPath() 获取核心框架目录
+ * @method static string getConfigPath() 获取应用配置目录
+ * @method static string getConfigExt() 获取配置后缀
+ * @method static float getBeginTime() 获取应用开启时间
+ * @method static integer getBeginMem() 获取应用初始内存占用
+ * @method static \think\App initialize() 初始化应用
+ * @method static bool initialized() 是否初始化过
+ * @method static void loadLangPack(string $langset) 加载语言包
+ * @method static void boot() 引导应用
+ * @method static void loadEvent(array $event) 注册应用事件
+ * @method static string parseClass(string $layer, string $name) 解析应用类的类名
+ * @method static bool runningInConsole() 是否运行在命令行下
+ */
+class App extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'app';
+ }
+}
diff --git a/src/think/facade/Cache.php b/src/think/facade/Cache.php
new file mode 100644
index 0000000..84e52a9
--- /dev/null
+++ b/src/think/facade/Cache.php
@@ -0,0 +1,48 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\cache\Driver;
+use think\cache\TagSet;
+use think\Facade;
+
+/**
+ * @see \think\Cache
+ * @package think\facade
+ * @mixin \think\Cache
+ * @method static string|null getDefaultDriver() 默认驱动
+ * @method static mixed getConfig(null|string $name = null, mixed $default = null) 获取缓存配置
+ * @method static array getStoreConfig(string $store, string $name = null, null $default = null) 获取驱动配置
+ * @method static Driver store(string $name = null) 连接或者切换缓存
+ * @method static bool clear() 清空缓冲池
+ * @method static mixed get(string $key, mixed $default = null) 读取缓存
+ * @method static bool set(string $key, mixed $value, int|\DateInterval|\DateTimeInterface $ttl = null) 写入缓存
+ * @method static bool delete(string $key) 删除缓存
+ * @method static iterable getMultiple(iterable $keys, mixed $default = null) 读取缓存
+ * @method static bool setMultiple(iterable $values, null|int|\DateInterval|\DateTimeInterface $ttl = null) 写入缓存
+ * @method static bool deleteMultiple(iterable $keys) 删除缓存
+ * @method static bool has(string $key) 判断缓存是否存在
+ * @method static TagSet tag(string|array $name) 缓存标签
+ */
+class Cache extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'cache';
+ }
+}
diff --git a/src/think/facade/Config.php b/src/think/facade/Config.php
new file mode 100644
index 0000000..d029b55
--- /dev/null
+++ b/src/think/facade/Config.php
@@ -0,0 +1,37 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Config
+ * @package think\facade
+ * @mixin \think\Config
+ * @method static array load(string $file, string $name = '') 加载配置文件(多种格式)
+ * @method static bool has(string $name) 检测配置是否存在
+ * @method static mixed get(string $name = null, mixed $default = null) 获取配置参数 为空则获取所有配置
+ * @method static array set(array $config, string $name = null) 设置配置参数 name为数组则为批量设置
+ */
+class Config extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'config';
+ }
+}
diff --git a/src/think/facade/Console.php b/src/think/facade/Console.php
new file mode 100644
index 0000000..b733a6d
--- /dev/null
+++ b/src/think/facade/Console.php
@@ -0,0 +1,56 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Definition as InputDefinition;
+use think\console\Output;
+use think\console\output\driver\Buffer;
+use think\Facade;
+
+/**
+ * Class Console
+ * @package think\facade
+ * @mixin \think\Console
+ * @method static Output|Buffer call(string $command, array $parameters = [], string $driver = 'buffer')
+ * @method static int run() 执行当前的指令
+ * @method static int doRun(Input $input, Output $output) 执行指令
+ * @method static void setDefinition(InputDefinition $definition) 设置输入参数定义
+ * @method static InputDefinition The InputDefinition instance getDefinition() 获取输入参数定义
+ * @method static string A help message. getHelp() Gets the help message.
+ * @method static void setCatchExceptions(bool $boolean) 是否捕获异常
+ * @method static void setAutoExit(bool $boolean) 是否自动退出
+ * @method static string getLongVersion() 获取完整的版本号
+ * @method static void addCommands(array $commands) 添加指令集
+ * @method static Command|void addCommand(string|Command $command, string $name = '') 添加一个指令
+ * @method static Command getCommand(string $name) 获取指令
+ * @method static bool hasCommand(string $name) 某个指令是否存在
+ * @method static array getNamespaces() 获取所有的命名空间
+ * @method static string findNamespace(string $namespace) 查找注册命名空间中的名称或缩写。
+ * @method static Command find(string $name) 查找指令
+ * @method static Command[] all(string $namespace = null) 获取所有的指令
+ * @method static string extractNamespace(string $name, int $limit = 0) 返回命名空间部分
+ */
+class Console extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'console';
+ }
+}
diff --git a/src/think/facade/Cookie.php b/src/think/facade/Cookie.php
new file mode 100644
index 0000000..afbb74e
--- /dev/null
+++ b/src/think/facade/Cookie.php
@@ -0,0 +1,40 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Cookie
+ * @package think\facade
+ * @mixin \think\Cookie
+ * @method static mixed get(mixed $name = '', string $default = null) 获取cookie
+ * @method static bool has(string $name) 是否存在Cookie参数
+ * @method static void set(string $name, string $value, mixed $option = null) Cookie 设置
+ * @method static void forever(string $name, string $value = '', mixed $option = null) 永久保存Cookie数据
+ * @method static void delete(string $name) Cookie删除
+ * @method static array getCookie() 获取cookie保存数据
+ * @method static void save() 保存Cookie
+ */
+class Cookie extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'cookie';
+ }
+}
diff --git a/src/think/facade/Env.php b/src/think/facade/Env.php
new file mode 100644
index 0000000..a4a506c
--- /dev/null
+++ b/src/think/facade/Env.php
@@ -0,0 +1,44 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Env
+ * @package think\facade
+ * @mixin \think\Env
+ * @method static void load(string $file) 读取环境变量定义文件
+ * @method static mixed get(string $name = null, mixed $default = null) 获取环境变量值
+ * @method static void set(string|array $env, mixed $value = null) 设置环境变量值
+ * @method static bool has(string $name) 检测是否存在环境变量
+ * @method static void __set(string $name, mixed $value) 设置环境变量
+ * @method static mixed __get(string $name) 获取环境变量
+ * @method static bool __isset(string $name) 检测是否存在环境变量
+ * @method static void offsetSet($name, $value)
+ * @method static bool offsetExists($name)
+ * @method static mixed offsetUnset($name)
+ * @method static mixed offsetGet($name)
+ */
+class Env extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'env';
+ }
+}
diff --git a/src/think/facade/Event.php b/src/think/facade/Event.php
new file mode 100644
index 0000000..157ac9a
--- /dev/null
+++ b/src/think/facade/Event.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Event
+ * @package think\facade
+ * @mixin \think\Event
+ * @method static \think\Event listenEvents(array $events) 批量注册事件监听
+ * @method static \think\Event listen(string $event, mixed $listener, bool $first = false) 注册事件监听
+ * @method static bool hasListener(string $event) 是否存在事件监听
+ * @method static void remove(string $event) 移除事件监听
+ * @method static \think\Event bind(array $events) 指定事件别名标识 便于调用
+ * @method static \think\Event subscribe(mixed $subscriber) 注册事件订阅者
+ * @method static \think\Event observe(string|object $observer, null|string $prefix = '') 自动注册事件观察者
+ * @method static mixed trigger(string|object $event, mixed $params = null, bool $once = false) 触发事件
+ * @method static mixed until($event, $params = null) 触发事件(只获取一个有效返回值)
+ */
+class Event extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'event';
+ }
+}
diff --git a/src/think/facade/Lang.php b/src/think/facade/Lang.php
new file mode 100644
index 0000000..8c7eb71
--- /dev/null
+++ b/src/think/facade/Lang.php
@@ -0,0 +1,41 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Lang
+ * @package think\facade
+ * @mixin \think\Lang
+ * @method static void setLangSet(string $lang) 设置当前语言
+ * @method static string getLangSet() 获取当前语言
+ * @method static string defaultLangSet() 获取默认语言
+ * @method static array load(string|array $file, string $range = '') 加载语言定义(不区分大小写)
+ * @method static bool has(string|null $name, string $range = '') 判断是否存在语言定义(不区分大小写)
+ * @method static mixed get(string|null $name = null, array $vars = [], string $range = '') 获取语言定义(不区分大小写)
+ * @method static string detect(\think\Request $request) 自动侦测设置获取语言选择
+ * @method static void saveToCookie(\think\Cookie $cookie) 保存当前语言到Cookie
+ */
+class Lang extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'lang';
+ }
+}
diff --git a/src/think/facade/Log.php b/src/think/facade/Log.php
new file mode 100644
index 0000000..268547c
--- /dev/null
+++ b/src/think/facade/Log.php
@@ -0,0 +1,58 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+use think\log\Channel;
+use think\log\ChannelSet;
+
+/**
+ * @see \think\Log
+ * @package think\facade
+ * @mixin \think\Log
+ * @method static string|null getDefaultDriver() 默认驱动
+ * @method static mixed getConfig(null|string $name = null, mixed $default = null) 获取日志配置
+ * @method static array getChannelConfig(string $channel, null $name = null, null $default = null) 获取渠道配置
+ * @method static Channel|ChannelSet channel(string|array $name = null) driver() 的别名
+ * @method static mixed createDriver(string $name)
+ * @method static \think\Log clear(string|array $channel = '*') 清空日志信息
+ * @method static \think\Log close(string|array $channel = '*') 关闭本次请求日志写入
+ * @method static array getLog(string $channel = null) 获取日志信息
+ * @method static bool save() 保存日志信息
+ * @method static \think\Log record(mixed $msg, string $type = 'info', array $context = [], bool $lazy = true) 记录日志信息
+ * @method static \think\Log write(mixed $msg, string $type = 'info', array $context = []) 实时写入日志信息
+ * @method static Event listen($listener) 注册日志写入事件监听
+ * @method static void log(string $level, string|\Stringable $message, array $context = []) 记录日志信息
+ * @method static void emergency(string|\Stringable $message, array $context = []) 记录emergency信息
+ * @method static void alert(string|\Stringable $message, array $context = []) 记录警报信息
+ * @method static void critical(string|\Stringable $message, array $context = []) 记录紧急情况
+ * @method static void error(string|\Stringable $message, array $context = []) 记录错误信息
+ * @method static void warning(string|\Stringable $message, array $context = []) 记录warning信息
+ * @method static void notice(string|\Stringable $message, array $context = []) 记录notice信息
+ * @method static void info(string|\Stringable $message, array $context = []) 记录一般信息
+ * @method static void debug(string|\Stringable $message, array $context = []) 记录调试信息
+ * @method static void sql(string|\Stringable $message, array $context = []) 记录sql信息
+ * @method static mixed __call($method, $parameters)
+ */
+class Log extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'log';
+ }
+}
diff --git a/src/think/facade/Middleware.php b/src/think/facade/Middleware.php
new file mode 100644
index 0000000..9ad74b2
--- /dev/null
+++ b/src/think/facade/Middleware.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Middleware
+ * @package think\facade
+ * @mixin \think\Middleware
+ * @method static void import(array $middlewares = [], string $type = 'global') 导入中间件
+ * @method static void add(mixed $middleware, string $type = 'global') 注册中间件
+ * @method static void route(mixed $middleware) 注册路由中间件
+ * @method static void controller(mixed $middleware) 注册控制器中间件
+ * @method static mixed unshift(mixed $middleware, string $type = 'global') 注册中间件到开始位置
+ * @method static array all(string $type = 'global') 获取注册的中间件
+ * @method static Pipeline pipeline(string $type = 'global') 调度管道
+ * @method static mixed end(\think\Response $response) 结束调度
+ * @method static \think\Response handleException(\think\Request $passable, \Throwable $e) 异常处理
+ */
+class Middleware extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'middleware';
+ }
+}
diff --git a/src/think/facade/Request.php b/src/think/facade/Request.php
new file mode 100644
index 0000000..afc60df
--- /dev/null
+++ b/src/think/facade/Request.php
@@ -0,0 +1,136 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+use think\file\UploadedFile;
+use think\route\Rule;
+
+/**
+ * @see \think\Request
+ * @package think\facade
+ * @mixin \think\Request
+ * @method static \think\Request setDomain(string $domain) 设置当前包含协议的域名
+ * @method static string domain(bool $port = false) 获取当前包含协议的域名
+ * @method static string rootDomain() 获取当前根域名
+ * @method static \think\Request setSubDomain(string $domain) 设置当前泛域名的值
+ * @method static string subDomain() 获取当前子域名
+ * @method static \think\Request setPanDomain(string $domain) 设置当前泛域名的值
+ * @method static string panDomain() 获取当前泛域名的值
+ * @method static \think\Request setUrl(string $url) 设置当前完整URL 包括QUERY_STRING
+ * @method static string url(bool $complete = false) 获取当前完整URL 包括QUERY_STRING
+ * @method static \think\Request setBaseUrl(string $url) 设置当前URL 不含QUERY_STRING
+ * @method static string baseUrl(bool $complete = false) 获取当前URL 不含QUERY_STRING
+ * @method static string baseFile(bool $complete = false) 获取当前执行的文件 SCRIPT_NAME
+ * @method static \think\Request setRoot(string $url) 设置URL访问根地址
+ * @method static string root(bool $complete = false) 获取URL访问根地址
+ * @method static string rootUrl() 获取URL访问根目录
+ * @method static \think\Request setPathinfo(string $pathinfo) 设置当前请求的pathinfo
+ * @method static string pathinfo() 获取当前请求URL的pathinfo信息(含URL后缀)
+ * @method static string ext() 当前URL的访问后缀
+ * @method static integer|float time(bool $float = false) 获取当前请求的时间
+ * @method static string type() 当前请求的资源类型
+ * @method static void mimeType(string|array $type, string $val = '') 设置资源类型
+ * @method static \think\Request setMethod(string $method) 设置请求类型
+ * @method static string method(bool $origin = false) 当前的请求类型
+ * @method static bool isGet() 是否为GET请求
+ * @method static bool isPost() 是否为POST请求
+ * @method static bool isPut() 是否为PUT请求
+ * @method static bool isDelete() 是否为DELTE请求
+ * @method static bool isHead() 是否为HEAD请求
+ * @method static bool isPatch() 是否为PATCH请求
+ * @method static bool isOptions() 是否为OPTIONS请求
+ * @method static bool isCli() 是否为cli
+ * @method static bool isCgi() 是否为cgi
+ * @method static mixed param(string|array $name = '', mixed $default = null, string|array|null $filter = '') 获取当前请求的参数
+ * @method static \think\Request setRule(Rule $rule) 设置路由变量
+ * @method static Rule|null rule() 获取当前路由对象
+ * @method static \think\Request setRoute(array $route) 设置路由变量
+ * @method static mixed route(string|array $name = '', mixed $default = null, string|array|null $filter = '') 获取路由参数
+ * @method static mixed get(string|array $name = '', mixed $default = null, string|array|null $filter = '') 获取GET参数
+ * @method static mixed middleware(string|null $name, mixed $default = null) 获取中间件传递的参数
+ * @method static mixed post(string|array $name = '', mixed $default = null, string|array|null $filter = '') 获取POST参数
+ * @method static mixed put(string|array $name = '', mixed $default = null, string|array|null $filter = '') 获取PUT参数
+ * @method static mixed delete(mixed $name = '', mixed $default = null, string|array|null $filter = '') 设置获取DELETE参数
+ * @method static mixed patch(mixed $name = '', mixed $default = null, string|array|null $filter = '') 设置获取PATCH参数
+ * @method static mixed request(string|array|bool $name = '', mixed $default = null, string|array|null $filter = '') 获取request变量
+ * @method static mixed env(string $name = '', string|null $default = null) 获取环境变量
+ * @method static mixed session(string $name = '', string|null $default = null) 获取session数据
+ * @method static mixed cookie(mixed $name = '', string|null $default = null, string|array|null $filter = '') 获取cookie参数
+ * @method static mixed server(string $name = '', string $default = '') 获取server参数
+ * @method static null|array|UploadedFile file(string $name = '') 获取上传的文件信息
+ * @method static string|array header(string $name = '', string|null $default = null) 设置或者获取当前的Header
+ * @method static mixed input(array $data = [], string|false $name = '', mixed $default = null, string|array|null $filter = '') 获取变量 支持过滤和默认值
+ * @method static mixed filter(mixed $filter = null) 设置或获取当前的过滤规则
+ * @method static mixed filterValue(mixed &$value, mixed $key, array $filters) 递归过滤给定的值
+ * @method static bool has(string $name, string $type = 'param', bool $checkEmpty = false) 是否存在某个请求参数
+ * @method static array only(array $name, mixed $data = 'param', string|array|null $filter = '') 获取指定的参数
+ * @method static mixed except(array $name, string $type = 'param') 排除指定参数获取
+ * @method static bool isSsl() 当前是否ssl
+ * @method static bool isJson() 当前是否JSON请求
+ * @method static bool isAjax(bool $ajax = false) 当前是否Ajax请求
+ * @method static bool isPjax(bool $pjax = false) 当前是否Pjax请求
+ * @method static string ip() 获取客户端IP地址
+ * @method static boolean isValidIP(string $ip, string $type = '') 检测是否是合法的IP地址
+ * @method static string ip2bin(string $ip) 将IP地址转换为二进制字符串
+ * @method static bool isMobile() 检测是否使用手机访问
+ * @method static string scheme() 当前URL地址中的scheme参数
+ * @method static string query() 当前请求URL地址中的query参数
+ * @method static \think\Request setHost(string $host) 设置当前请求的host(包含端口)
+ * @method static string host(bool $strict = false) 当前请求的host
+ * @method static int port() 当前请求URL地址中的port参数
+ * @method static string protocol() 当前请求 SERVER_PROTOCOL
+ * @method static int remotePort() 当前请求 REMOTE_PORT
+ * @method static string contentType() 当前请求 HTTP_CONTENT_TYPE
+ * @method static string secureKey() 获取当前请求的安全Key
+ * @method static \think\Request setLayer(string $layer) 设置当前的分层名
+ * @method static \think\Request setController(string $controller) 设置当前的控制器名
+ * @method static \think\Request setAction(string $action) 设置当前的操作名
+ * @method static string layer(bool $convert = false) 获取当前的模块名
+ * @method static string controller(bool $convert = false, bool $base = false) 获取当前的控制器名
+ * @method static string action(bool $convert = false) 获取当前的操作名
+ * @method static string getContent() 设置或者获取当前请求的content
+ * @method static string getInput() 获取当前请求的php://input
+ * @method static string buildToken(string $name = '__token__', mixed $type = 'md5') 生成请求令牌
+ * @method static bool checkToken(string $token = '__token__', array $data = []) 检查请求令牌
+ * @method static \think\Request withMiddleware(array $middleware) 设置在中间件传递的数据
+ * @method static \think\Request withGet(array $get) 设置GET数据
+ * @method static \think\Request withPost(array $post) 设置POST数据
+ * @method static \think\Request withCookie(array $cookie) 设置COOKIE数据
+ * @method static \think\Request withSession(Session $session) 设置SESSION数据
+ * @method static \think\Request withServer(array $server) 设置SERVER数据
+ * @method static \think\Request withHeader(array $header) 设置HEADER数据
+ * @method static \think\Request withEnv(Env $env) 设置ENV数据
+ * @method static \think\Request withInput(string $input) 设置php://input数据
+ * @method static \think\Request withFiles(array $files) 设置文件上传数据
+ * @method static \think\Request withRoute(array $route) 设置ROUTE变量
+ * @method static mixed __set(string $name, mixed $value) 设置中间传递数据
+ * @method static mixed __get(string $name) 获取中间传递数据的值
+ * @method static boolean __isset(string $name) 检测中间传递数据的值
+ * @method static bool offsetExists(mixed $name)
+ * @method static mixed offsetGet(mixed $name)
+ * @method static mixed offsetSet(mixed $name, $value)
+ * @method static mixed offsetUnset(mixed $name)
+ */
+class Request extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'request';
+ }
+}
diff --git a/src/think/facade/Route.php b/src/think/facade/Route.php
new file mode 100644
index 0000000..0f3aa01
--- /dev/null
+++ b/src/think/facade/Route.php
@@ -0,0 +1,82 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+use think\route\Dispatch;
+use think\route\Domain;
+use think\route\Rule;
+use think\route\RuleGroup;
+use think\route\RuleItem;
+use think\route\RuleName;
+use think\route\Url as UrlBuild;
+
+/**
+ * @see \think\Route
+ * @package think\facade
+ * @mixin \think\Route
+ * @method static mixed config(string $name = null)
+ * @method static \think\Route lazy(bool $lazy = true) 设置路由域名及分组(包括资源路由)是否延迟解析
+ * @method static \think\Route mergeRuleRegex(bool $merge = true) 设置路由域名及分组(包括资源路由)是否合并解析
+ * @method static void setGroup(RuleGroup $group) 设置当前分组
+ * @method static RuleGroup getGroup(string $name = null) 获取指定标识的路由分组 不指定则获取当前分组
+ * @method static \think\Route pattern(array $pattern) 注册变量规则
+ * @method static \think\Route option(array $option) 注册路由参数
+ * @method static Domain domain(string|array $name, mixed $rule = null) 注册域名路由
+ * @method static array getDomains() 获取域名
+ * @method static RuleName getRuleName() 获取RuleName对象
+ * @method static \think\Route bind(string $bind, string $domain = null) 设置路由绑定
+ * @method static array getBind() 读取路由绑定信息
+ * @method static string|null getDomainBind(string $domain = null) 读取路由绑定
+ * @method static RuleItem[] getName(string $name = null, string $domain = null, string $method = '*') 读取路由标识
+ * @method static void import(array $name) 批量导入路由标识
+ * @method static void setName(string $name, RuleItem $ruleItem, bool $first = false) 注册路由标识
+ * @method static void setRule(string $rule, RuleItem $ruleItem = null) 保存路由规则
+ * @method static RuleItem[] getRule(string $rule) 读取路由
+ * @method static array getRuleList() 读取路由列表
+ * @method static void clear() 清空路由规则
+ * @method static RuleItem rule(string $rule, mixed $route = null, string $method = '*') 注册路由规则
+ * @method static \think\Route setCrossDomainRule(Rule $rule) 设置跨域有效路由规则
+ * @method static RuleGroup group(string|\Closure $name, mixed $route = null) 注册路由分组
+ * @method static RuleItem any(string $rule, mixed $route) 注册路由
+ * @method static RuleItem get(string $rule, mixed $route) 注册GET路由
+ * @method static RuleItem post(string $rule, mixed $route) 注册POST路由
+ * @method static RuleItem put(string $rule, mixed $route) 注册PUT路由
+ * @method static RuleItem delete(string $rule, mixed $route) 注册DELETE路由
+ * @method static RuleItem patch(string $rule, mixed $route) 注册PATCH路由
+ * @method static RuleItem head(string $rule, mixed $route) 注册HEAD路由
+ * @method static RuleItem options(string $rule, mixed $route) 注册OPTIONS路由
+ * @method static Resource resource(string $rule, string $route, ?\Closure $extend = null) 注册资源路由
+ * @method static RuleItem view(string $rule, string $template = '', array $vars = []) 注册视图路由
+ * @method static RuleItem redirect(string $rule, string $route = '', int $status = 301) 注册重定向路由
+ * @method static \think\Route rest(string|array $name, array|bool $resource = []) rest方法定义和修改
+ * @method static array|null getRest(string $name = null) 获取rest方法定义的参数
+ * @method static RuleItem miss(string|\Closure $route, string $method = '*') 注册未匹配路由规则后的处理
+ * @method static Response dispatch(\think\Request $request, Closure|bool $withRoute = true) 路由调度
+ * @method static Dispatch|false check() 检测URL路由
+ * @method static Dispatch url(string $url) 默认URL解析
+ * @method static UrlBuild buildUrl(string $url = '', array $vars = []) URL生成 支持路由反射
+ * @method static RuleGroup __call(string $method, array $args) 设置全局的路由分组参数
+ */
+class Route extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'route';
+ }
+}
diff --git a/src/think/facade/Session.php b/src/think/facade/Session.php
new file mode 100644
index 0000000..d7e7c14
--- /dev/null
+++ b/src/think/facade/Session.php
@@ -0,0 +1,45 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Session
+ * @package think\facade
+ * @mixin \think\Session
+ * @method static void set(string $name, mixed $value) 设置Session值
+ * @method static mixed get(string $name, mixed $default = null) 获取Session值
+ * @method static array all() 获取全部Session值
+ * @method static void delete(string $name) 删除Session值
+ * @method static mixed pull(string $name) 获取并删除Session值
+ * @method static void push(string $key, mixed $value) 添加数据到一个session数组
+ * @method static bool has(string $name) 判断Session值
+ * @method static void clear() 清空Session数据
+ * @method static void destroy() 销毁Session
+ * @method static void save() 写入Session数据(通常情况自动写入)
+ * @method static mixed getConfig(null|string $name = null, mixed $default = null) 获取Session配置
+ * @method static string|null getDefaultDriver() 默认驱动
+ */
+class Session extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'session';
+ }
+}
diff --git a/src/think/facade/View.php b/src/think/facade/View.php
new file mode 100644
index 0000000..c289229
--- /dev/null
+++ b/src/think/facade/View.php
@@ -0,0 +1,43 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\View
+ * @package think\facade
+ * @mixin \think\View
+ * @method static \think\View engine(string $type = null) 获取模板引擎
+ * @method static \think\View assign(string|array $name, mixed $value = null) 模板变量赋值
+ * @method static \think\View filter(callable $filter = null) 视图过滤
+ * @method static string fetch(string $template = '', array $vars = []) 解析和获取模板内容 用于输出
+ * @method static string display(string $content, array $vars = []) 渲染内容输出
+ * @method static mixed __set(string $name, mixed $value) 模板变量赋值
+ * @method static mixed __get(string $name) 取得模板显示变量的值
+ * @method static bool __isset(string $name) 检测模板变量是否设置
+ * @method static string|null getDefaultDriver() 默认驱动
+ */
+class View extends Facade
+{
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'view';
+ }
+}
diff --git a/src/think/file/UploadedFile.php b/src/think/file/UploadedFile.php
new file mode 100644
index 0000000..4e3c3d2
--- /dev/null
+++ b/src/think/file/UploadedFile.php
@@ -0,0 +1,129 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\file;
+
+use think\exception\FileException;
+use think\File;
+
+class UploadedFile extends File
+{
+
+ private $test = false;
+ private $originalName;
+ private $mimeType;
+ private $error;
+
+ public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $error = null, bool $test = false)
+ {
+ $this->originalName = $originalName;
+ $this->mimeType = $mimeType ?: 'application/octet-stream';
+ $this->test = $test;
+ $this->error = $error ?: UPLOAD_ERR_OK;
+
+ parent::__construct($path, UPLOAD_ERR_OK === $this->error);
+ }
+
+ public function isValid(): bool
+ {
+ $isOk = UPLOAD_ERR_OK === $this->error;
+
+ return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
+ }
+
+ /**
+ * 上传文件
+ * @access public
+ * @param string $directory 保存路径
+ * @param string|null $name 保存的文件名
+ * @return File
+ */
+ public function move(string $directory, ?string $name = null): File
+ {
+ if ($this->isValid()) {
+ if ($this->test) {
+ return parent::move($directory, $name);
+ }
+
+ $target = $this->getTargetFile($directory, $name);
+
+ set_error_handler(function ($type, $msg) use (&$error) {
+ $error = $msg;
+ });
+
+ $moved = move_uploaded_file($this->getPathname(), (string) $target);
+ restore_error_handler();
+ if (!$moved) {
+ throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error)));
+ }
+
+ @chmod((string) $target, 0666 & ~umask());
+
+ return $target;
+ }
+
+ throw new FileException($this->getErrorMessage());
+ }
+
+ /**
+ * 获取错误信息
+ * @access public
+ * @return string
+ */
+ protected function getErrorMessage(): string
+ {
+ return match ($this->error) {
+ 1,2 => 'upload File size exceeds the maximum value',
+ 3 => 'only the portion of file is uploaded',
+ 4 => 'no file to uploaded',
+ 6 => 'upload temp dir not found',
+ 7 => 'file write error',
+ default => 'unknown upload error',
+ };
+ }
+
+ /**
+ * 获取上传文件类型信息
+ * @return string
+ */
+ public function getOriginalMime(): string
+ {
+ return $this->mimeType;
+ }
+
+ /**
+ * 上传文件名
+ * @return string
+ */
+ public function getOriginalName(): string
+ {
+ return $this->originalName;
+ }
+
+ /**
+ * 获取上传文件扩展名
+ * @return string
+ */
+ public function getOriginalExtension(): string
+ {
+ return pathinfo($this->originalName, PATHINFO_EXTENSION);
+ }
+
+ /**
+ * 获取文件扩展名
+ * @return string
+ */
+ public function extension(): string
+ {
+ return $this->getOriginalExtension();
+ }
+}
diff --git a/src/think/initializer/BootService.php b/src/think/initializer/BootService.php
new file mode 100644
index 0000000..04ef6cb
--- /dev/null
+++ b/src/think/initializer/BootService.php
@@ -0,0 +1,26 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\initializer;
+
+use think\App;
+
+/**
+ * 启动系统服务
+ */
+class BootService
+{
+ public function init(App $app)
+ {
+ $app->boot();
+ }
+}
diff --git a/src/think/initializer/Error.php b/src/think/initializer/Error.php
new file mode 100644
index 0000000..6cd0509
--- /dev/null
+++ b/src/think/initializer/Error.php
@@ -0,0 +1,120 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\initializer;
+
+use think\App;
+use think\console\Output as ConsoleOutput;
+use think\exception\ErrorException;
+use think\exception\Handle;
+use Throwable;
+
+/**
+ * 错误和异常处理
+ */
+class Error
+{
+ /** @var App */
+ protected $app;
+
+ /**
+ * 注册异常处理
+ * @access public
+ * @param App $app
+ * @return void
+ */
+ public function init(App $app)
+ {
+ $this->app = $app;
+ error_reporting(E_ALL);
+ ini_set('display_errors', 'Off');
+ set_error_handler([$this, 'appError']);
+ set_exception_handler([$this, 'appException']);
+ register_shutdown_function([$this, 'appShutdown']);
+ }
+
+ /**
+ * Exception Handler
+ * @access public
+ * @param \Throwable $e
+ */
+ public function appException(Throwable $e): void
+ {
+ $handler = $this->getExceptionHandler();
+
+ $handler->report($e);
+
+ if ($this->app->runningInConsole()) {
+ $handler->renderForConsole(new ConsoleOutput, $e);
+ } else {
+ $response = $handler->render($this->app->request, $e);
+ $response->send();
+ $this->app->http->end($response);
+ }
+ }
+
+ /**
+ * Error Handler
+ * @access public
+ * @param integer $errno 错误编号
+ * @param string $errstr 详细错误信息
+ * @param string $errfile 出错的文件
+ * @param integer $errline 出错行号
+ * @throws ErrorException
+ */
+ public function appError(int $errno, string $errstr, string $errfile = '', int $errline = 0): void
+ {
+ $exception = new ErrorException($errno, $errstr, $errfile, $errline);
+
+ if (error_reporting() & $errno) {
+ // 将错误信息托管至 think\exception\ErrorException
+ throw $exception;
+ }
+ }
+
+ /**
+ * Shutdown Handler
+ * @access public
+ */
+ public function appShutdown(): void
+ {
+ if (!is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
+ // 将错误信息托管至think\ErrorException
+ $exception = new ErrorException($error['type'], $error['message'], $error['file'], $error['line']);
+
+ $this->appException($exception);
+ }
+ }
+
+ /**
+ * 确定错误类型是否致命
+ *
+ * @access protected
+ * @param int $type
+ * @return bool
+ */
+ protected function isFatal(int $type): bool
+ {
+ return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]);
+ }
+
+ /**
+ * Get an instance of the exception handler.
+ *
+ * @access protected
+ * @return Handle
+ */
+ protected function getExceptionHandler()
+ {
+ return $this->app->make(Handle::class);
+ }
+}
diff --git a/src/think/initializer/RegisterService.php b/src/think/initializer/RegisterService.php
new file mode 100644
index 0000000..06620fe
--- /dev/null
+++ b/src/think/initializer/RegisterService.php
@@ -0,0 +1,48 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\initializer;
+
+use think\App;
+use think\service\ModelService;
+use think\service\PaginatorService;
+use think\service\ValidateService;
+
+/**
+ * 注册系统服务
+ */
+class RegisterService
+{
+
+ protected $services = [
+ PaginatorService::class,
+ ValidateService::class,
+ ModelService::class,
+ ];
+
+ public function init(App $app)
+ {
+ $file = $app->getRootPath() . 'vendor/services.php';
+
+ $services = $this->services;
+
+ if (is_file($file)) {
+ $services = array_merge($services, include $file);
+ }
+
+ foreach ($services as $service) {
+ if (class_exists($service)) {
+ $app->register($service);
+ }
+ }
+ }
+}
diff --git a/src/think/log/Channel.php b/src/think/log/Channel.php
new file mode 100644
index 0000000..6c65dde
--- /dev/null
+++ b/src/think/log/Channel.php
@@ -0,0 +1,162 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types = 1);
+
+namespace think\log;
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\LoggerTrait;
+use Stringable;
+use think\contract\LogHandlerInterface;
+use think\Event;
+use think\event\LogRecord;
+use think\event\LogWrite;
+
+class Channel implements LoggerInterface
+{
+ use LoggerTrait;
+
+ /**
+ * 日志信息
+ * @var array
+ */
+ protected array $log = [];
+
+ /**
+ * 关闭日志
+ * @var bool
+ */
+ protected bool $close = false;
+
+ public function __construct(protected string $name, protected LogHandlerInterface $logger, protected array $allow, protected bool $lazy, protected Event $event)
+ {
+ }
+
+ /**
+ * 关闭通道
+ */
+ public function close(): void
+ {
+ $this->clear();
+ $this->close = true;
+ }
+
+ /**
+ * 清空日志
+ */
+ public function clear(): void
+ {
+ $this->log = [];
+ }
+
+ /**
+ * 记录日志信息
+ * @access public
+ * @param mixed $msg 日志信息
+ * @param string $type 日志级别
+ * @param array $context 替换内容
+ * @param bool $lazy
+ * @return $this
+ */
+ public function record($msg, string $type = 'info', array $context = [], bool $lazy = true)
+ {
+ if ($this->close || (!empty($this->allow) && !in_array($type, $this->allow))) {
+ return $this;
+ }
+
+ if ($msg instanceof Stringable) {
+ $msg = $msg->__toString();
+ }
+
+ if (is_string($msg) && !empty($context)) {
+ $replace = [];
+ foreach ($context as $key => $val) {
+ $replace['{' . $key . '}'] = $val;
+ }
+
+ $msg = strtr($msg, $replace);
+ }
+
+ if (!empty($msg)) {
+ $record = new LogRecord($type, $msg);
+ $this->log[] = $record;
+ if ($this->event) {
+ $this->event->trigger($record);
+ }
+ }
+
+ if (!$this->lazy || !$lazy) {
+ $this->save();
+ }
+
+ return $this;
+ }
+
+ /**
+ * 实时写入日志信息
+ * @access public
+ * @param mixed $msg 调试信息
+ * @param string $type 日志级别
+ * @param array $context 替换内容
+ * @return $this
+ */
+ public function write($msg, string $type = 'info', array $context = [])
+ {
+ return $this->record($msg, $type, $context, false);
+ }
+
+ /**
+ * 获取日志信息
+ * @return array
+ */
+ public function getLog(): array
+ {
+ return $this->log;
+ }
+
+ /**
+ * 保存日志
+ * @return bool
+ */
+ public function save(): bool
+ {
+ $log = $this->log;
+ if ($this->event) {
+ $event = new LogWrite($this->name, $this->log);
+ $this->event->trigger($event);
+ $log = $event->log;
+ }
+
+ $this->logger->save($log);
+ $this->log = [];
+
+ return true;
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string|Stringable $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function log($level, $message, array $context = []): void
+ {
+ $this->record($message, $level, $context);
+ }
+
+ public function __call($method, $parameters)
+ {
+ $this->log($method, ...$parameters);
+ }
+}
diff --git a/src/think/log/ChannelSet.php b/src/think/log/ChannelSet.php
new file mode 100644
index 0000000..a3f7251
--- /dev/null
+++ b/src/think/log/ChannelSet.php
@@ -0,0 +1,34 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\log;
+
+use think\Log;
+
+/**
+ * Class ChannelSet
+ * @package think\log
+ * @mixin Channel
+ */
+class ChannelSet
+{
+ public function __construct(protected Log $log, protected array $channels)
+ {
+ }
+
+ public function __call($method, $arguments)
+ {
+ foreach ($this->channels as $channel) {
+ $this->log->channel($channel)->{$method}(...$arguments);
+ }
+ }
+}
diff --git a/src/think/log/driver/File.php b/src/think/log/driver/File.php
new file mode 100644
index 0000000..12fcafc
--- /dev/null
+++ b/src/think/log/driver/File.php
@@ -0,0 +1,199 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\log\driver;
+
+use think\App;
+use think\contract\LogHandlerInterface;
+use think\event\LogRecord;
+
+/**
+ * 本地化调试输出到文件
+ */
+class File implements LogHandlerInterface
+{
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [
+ 'time_format' => 'c',
+ 'single' => false,
+ 'file_size' => 2097152,
+ 'path' => '',
+ 'apart_level' => [],
+ 'max_files' => 0,
+ 'json' => false,
+ 'json_options' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
+ 'format' => '[%s][%s] %s',
+ ];
+
+ // 实例化并传入参数
+ public function __construct(App $app, $config = [])
+ {
+ if (is_array($config)) {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ if (empty($this->config['format'])) {
+ $this->config['format'] = '[%s][%s] %s';
+ }
+
+ if (empty($this->config['path'])) {
+ $this->config['path'] = $app->getRuntimePath() . 'log';
+ }
+
+ if (substr($this->config['path'], -1) != DIRECTORY_SEPARATOR) {
+ $this->config['path'] .= DIRECTORY_SEPARATOR;
+ }
+ }
+
+ /**
+ * 日志写入接口
+ * @access public
+ * @param array $log 日志信息
+ * @return bool
+ */
+ public function save(array $log): bool
+ {
+ $destination = $this->getMasterLogFile();
+
+ $path = dirname($destination);
+ !is_dir($path) && mkdir($path, 0755, true);
+
+ $messages = [];
+
+ // 日志信息封装
+ foreach ($log as $record) {
+ $type = $record->type;
+ $msg = $record->message;
+ $time = $record->time->format($this->config['time_format']);
+ if (!is_string($msg)) {
+ $msg = var_export($msg, true);
+ }
+
+ $filename = $destination;
+ if (true === $this->config['apart_level'] || in_array($type, $this->config['apart_level'])) {
+ // 独立记录的日志级别
+ $filename = $this->getApartLevelFile($path, $type);
+ }
+
+ if (!isset($messages[$filename])) {
+ $messages[$filename] = [];
+ }
+
+ $messages[$filename][] = $this->config['json'] ?
+ json_encode(['time' => $time, 'type' => $type, 'msg' => $msg], $this->config['json_options']) :
+ sprintf($this->config['format'], $time, $type, $msg);
+ }
+
+ foreach ($messages as $filename => $message) {
+ $this->write($message, $filename);
+ }
+
+ return true;
+ }
+
+ /**
+ * 日志写入
+ * @access protected
+ * @param array $message 日志信息
+ * @param string $destination 日志文件
+ * @return bool
+ */
+ protected function write(array $message, string $destination): bool
+ {
+ // 检测日志文件大小,超过配置大小则备份日志文件重新生成
+ $this->checkLogSize($destination);
+
+ return error_log(implode(PHP_EOL, $message) . PHP_EOL, 3, $destination);
+ }
+
+ /**
+ * 获取主日志文件名
+ * @access public
+ * @return string
+ */
+ protected function getMasterLogFile(): string
+ {
+
+ if ($this->config['max_files']) {
+ $files = glob($this->config['path'] . '*.log');
+
+ try {
+ if (count($files) > $this->config['max_files']) {
+ set_error_handler(fn() => null);
+ unlink($files[0]);
+ restore_error_handler();
+ }
+ } catch (\Exception $e) {
+ //
+ }
+ }
+
+ if ($this->config['single']) {
+ $name = is_string($this->config['single']) ? $this->config['single'] : 'single';
+ $destination = $this->config['path'] . $name . '.log';
+ } else {
+
+ if ($this->config['max_files']) {
+ $filename = date('Ymd') . '.log';
+ } else {
+ $filename = date('Ym') . DIRECTORY_SEPARATOR . date('d') . '.log';
+ }
+
+ $destination = $this->config['path'] . $filename;
+ }
+
+ return $destination;
+ }
+
+ /**
+ * 获取独立日志文件名
+ * @access public
+ * @param string $path 日志目录
+ * @param string $type 日志类型
+ * @return string
+ */
+ protected function getApartLevelFile(string $path, string $type): string
+ {
+
+ if ($this->config['single']) {
+ $name = is_string($this->config['single']) ? $this->config['single'] : 'single';
+
+ $name .= '_' . $type;
+ } elseif ($this->config['max_files']) {
+ $name = date('Ymd') . '_' . $type;
+ } else {
+ $name = date('d') . '_' . $type;
+ }
+
+ return $path . DIRECTORY_SEPARATOR . $name . '.log';
+ }
+
+ /**
+ * 检查日志文件大小并自动生成备份文件
+ * @access protected
+ * @param string $destination 日志文件
+ * @return void
+ */
+ protected function checkLogSize(string $destination): void
+ {
+ if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) {
+ try {
+ rename($destination, dirname($destination) . DIRECTORY_SEPARATOR . basename($destination, '.log') . '-' . time() . '.log');
+ } catch (\Exception $e) {
+ //
+ }
+ }
+ }
+}
diff --git a/src/think/middleware/AllowCrossDomain.php b/src/think/middleware/AllowCrossDomain.php
new file mode 100644
index 0000000..ea50f86
--- /dev/null
+++ b/src/think/middleware/AllowCrossDomain.php
@@ -0,0 +1,63 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\middleware;
+
+use Closure;
+use think\Config;
+use think\Request;
+use think\Response;
+
+/**
+ * 跨域请求支持
+ */
+class AllowCrossDomain
+{
+ protected $cookieDomain;
+
+ protected $header = [
+ 'Access-Control-Allow-Credentials' => 'true',
+ 'Access-Control-Max-Age' => 1800,
+ 'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
+ ];
+
+ public function __construct(Config $config)
+ {
+ $this->cookieDomain = $config->get('cookie.domain', '');
+ }
+
+ /**
+ * 允许跨域请求
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @param array $header
+ * @return Response
+ */
+ public function handle(Request $request, Closure $next, array $header = []): Response
+ {
+ $header = !empty($header) ? array_merge($this->header, $header) : $this->header;
+
+ if (!isset($header['Access-Control-Allow-Origin'])) {
+ $origin = $request->header('origin');
+
+ if ($origin && ('' == $this->cookieDomain || str_contains($origin, $this->cookieDomain))) {
+ $header['Access-Control-Allow-Origin'] = $origin;
+ } else {
+ $header['Access-Control-Allow-Origin'] = '*';
+ }
+ }
+
+ return $next($request)->header($header);
+ }
+}
diff --git a/src/think/middleware/CheckRequestCache.php b/src/think/middleware/CheckRequestCache.php
new file mode 100644
index 0000000..b771e9d
--- /dev/null
+++ b/src/think/middleware/CheckRequestCache.php
@@ -0,0 +1,183 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\middleware;
+
+use Closure;
+use think\Cache;
+use think\Config;
+use think\Request;
+use think\Response;
+
+/**
+ * 请求缓存处理
+ */
+class CheckRequestCache
+{
+ /**
+ * 缓存对象
+ * @var Cache
+ */
+ protected $cache;
+
+ /**
+ * 配置参数
+ * @var array
+ */
+ protected $config = [
+ // 请求缓存规则 true为自动规则
+ 'request_cache_key' => true,
+ // 请求缓存有效期
+ 'request_cache_expire' => null,
+ // 全局请求缓存排除规则
+ 'request_cache_except' => [],
+ // 请求缓存的Tag
+ 'request_cache_tag' => '',
+ ];
+
+ public function __construct(Cache $cache, Config $config)
+ {
+ $this->cache = $cache;
+ $this->config = array_merge($this->config, $config->get('route'));
+ }
+
+ /**
+ * 设置当前地址的请求缓存
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @param mixed $cache
+ * @return Response
+ */
+ public function handle(Request $request, Closure $next, $cache = null): Response
+ {
+ if ($request->isGet() && false !== $cache) {
+ if (false === $this->config['request_cache_key']) {
+ // 关闭当前缓存
+ $cache = false;
+ }
+
+ $cache = $cache ?? $this->getRequestCache($request);
+
+ if ($cache) {
+ if (is_array($cache)) {
+ [$key, $expire, $tag] = array_pad($cache, 3, '');
+ } else {
+ $key = md5($request->url(true));
+ $expire = $cache;
+ $tag = '';
+ }
+
+ $key = $this->parseCacheKey($request, $key);
+
+ if (strtotime($request->server('HTTP_IF_MODIFIED_SINCE', '')) + $expire > $request->server('REQUEST_TIME')) {
+ // 读取缓存
+ return Response::create()->code(304);
+ } elseif (($hit = $this->cache->get($key)) !== null) {
+ [$content, $header, $when] = $hit;
+ if (null === $expire || $when + $expire > $request->server('REQUEST_TIME')) {
+ return Response::create($content)->header($header);
+ }
+ }
+ }
+ }
+
+ $response = $next($request);
+
+ if (isset($key) && 200 == $response->getCode() && $response->isAllowCache()) {
+ $header = $response->getHeader();
+ $header['Cache-Control'] = 'max-age=' . $expire . ',must-revalidate';
+ $header['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT';
+ $header['Expires'] = gmdate('D, d M Y H:i:s', time() + $expire) . ' GMT';
+
+ $this->cache->tag($tag)->set($key, [$response->getContent(), $header, time()], $expire);
+ }
+
+ return $response;
+ }
+
+ /**
+ * 读取当前地址的请求缓存信息
+ * @access protected
+ * @param Request $request
+ * @return mixed
+ */
+ protected function getRequestCache($request)
+ {
+ $key = $this->config['request_cache_key'];
+ $expire = $this->config['request_cache_expire'];
+ $except = $this->config['request_cache_except'];
+ $tag = $this->config['request_cache_tag'];
+
+ foreach ($except as $rule) {
+ if (0 === stripos($request->url(), $rule)) {
+ return;
+ }
+ }
+
+ return [$key, $expire, $tag];
+ }
+
+ /**
+ * 读取当前地址的请求缓存信息
+ * @access protected
+ * @param Request $request
+ * @param mixed $key
+ * @return null|string
+ */
+ protected function parseCacheKey($request, $key)
+ {
+ if ($key instanceof Closure) {
+ $key = call_user_func($key, $request);
+ }
+
+ if (false === $key) {
+ // 关闭当前缓存
+ return;
+ }
+
+ if (true === $key) {
+ // 自动缓存功能
+ $key = '__URL__';
+ } elseif (str_contains($key, '|')) {
+ [$key, $fun] = explode('|', $key);
+ }
+
+ // 特殊规则替换
+ if (str_contains($key, '__')) {
+ $key = str_replace(['__CONTROLLER__', '__ACTION__', '__URL__'], [$request->controller(), $request->action(), md5($request->url(true))], $key);
+ }
+
+ if (str_contains($key, ':')) {
+ $param = $request->param();
+
+ foreach ($param as $item => $val) {
+ if (is_string($val) && str_contains($key, ':' . $item)) {
+ $key = str_replace(':' . $item, (string) $val, $key);
+ }
+ }
+ } elseif (str_contains($key, ']')) {
+ if ('[' . $request->ext() . ']' == $key) {
+ // 缓存某个后缀的请求
+ $key = md5($request->url());
+ } else {
+ return;
+ }
+ }
+
+ if (isset($fun)) {
+ $key = $fun($key);
+ }
+
+ return $key;
+ }
+}
diff --git a/src/think/middleware/FormTokenCheck.php b/src/think/middleware/FormTokenCheck.php
new file mode 100644
index 0000000..c32ee41
--- /dev/null
+++ b/src/think/middleware/FormTokenCheck.php
@@ -0,0 +1,44 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\middleware;
+
+use Closure;
+use think\exception\ValidateException;
+use think\Request;
+use think\Response;
+
+/**
+ * 表单令牌支持
+ */
+class FormTokenCheck
+{
+
+ /**
+ * 表单令牌检测
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @param string $token 表单令牌Token名称
+ * @return Response
+ */
+ public function handle(Request $request, Closure $next, ?string $token = null): Response
+ {
+ $check = $request->checkToken($token ?: '__token__');
+
+ if (false === $check) {
+ throw new ValidateException('invalid token');
+ }
+
+ return $next($request);
+ }
+}
diff --git a/src/think/middleware/LoadLangPack.php b/src/think/middleware/LoadLangPack.php
new file mode 100644
index 0000000..16b14dc
--- /dev/null
+++ b/src/think/middleware/LoadLangPack.php
@@ -0,0 +1,113 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\middleware;
+
+use Closure;
+use think\App;
+use think\Config;
+use think\Cookie;
+use think\Lang;
+use think\Request;
+use think\Response;
+
+/**
+ * 多语言加载
+ */
+class LoadLangPack
+{
+ protected $config;
+
+ public function __construct(protected App $app, protected Lang $lang, Config $config)
+ {
+ $this->config = $lang->getConfig();
+ }
+
+ /**
+ * 路由初始化(路由规则注册)
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @return Response
+ */
+ public function handle(Request $request, Closure $next): Response
+ {
+ // 自动侦测当前语言
+ $langset = $this->detect($request);
+
+ if ($this->lang->defaultLangSet() != $langset) {
+ $this->lang->switchLangSet($langset);
+ }
+
+ $this->saveToCookie($this->app->cookie, $langset);
+
+ return $next($request);
+ }
+
+ /**
+ * 自动侦测设置获取语言选择
+ * @access protected
+ * @param Request $request
+ * @return string
+ */
+ protected function detect(Request $request): string
+ {
+ // 自动侦测设置获取语言选择
+ $langSet = '';
+
+ if ($request->get($this->config['detect_var'])) {
+ // url中设置了语言变量
+ $langSet = $request->get($this->config['detect_var']);
+ } elseif ($request->header($this->config['header_var'])) {
+ // Header中设置了语言变量
+ $langSet = $request->header($this->config['header_var']);
+ } elseif ($request->cookie($this->config['cookie_var'])) {
+ // Cookie中设置了语言变量
+ $langSet = $request->cookie($this->config['cookie_var']);
+ } elseif ($this->config['auto_detect_browser'] && $request->server('HTTP_ACCEPT_LANGUAGE')) {
+ // 自动侦测浏览器语言
+ $langSet = $request->server('HTTP_ACCEPT_LANGUAGE');
+ }
+
+ if (preg_match('/^([a-z\d\-]+)/i', $langSet, $matches)) {
+ $langSet = strtolower($matches[1]);
+ if (isset($this->config['accept_language'][$langSet])) {
+ $langSet = $this->config['accept_language'][$langSet];
+ }
+ } else {
+ $langSet = $this->lang->getLangSet();
+ }
+
+ if (empty($this->config['allow_lang_list']) || in_array($langSet, $this->config['allow_lang_list'])) {
+ // 合法的语言
+ $this->lang->setLangSet($langSet);
+ } else {
+ $langSet = $this->lang->getLangSet();
+ }
+
+ return $langSet;
+ }
+
+ /**
+ * 保存当前语言到Cookie
+ * @access protected
+ * @param Cookie $cookie Cookie对象
+ * @param string $langSet 语言
+ * @return void
+ */
+ protected function saveToCookie(Cookie $cookie, string $langSet): void
+ {
+ if ($this->config['use_cookie']) {
+ $cookie->set($this->config['cookie_var'], $langSet);
+ }
+ }
+}
diff --git a/src/think/middleware/SessionInit.php b/src/think/middleware/SessionInit.php
new file mode 100644
index 0000000..500cc19
--- /dev/null
+++ b/src/think/middleware/SessionInit.php
@@ -0,0 +1,71 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\middleware;
+
+use Closure;
+use think\App;
+use think\Request;
+use think\Response;
+use think\Session;
+
+/**
+ * Session初始化
+ */
+class SessionInit
+{
+ public function __construct(protected App $app, protected Session $session)
+ {
+ }
+
+ /**
+ * Session初始化
+ * @access public
+ * @param Request $request
+ * @param Closure $next
+ * @return Response
+ */
+ public function handle(Request $request, Closure $next): Response
+ {
+ // Session初始化
+ $varSessionId = $this->app->config->get('session.var_session_id');
+ $cookieName = $this->session->getName();
+
+ if ($varSessionId && $request->request($varSessionId)) {
+ $sessionId = $request->request($varSessionId);
+ } else {
+ $sessionId = $request->cookie($cookieName);
+ }
+
+ if ($sessionId) {
+ $this->session->setId($sessionId);
+ }
+
+ $this->session->init();
+
+ $request->withSession($this->session);
+
+ /** @var Response $response */
+ $response = $next($request);
+
+ $response->setSession($this->session);
+
+ $this->app->cookie->set($cookieName, $this->session->getId(), $this->session->getConfig('expire'));
+
+ return $response;
+ }
+
+ public function end(Response $response): void
+ {
+ $this->session->save();
+ }
+}
diff --git a/src/think/response/File.php b/src/think/response/File.php
new file mode 100644
index 0000000..85d5134
--- /dev/null
+++ b/src/think/response/File.php
@@ -0,0 +1,161 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\response;
+
+use think\Exception;
+use think\Response;
+
+/**
+ * File Response
+ */
+class File extends Response
+{
+ protected $expire = 360;
+ protected $name;
+ protected $mimeType;
+ protected $isContent = false;
+ protected $force = true;
+
+ public function __construct($data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ * @throws \Exception
+ */
+ protected function output($data)
+ {
+ if (!$this->isContent && !is_file($data)) {
+ throw new Exception('file not exists:' . $data);
+ }
+
+ while (ob_get_level() > 0) {
+ ob_end_clean();
+ }
+
+ if (!empty($this->name)) {
+ $name = $this->name;
+ } else {
+ $name = !$this->isContent ? pathinfo($data, PATHINFO_BASENAME) : '';
+ }
+ $name = urlencode($name); // 支持中文名称
+
+ if ($this->isContent) {
+ $mimeType = $this->mimeType;
+ $size = strlen($data);
+ } else {
+ $mimeType = $this->getMimeType($data);
+ $size = filesize($data);
+ }
+
+ $this->header['Pragma'] = 'public';
+ $this->header['Content-Type'] = $mimeType ?: 'application/octet-stream';
+ $this->header['Cache-control'] = 'max-age=' . $this->expire;
+ $this->header['Content-Disposition'] = ($this->force ? 'attachment; ' : '') . 'filename="' . $name . '";' . "filename* = UTF-8''{$name}";
+ $this->header['Content-Length'] = $size;
+ $this->header['Content-Transfer-Encoding'] = 'binary';
+ $this->header['Expires'] = gmdate("D, d M Y H:i:s", time() + $this->expire) . ' GMT';
+
+ $this->lastModified(gmdate('D, d M Y H:i:s', time()) . ' GMT');
+
+ return $this->isContent ? $data : file_get_contents($data);
+ }
+
+ /**
+ * 设置是否为内容 必须配合mimeType方法使用
+ * @access public
+ * @param bool $content
+ * @return $this
+ */
+ public function isContent(bool $content = true)
+ {
+ $this->isContent = $content;
+ return $this;
+ }
+
+ /**
+ * 设置有效期
+ * @access public
+ * @param integer $expire 有效期
+ * @return $this
+ */
+ public function expire(int $expire)
+ {
+ $this->expire = $expire;
+ return $this;
+ }
+
+ /**
+ * 设置文件类型
+ * @access public
+ * @param string $filename 文件名
+ * @return $this
+ */
+ public function mimeType(string $mimeType)
+ {
+ $this->mimeType = $mimeType;
+ return $this;
+ }
+
+ /**
+ * 设置文件强制下载
+ * @access public
+ * @param bool $force 强制浏览器下载
+ * @return $this
+ */
+ public function force(bool $force)
+ {
+ $this->force = $force;
+ return $this;
+ }
+
+ /**
+ * 获取文件类型信息
+ * @access public
+ * @param string $filename 文件名
+ * @return string
+ */
+ protected function getMimeType(string $filename): string
+ {
+ if (!empty($this->mimeType)) {
+ return $this->mimeType;
+ }
+
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+
+ return finfo_file($finfo, $filename);
+ }
+
+ /**
+ * 设置下载文件的显示名称
+ * @access public
+ * @param string $filename 文件名
+ * @param bool $extension 后缀自动识别
+ * @return $this
+ */
+ public function name(string $filename, bool $extension = true)
+ {
+ $this->name = $filename;
+
+ if ($extension && !str_contains($filename, '.')) {
+ $this->name .= '.' . pathinfo($this->data, PATHINFO_EXTENSION);
+ }
+
+ return $this;
+ }
+}
diff --git a/src/think/response/Html.php b/src/think/response/Html.php
new file mode 100644
index 0000000..291be20
--- /dev/null
+++ b/src/think/response/Html.php
@@ -0,0 +1,34 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Response;
+
+/**
+ * Html Response
+ */
+class Html extends Response
+{
+ /**
+ * 输出type
+ * @var string
+ */
+ protected $contentType = 'text/html';
+
+ public function __construct(Cookie $cookie, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+ $this->cookie = $cookie;
+ }
+}
diff --git a/src/think/response/Json.php b/src/think/response/Json.php
new file mode 100644
index 0000000..ed245aa
--- /dev/null
+++ b/src/think/response/Json.php
@@ -0,0 +1,61 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Response;
+
+/**
+ * Json Response
+ */
+class Json extends Response
+{
+ // 输出参数
+ protected $options = [
+ 'json_encode_param' => JSON_UNESCAPED_UNICODE,
+ ];
+
+ protected $contentType = 'application/json';
+
+ public function __construct(Cookie $cookie, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+ $this->cookie = $cookie;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return string
+ * @throws \Exception
+ */
+ protected function output($data): string
+ {
+ try {
+ // 返回JSON数据格式到客户端 包含状态信息
+ $data = json_encode($data, $this->options['json_encode_param']);
+
+ if (false === $data) {
+ throw new \InvalidArgumentException(json_last_error_msg());
+ }
+
+ return $data;
+ } catch (\Exception $e) {
+ if ($e->getPrevious()) {
+ throw $e->getPrevious();
+ }
+ throw $e;
+ }
+ }
+}
diff --git a/src/think/response/Jsonp.php b/src/think/response/Jsonp.php
new file mode 100644
index 0000000..9762998
--- /dev/null
+++ b/src/think/response/Jsonp.php
@@ -0,0 +1,73 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Request;
+use think\Response;
+
+/**
+ * Jsonp Response
+ */
+class Jsonp extends Response
+{
+ // 输出参数
+ protected $options = [
+ 'var_jsonp_handler' => 'callback',
+ 'default_jsonp_handler' => 'jsonpReturn',
+ 'json_encode_param' => JSON_UNESCAPED_UNICODE,
+ ];
+
+ protected $contentType = 'application/javascript';
+
+ protected $request;
+
+ public function __construct(Cookie $cookie, Request $request, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+
+ $this->cookie = $cookie;
+ $this->request = $request;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return string
+ * @throws \Exception
+ */
+ protected function output($data): string
+ {
+ try {
+ // 返回JSON数据格式到客户端 包含状态信息 [当url_common_param为false时是无法获取到$_GET的数据的,故使用Request来获取]
+ $varJsonpHandler = $this->request->param($this->options['var_jsonp_handler'], "");
+ $handler = !empty($varJsonpHandler) ? $varJsonpHandler : $this->options['default_jsonp_handler'];
+
+ $data = json_encode($data, $this->options['json_encode_param']);
+
+ if (false === $data) {
+ throw new \InvalidArgumentException(json_last_error_msg());
+ }
+
+ $data = $handler . '(' . $data . ');';
+
+ return $data;
+ } catch (\Exception $e) {
+ if ($e->getPrevious()) {
+ throw $e->getPrevious();
+ }
+ throw $e;
+ }
+ }
+}
diff --git a/src/think/response/Redirect.php b/src/think/response/Redirect.php
new file mode 100644
index 0000000..b23020a
--- /dev/null
+++ b/src/think/response/Redirect.php
@@ -0,0 +1,102 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Request;
+use think\Response;
+use think\Session;
+
+/**
+ * Redirect Response
+ */
+class Redirect extends Response
+{
+
+ protected $request;
+
+ public function __construct(Cookie $cookie, Request $request, Session $session, $data = '', int $code = 302)
+ {
+ $this->init((string) $data, $code);
+
+ $this->cookie = $cookie;
+ $this->request = $request;
+ $this->session = $session;
+
+ $this->cacheControl('no-cache,must-revalidate');
+ }
+
+ public function data($data)
+ {
+ $this->header['Location'] = $data;
+ return parent::data($data);
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return string
+ */
+ protected function output($data): string
+ {
+ return '';
+ }
+
+ /**
+ * 重定向传值(通过Session)
+ * @access protected
+ * @param string|array $name 变量名或者数组
+ * @param mixed $value 值
+ * @return $this
+ */
+ public function with($name, $value = null)
+ {
+ if (is_array($name)) {
+ foreach ($name as $key => $val) {
+ $this->session->flash($key, $val);
+ }
+ } else {
+ $this->session->flash($name, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 记住当前url后跳转
+ * @access public
+ * @return $this
+ */
+ public function remember($complete = false)
+ {
+ $this->session->set('redirect_url', $this->request->url($complete));
+
+ return $this;
+ }
+
+ /**
+ * 跳转到上次记住的url
+ * @access public
+ * @return $this
+ */
+ public function restore()
+ {
+ if ($this->session->has('redirect_url')) {
+ $this->data($this->session->get('redirect_url'));
+ $this->session->delete('redirect_url');
+ }
+
+ return $this;
+ }
+}
diff --git a/src/think/response/View.php b/src/think/response/View.php
new file mode 100644
index 0000000..42d434d
--- /dev/null
+++ b/src/think/response/View.php
@@ -0,0 +1,150 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\response;
+
+use think\Cookie;
+use think\Response;
+use think\View as BaseView;
+
+/**
+ * View Response
+ */
+class View extends Response
+{
+ /**
+ * 输出参数
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * 输出变量
+ * @var array
+ */
+ protected $vars = [];
+
+ /**
+ * 输出过滤
+ * @var mixed
+ */
+ protected $filter;
+
+ /**
+ * 输出type
+ * @var string
+ */
+ protected $contentType = 'text/html';
+
+ /**
+ * View对象
+ * @var BaseView
+ */
+ protected $view;
+
+ /**
+ * 是否内容渲染
+ * @var bool
+ */
+ protected $isContent = false;
+
+ public function __construct(Cookie $cookie, BaseView $view, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+
+ $this->cookie = $cookie;
+ $this->view = $view;
+ }
+
+ /**
+ * 设置是否为内容渲染
+ * @access public
+ * @param bool $content
+ * @return $this
+ */
+ public function isContent(bool $content = true)
+ {
+ $this->isContent = $content;
+ return $this;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return string
+ */
+ protected function output($data): string
+ {
+ // 渲染模板输出
+ $this->view->filter($this->filter);
+ return $this->isContent ?
+ $this->view->display($data, $this->vars) :
+ $this->view->fetch($data, $this->vars);
+ }
+
+ /**
+ * 获取视图变量
+ * @access public
+ * @param string|null $name 模板变量
+ * @return mixed
+ */
+ public function getVars(?string $name = null)
+ {
+ if (is_null($name)) {
+ return $this->vars;
+ } else {
+ return $this->vars[$name] ?? null;
+ }
+ }
+
+ /**
+ * 模板变量赋值
+ * @access public
+ * @param string|array $name 模板变量
+ * @param mixed $value 变量值
+ * @return $this
+ */
+ public function assign(string|array $name, $value = null)
+ {
+ if (is_array($name)) {
+ $this->vars = array_merge($this->vars, $name);
+ } else {
+ $this->vars[$name] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 视图内容过滤
+ * @access public
+ * @param callable|null $filter
+ * @return $this
+ */
+ public function filter(?callable $filter = null)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * 检查模板是否存在
+ * @access public
+ * @param string $name 模板名
+ * @return bool
+ */
+ public function exists(string $name): bool
+ {
+ return $this->view->exists($name);
+ }
+}
diff --git a/src/think/response/Xml.php b/src/think/response/Xml.php
new file mode 100644
index 0000000..168fecf
--- /dev/null
+++ b/src/think/response/Xml.php
@@ -0,0 +1,127 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\response;
+
+use think\Collection;
+use think\Cookie;
+use think\Model;
+use think\Response;
+
+/**
+ * XML Response
+ */
+class Xml extends Response
+{
+ // 输出参数
+ protected $options = [
+ // 根节点名
+ 'root_node' => 'think',
+ // 根节点属性
+ 'root_attr' => '',
+ //数字索引的子节点名
+ 'item_node' => 'item',
+ // 数字索引子节点key转换的属性名
+ 'item_key' => 'id',
+ // 数据编码
+ 'encoding' => 'utf-8',
+ ];
+
+ protected $contentType = 'text/xml';
+
+ public function __construct(Cookie $cookie, $data = '', int $code = 200)
+ {
+ $this->init($data, $code);
+ $this->cookie = $cookie;
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ */
+ protected function output($data): string
+ {
+ if (is_string($data)) {
+ if (!str_starts_with($data, 'options['encoding'];
+ $xml = "";
+ $data = $xml . $data;
+ }
+ return $data;
+ }
+
+ // XML数据转换
+ return $this->xmlEncode($data, $this->options['root_node'], $this->options['item_node'], $this->options['root_attr'], $this->options['item_key'], $this->options['encoding']);
+ }
+
+ /**
+ * XML编码
+ * @access protected
+ * @param mixed $data 数据
+ * @param string $root 根节点名
+ * @param string $item 数字索引的子节点名
+ * @param mixed $attr 根节点属性
+ * @param string $id 数字索引子节点key转换的属性名
+ * @param string $encoding 数据编码
+ * @return string
+ */
+ protected function xmlEncode($data, string $root, string $item, $attr, string $id, string $encoding): string
+ {
+ if (is_array($attr)) {
+ $array = [];
+ foreach ($attr as $key => $value) {
+ $array[] = "{$key}=\"{$value}\"";
+ }
+ $attr = implode(' ', $array);
+ }
+
+ $attr = trim($attr);
+ $attr = empty($attr) ? '' : " {$attr}";
+ $xml = "";
+ $xml .= "<{$root}{$attr}>";
+ $xml .= $this->dataToXml($data, $item, $id);
+ $xml .= "{$root}>";
+
+ return $xml;
+ }
+
+ /**
+ * 数据XML编码
+ * @access protected
+ * @param mixed $data 数据
+ * @param string $item 数字索引时的节点名称
+ * @param string $id 数字索引key转换为的属性名
+ * @return string
+ */
+ protected function dataToXml($data, string $item, string $id): string
+ {
+ $xml = $attr = '';
+
+ if ($data instanceof Collection || $data instanceof Model) {
+ $data = $data->toArray();
+ }
+
+ foreach ($data as $key => $val) {
+ if (is_numeric($key)) {
+ $id && $attr = " {$id}=\"{$key}\"";
+ $key = $item;
+ }
+ $xml .= "<{$key}{$attr}>";
+ $xml .= (is_array($val) || is_object($val)) ? $this->dataToXml($val, $item, $id) : $val;
+ $xml .= "{$key}>";
+ }
+
+ return $xml;
+ }
+}
diff --git a/src/think/route/Dispatch.php b/src/think/route/Dispatch.php
new file mode 100644
index 0000000..bde9889
--- /dev/null
+++ b/src/think/route/Dispatch.php
@@ -0,0 +1,340 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use Psr\Http\Message\ResponseInterface;
+use ReflectionClass;
+use ReflectionException;
+use ReflectionMethod;
+use think\App;
+use think\Container;
+use think\exception\HttpException;
+use think\Request;
+use think\Response;
+use think\Validate;
+
+/**
+ * 路由调度基础类
+ */
+abstract class Dispatch
+{
+ /**
+ * 应用对象
+ * @var App
+ */
+ protected $app;
+
+ public function __construct(protected Request $request, protected Rule $rule, protected $dispatch, protected array $param = [], protected array $option = [], protected ?RuleItem $miss = null)
+ {
+ }
+
+ /**
+ * 执行路由调度
+ * @access public
+ * @return Response
+ */
+ public function run(): Response
+ {
+ $data = $this->exec();
+ return $this->autoResponse($data);
+ }
+
+ protected function autoResponse($data): Response
+ {
+ if ($data instanceof Response) {
+ $response = $data;
+ } elseif ($data instanceof ResponseInterface) {
+ $response = Response::create((string) $data->getBody(), 'html', $data->getStatusCode());
+
+ foreach ($data->getHeaders() as $header => $values) {
+ $response->header([$header => implode(", ", $values)]);
+ }
+ } elseif (!is_null($data)) {
+ // 默认自动识别响应输出类型
+ $type = $this->request->isJson() ? 'json' : 'html';
+ $response = Response::create($data, $type);
+ } else {
+ $data = ob_get_clean();
+
+ $content = false === $data ? '' : $data;
+ $status = '' === $content && $this->request->isJson() ? 204 : 200;
+ $response = Response::create($content, 'html', $status);
+ }
+
+ return $response;
+ }
+
+ /**
+ * 检查路由后置操作
+ * @access protected
+ * @return void
+ */
+ protected function doRouteAfter(): void
+ {
+ $option = $this->option;
+
+ // 添加中间件
+ if (!empty($option['middleware'])) {
+ if (isset($option['without_middleware'])) {
+ $middleware = !empty($option['without_middleware']) ? array_diff($option['middleware'], $option['without_middleware']) : [];
+ } else {
+ $middleware = $option['middleware'];
+ }
+ $this->app->middleware->import($middleware, 'route');
+ }
+
+ if (!empty($option['append'])) {
+ $this->param = array_merge($this->param, $option['append']);
+ }
+
+ // 绑定模型数据
+ if (!empty($option['model'])) {
+ $this->createBindModel($option['model'], $this->param);
+ }
+
+ // 记录当前请求的路由规则
+ $this->request->setRule($this->rule);
+
+ // 记录路由变量
+ $this->request->setRoute($this->param);
+
+ // 数据自动验证
+ if (isset($option['validate'])) {
+ $this->autoValidate($option['validate']);
+ }
+ }
+
+ /**
+ * 获取操作的绑定参数
+ * @access protected
+ * @return array
+ */
+ protected function getActionBindVars(): array
+ {
+ $bind = $this->rule->config('action_bind_param');
+ return match ($bind) {
+ 'route' => $this->param,
+ 'param' => $this->request->param(),
+ default => array_merge($this->request->get(), $this->param),
+ };
+ }
+
+ /**
+ * 执行中间件调度
+ * @access public
+ * @param object $controller 控制器实例
+ * @return void
+ */
+ protected function responseWithMiddlewarePipeline($instance, $action)
+ {
+ // 注册控制器中间件
+ $this->registerControllerMiddleware($instance);
+ return $this->app->middleware->pipeline('controller')
+ ->send($this->request)
+ ->then(function () use ($instance, $action) {
+ // 获取当前操作名
+ $suffix = $this->rule->config('action_suffix');
+ $action = $action . $suffix;
+
+ if (is_callable([$instance, $action])) {
+ $vars = $this->getActionBindVars();
+ try {
+ $reflect = new ReflectionMethod($instance, $action);
+ // 严格获取当前操作方法名
+ $actionName = $reflect->getName();
+ if ($suffix) {
+ $actionName = substr($actionName, 0, -strlen($suffix));
+ }
+
+ $this->request->setAction($actionName);
+ } catch (ReflectionException $e) {
+ $reflect = new ReflectionMethod($instance, '__call');
+ $vars = [$action, $vars];
+ $this->request->setAction($action);
+ }
+ } else {
+ // 操作不存在
+ throw new HttpException(404, 'method not exists:' . $instance::class . '->' . $action . '()');
+ }
+
+ $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
+
+ return $this->autoResponse($data);
+ });
+ }
+
+ /**
+ * 使用反射机制注册控制器中间件
+ * @access public
+ * @param object $controller 控制器实例
+ * @return void
+ */
+ protected function registerControllerMiddleware($controller): void
+ {
+ $class = new ReflectionClass($controller);
+
+ if ($class->hasProperty('middleware')) {
+ $reflectionProperty = $class->getProperty('middleware');
+ $reflectionProperty->setAccessible(true);
+
+ $middlewares = $reflectionProperty->getValue($controller);
+ $action = $this->request->action(true);
+
+ foreach ($middlewares as $key => $val) {
+ if (!is_int($key)) {
+ $middleware = $key;
+ $options = $val;
+ } elseif (isset($val['middleware'])) {
+ $middleware = $val['middleware'];
+ $options = $val['options'] ?? [];
+ } else {
+ $middleware = $val;
+ $options = [];
+ }
+
+ if (isset($options['only']) && !in_array($action, $this->parseActions($options['only']))) {
+ continue;
+ } elseif (isset($options['except']) && in_array($action, $this->parseActions($options['except']))) {
+ continue;
+ }
+
+ if (is_string($middleware) && str_contains($middleware, ':')) {
+ $middleware = explode(':', $middleware);
+ if (count($middleware) > 1) {
+ $middleware = [$middleware[0], array_slice($middleware, 1)];
+ }
+ }
+
+ $this->app->middleware->controller($middleware);
+ }
+ }
+ }
+
+ protected function parseActions($actions)
+ {
+ return array_map(function ($item) {
+ return strtolower($item);
+ }, is_string($actions) ? explode(',', $actions) : $actions);
+ }
+
+ /**
+ * 路由绑定模型实例
+ * @access protected
+ * @param array $bindModel 绑定模型
+ * @param array $matches 路由变量
+ * @return void
+ */
+ protected function createBindModel(array $bindModel, array $matches): void
+ {
+ foreach ($bindModel as $key => $val) {
+ if ($val instanceof \Closure) {
+ $result = $this->app->invokeFunction($val, $matches);
+ } else {
+ $fields = explode('&', $key);
+
+ if (is_array($val)) {
+ [$model, $exception] = $val;
+ } else {
+ $model = $val;
+ $exception = true;
+ }
+
+ $where = [];
+ $match = true;
+
+ foreach ($fields as $field) {
+ if (!isset($matches[$field])) {
+ $match = false;
+ break;
+ } else {
+ $where[] = [$field, '=', $matches[$field]];
+ }
+ }
+
+ if ($match) {
+ $result = $model::where($where)->failException($exception)->find();
+ }
+ }
+
+ if (!empty($result)) {
+ // 注入容器
+ $this->app->instance($result::class, $result);
+ }
+ }
+ }
+
+ /**
+ * 验证数据
+ * @access protected
+ * @param array $option
+ * @return void
+ * @throws \think\exception\ValidateException
+ */
+ protected function autoValidate(array $option): void
+ {
+ [$validate, $scene, $message, $batch] = $option;
+
+ if (is_array($validate)) {
+ // 指定验证规则
+ $v = new Validate();
+ $v->rule($validate);
+ } else {
+ // 调用验证器
+ $class = str_contains($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
+
+ $v = new $class();
+
+ if (!empty($scene)) {
+ $v->scene($scene);
+ }
+ }
+
+ /** @var Validate $v */
+ $v->message($message)
+ ->batch($batch)
+ ->failException(true)
+ ->check($this->request->param());
+ }
+
+ public function getDispatch()
+ {
+ return $this->dispatch;
+ }
+
+ public function getParam(): array
+ {
+ return $this->param;
+ }
+
+ abstract public function exec();
+
+ public function __sleep()
+ {
+ return ['rule', 'dispatch', 'param', 'controller', 'actionName'];
+ }
+
+ public function __wakeup()
+ {
+ $this->app = Container::pull('app');
+ $this->request = $this->app->request;
+ }
+
+ public function __debugInfo()
+ {
+ return [
+ 'dispatch' => $this->dispatch,
+ 'param' => $this->param,
+ 'rule' => $this->rule,
+ ];
+ }
+}
diff --git a/src/think/route/Domain.php b/src/think/route/Domain.php
new file mode 100644
index 0000000..705c23c
--- /dev/null
+++ b/src/think/route/Domain.php
@@ -0,0 +1,84 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use Closure;
+use think\Route;
+use think\Container;
+
+/**
+ * 域名路由
+ */
+class Domain extends RuleGroup
+{
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由对象
+ * @param string $name 路由域名
+ * @param mixed $rule 域名路由
+ * @param bool $lazy 延迟解析
+ */
+ public function __construct(Route $router, ?string $name = null, $rule = null, bool $lazy = false)
+ {
+ $this->router = $router;
+ $this->domain = $name;
+ $this->rule = $rule;
+
+ if (!$lazy && !is_null($rule)) {
+ $this->parseGroupRule($rule);
+ }
+ }
+
+ /**
+ * 解析分组和域名的路由规则及绑定
+ * @access public
+ * @param mixed $rule 路由规则
+ * @return void
+ */
+ public function parseGroupRule($rule): void
+ {
+ $origin = $this->router->getGroup();
+ $this->router->setGroup($this);
+
+ if ($rule instanceof Closure) {
+ Container::getInstance()->invokeFunction($rule);
+ } elseif ($this->config('route_auto_group')) {
+ $this->loadGroupRoutes();
+ }
+
+ $this->router->setGroup($origin);
+ $this->hasParsed = true;
+ }
+
+ /**
+ * 自动加载分组(子目录)路由
+ * @access protected
+ * @param string $dir 目录名
+ * @return void
+ */
+ protected function loadGroupRoutes(): void
+ {
+ $routePath = root_path('route');
+ if (is_dir($routePath)) {
+ $dirs = glob($routePath . '*', GLOB_ONLYDIR);
+ foreach ($dirs as $dir) {
+ // 自动检查分组子目录
+ $groupName = str_replace('\\', '/', substr_replace($dir, '', 0, strlen($routePath)));
+ if (!$this->router->getRuleName()->hasGroup($groupName)) {
+ $this->router->group($groupName);
+ }
+ }
+ }
+ }
+}
diff --git a/src/think/route/Resource.php b/src/think/route/Resource.php
new file mode 100644
index 0000000..fe1a473
--- /dev/null
+++ b/src/think/route/Resource.php
@@ -0,0 +1,263 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use Closure;
+use think\Container;
+use think\Route;
+
+/**
+ * 资源路由类
+ */
+class Resource extends RuleGroup
+{
+ /**
+ * REST方法定义
+ * @var array
+ */
+ protected $rest = [];
+
+ /**
+ * 模型绑定
+ * @var array
+ */
+ protected $model = [];
+
+ /**
+ * 数据验证
+ * @var array
+ */
+ protected $validate = [];
+
+ /**
+ * 中间件
+ * @var array
+ */
+ protected $middleware = [];
+
+ /**
+ * 扩展规则
+ * @var Closure
+ */
+ protected $extend;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由对象
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 资源名称
+ * @param string $route 路由地址
+ * @param array $rest 资源定义
+ */
+ public function __construct(Route $router, ?RuleGroup $parent = null, string $name = '', string $route = '', array $rest = [])
+ {
+ $name = ltrim($name, '/');
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->rule = $name;
+ $this->route = $route;
+ $this->name = str_contains($name, '.') ? strstr($name, '.', true) : $name;
+
+ $this->setFullName();
+
+ // 资源路由默认为完整匹配
+ $this->option['complete_match'] = true;
+
+ $this->rest = $rest;
+
+ if ($this->parent) {
+ $this->domain = $this->parent->getDomain();
+ $this->parent->addRuleItem($this);
+ }
+ }
+
+ /**
+ * 扩展额外路由规则
+ * @access public
+ * @param Closure $extend 路由规则闭包定义
+ * @return $this
+ */
+ public function extend(?Closure $extend)
+ {
+ $this->extend = $extend;
+ return $this;
+ }
+
+ /**
+ * 生成资源路由规则
+ * @access public
+ * @param mixed $rule 路由规则
+ * @return void
+ */
+ public function parseGroupRule($rule): void
+ {
+ $option = $this->option;
+ $origin = $this->router->getGroup();
+ $this->router->setGroup($this);
+
+ if (str_contains($rule, '.')) {
+ // 注册嵌套资源路由
+ $array = explode('.', $rule);
+ $last = array_pop($array);
+ $item = [];
+
+ foreach ($array as $val) {
+ $item[] = $val . '/<' . ($option['var'][$val] ?? $val . '_id') . '>';
+ }
+
+ $rule = implode('/', $item) . '/' . $last;
+ $id = $option['var'][$last] ?? 'id';
+ } else {
+ $id = $option['var'][$rule] ?? 'id';
+ }
+
+ $prefix = substr($rule, strlen($this->name) + 1);
+
+ // 注册资源路由
+ foreach ($this->rest as $key => $val) {
+ if ((isset($option['only']) && !in_array($key, $option['only']))
+ || (isset($option['except']) && in_array($key, $option['except']))
+ ) {
+ continue;
+ }
+
+ if (str_contains($val[1], '') && 'id' != $id) {
+ $val[1] = str_replace('', '<' . $id . '>', $val[1]);
+ }
+
+ $ruleItem = $this->addRule(trim($prefix . $val[1], '/'), $this->route . '/' . $val[2], $val[0]);
+
+ foreach (['model', 'validate', 'middleware', 'pattern'] as $name) {
+ if (isset($this->$name[$key])) {
+ call_user_func_array([$ruleItem, $name], (array) $this->$name[$key]);
+ }
+ }
+ }
+
+ if ($this->extend) {
+ // 扩展路由规则
+ $group = new RuleGroup($this->router, $this, $prefix . '/<' . $id . '>');
+ $this->router->setGroup($group);
+ Container::getInstance()->invokeFunction($this->extend);
+ }
+
+ $this->router->setGroup($origin);
+ $this->hasParsed = true;
+ }
+
+ /**
+ * 设置资源允许
+ * @access public
+ * @param array $only 资源允许
+ * @return $this
+ */
+ public function only(array $only)
+ {
+ return $this->setOption('only', $only);
+ }
+
+ /**
+ * 设置资源排除
+ * @access public
+ * @param array $except 排除资源
+ * @return $this
+ */
+ public function except(array $except)
+ {
+ return $this->setOption('except', $except);
+ }
+
+ /**
+ * 设置资源路由的变量
+ * @access public
+ * @param array $vars 资源变量
+ * @return $this
+ */
+ public function vars(array $vars)
+ {
+ return $this->setOption('var', $vars);
+ }
+
+ /**
+ * 绑定资源验证
+ * @access public
+ * @param array|string $name 资源类型或者验证信息
+ * @param array|string $validate 验证信息
+ * @return $this
+ */
+ public function withValidate(array | string $name, array | string $validate = [])
+ {
+ if (is_array($name)) {
+ $this->validate = array_merge($this->validate, $name);
+ } else {
+ $this->validate[$name] = $validate;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 绑定资源模型
+ * @access public
+ * @param array|string $name 资源类型或者模型绑定
+ * @param array|string $model 模型绑定
+ * @return $this
+ */
+ public function withModel(array | string $name, array | string $model = [])
+ {
+ if (is_array($name)) {
+ $this->model = array_merge($this->model, $name);
+ } else {
+ $this->model[$name] = $model;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 绑定资源中间件
+ * @access public
+ * @param array|string $name 资源类型或者中间件定义
+ * @param array|string $middleware 中间件定义
+ * @return $this
+ */
+ public function withMiddleware(array | string $name, array | string $middleware = [])
+ {
+ if (is_array($name)) {
+ $this->middleware = array_merge($this->middleware, $name);
+ } else {
+ $this->middleware[$name] = $middleware;
+ }
+
+ return $this;
+ }
+
+ /**
+ * rest方法定义和修改
+ * @access public
+ * @param array|string $name 方法名称
+ * @param array|bool $resource 资源
+ * @return $this
+ */
+ public function rest(array | string $name, array | bool $resource = [])
+ {
+ if (is_array($name)) {
+ $this->rest = $resource ? $name : array_merge($this->rest, $name);
+ } else {
+ $this->rest[$name] = $resource;
+ }
+
+ return $this;
+ }
+}
diff --git a/src/think/route/ResourceRegister.php b/src/think/route/ResourceRegister.php
new file mode 100644
index 0000000..446a84c
--- /dev/null
+++ b/src/think/route/ResourceRegister.php
@@ -0,0 +1,72 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+/**
+ * 资源路由注册类
+ */
+class ResourceRegister
+{
+ /**
+ * 资源路由
+ * @var Resource
+ */
+ protected $resource;
+
+ /**
+ * 是否注册过
+ * @var bool
+ */
+ protected $registered = false;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Resource $resource 资源路由
+ */
+ public function __construct(Resource $resource)
+ {
+ $this->resource = $resource;
+ }
+
+ /**
+ * 注册资源路由
+ * @access protected
+ * @return void
+ */
+ protected function register()
+ {
+ $this->registered = true;
+
+ $this->resource->parseGroupRule($this->resource->getRule());
+ }
+
+ /**
+ * 动态方法
+ * @access public
+ * @param string $method 方法名
+ * @param array $args 调用参数
+ * @return mixed
+ */
+ public function __call($method, $args)
+ {
+ return call_user_func_array([$this->resource, $method], $args);
+ }
+
+ public function __destruct()
+ {
+ if (!$this->registered) {
+ $this->register();
+ }
+ }
+}
diff --git a/src/think/route/Rule.php b/src/think/route/Rule.php
new file mode 100644
index 0000000..73d7dd7
--- /dev/null
+++ b/src/think/route/Rule.php
@@ -0,0 +1,1055 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use Closure;
+use think\Container;
+use think\middleware\AllowCrossDomain;
+use think\middleware\CheckRequestCache;
+use think\middleware\FormTokenCheck;
+use think\Request;
+use think\Route;
+use think\route\dispatch\Callback as CallbackDispatch;
+use think\route\dispatch\Controller as ControllerDispatch;
+
+/**
+ * 路由规则基础类
+ */
+abstract class Rule
+{
+ /**
+ * 路由标识
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * 所在域名
+ * @var string
+ */
+ protected $domain;
+
+ /**
+ * 路由对象
+ * @var Route
+ */
+ protected $router;
+
+ /**
+ * 路由所属分组
+ * @var RuleGroup
+ */
+ protected $parent;
+
+ /**
+ * 路由规则
+ * @var mixed
+ */
+ protected $rule;
+
+ /**
+ * 路由地址
+ * @var string|Closure
+ */
+ protected $route;
+
+ /**
+ * 请求类型
+ * @var string
+ */
+ protected $method = '*';
+
+ /**
+ * 路由变量
+ * @var array
+ */
+ protected $vars = [];
+
+ /**
+ * 路由参数
+ * @var array
+ */
+ protected $option = [];
+
+ /**
+ * 路由变量规则
+ * @var array
+ */
+ protected $pattern = [];
+
+ /**
+ * 预定义变量规则
+ * @var array
+ */
+ protected $regex = [
+ 'int' => '\d+',
+ 'float' => '\d+\.\d+',
+ 'alpha' => '[A-Za-z]+',
+ 'alphaNum' => '[A-Za-z0-9]+',
+ 'alphaDash' => '[A-Za-z0-9\-\_]+',
+ ];
+
+ /**
+ * 需要和分组合并的路由参数
+ * @var array
+ */
+ protected $mergeOptions = ['model', 'append', 'middleware'];
+
+ abstract public function check(Request $request, string $url, bool $completeMatch = false);
+
+ /**
+ * 设置路由参数
+ * @access public
+ * @param array $option 参数
+ * @return $this
+ */
+ public function option(array $option)
+ {
+ $this->option = array_merge($this->option, $option);
+
+ return $this;
+ }
+
+ /**
+ * 设置单个路由参数
+ * @access public
+ * @param string $name 参数名
+ * @param mixed $value 值
+ * @return $this
+ */
+ public function setOption(string $name, $value)
+ {
+ $this->option[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * 注册变量规则
+ * @access public
+ * @param array $regex 变量规则
+ * @return $this
+ */
+ public function regex(array $regex)
+ {
+ $this->regex = array_merge($this->regex, $regex);
+
+ return $this;
+ }
+
+ /**
+ * 注册变量(正则)规则
+ * @access public
+ * @param array $pattern 变量规则
+ * @return $this
+ */
+ public function pattern(array $pattern)
+ {
+ $this->pattern = array_merge($this->pattern, $pattern);
+
+ return $this;
+ }
+
+ /**
+ * 注册路由变量和请求变量的匹配规则(支持验证类的所有内置规则)
+ *
+ * @access public
+ * @param string $name 变量名
+ * @param mixed $rule 变量规则
+ * @return $this
+ */
+ public function when(string | array $name, $rule = null)
+ {
+ if (is_array($name)) {
+ $this->option['var_rule'] = $name;
+ } else {
+ $this->option['var_rule'][$name] = $rule;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置标识
+ * @access public
+ * @param string $name 标识名
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * 获取路由对象
+ * @access public
+ * @return Route
+ */
+ public function getRouter(): Route
+ {
+ return $this->router;
+ }
+
+ /**
+ * 获取Name
+ * @access public
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name ?: '';
+ }
+
+ /**
+ * 获取当前路由规则
+ * @access public
+ * @return mixed
+ */
+ public function getRule()
+ {
+ return $this->rule;
+ }
+
+ /**
+ * 获取当前路由地址
+ * @access public
+ * @return mixed
+ */
+ public function getRoute()
+ {
+ return $this->route;
+ }
+
+ /**
+ * 获取当前路由的变量
+ * @access public
+ * @return array
+ */
+ public function getVars(): array
+ {
+ return $this->vars;
+ }
+
+ /**
+ * 获取Parent对象
+ * @access public
+ * @return $this|null
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 获取路由所在域名
+ * @access public
+ * @return string
+ */
+ public function getDomain(): string
+ {
+ return $this->domain ?: $this->parent->getDomain();
+ }
+
+ /**
+ * 获取路由参数
+ * @access public
+ * @param string $name 变量名
+ * @return mixed
+ */
+ public function config(string $name = '')
+ {
+ return $this->router->config($name);
+ }
+
+ /**
+ * 获取变量规则定义
+ * @access public
+ * @param string $name 变量名
+ * @return mixed
+ */
+ public function getPattern(string $name = '')
+ {
+ $pattern = $this->pattern;
+
+ if ($this->parent) {
+ $pattern = array_merge($this->parent->getPattern(), $pattern);
+ }
+
+ if ('' === $name) {
+ return $pattern;
+ }
+
+ return $pattern[$name] ?? null;
+ }
+
+ /**
+ * 获取路由参数定义
+ * @access public
+ * @param string $name 参数名
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function getOption(string $name = '', $default = null)
+ {
+ $option = $this->option;
+
+ if ($this->parent) {
+ $parentOption = $this->parent->getOption();
+
+ // 合并分组参数
+ foreach ($this->mergeOptions as $item) {
+ $option[$item] = array_merge($parentOption[$item] ?? [], $option[$item] ?? []);
+ }
+
+ $option = array_merge($parentOption, $option);
+ }
+
+ if ('' === $name) {
+ return $option;
+ }
+
+ return $option[$name] ?? $default;
+ }
+
+ /**
+ * 获取当前路由的请求类型
+ * @access public
+ * @return string
+ */
+ public function getMethod(): string
+ {
+ return strtolower($this->method);
+ }
+
+ /**
+ * 设置路由请求类型
+ * @access public
+ * @param string $method 请求类型
+ * @return $this
+ */
+ public function method(string $method)
+ {
+ return $this->setOption('method', strtolower($method));
+ }
+
+ /**
+ * 检查后缀
+ * @access public
+ * @param string $ext URL后缀
+ * @return $this
+ */
+ public function ext(string $ext = '')
+ {
+ return $this->setOption('ext', $ext);
+ }
+
+ /**
+ * 检查禁止后缀
+ * @access public
+ * @param string $ext URL后缀
+ * @return $this
+ */
+ public function denyExt(string $ext = '')
+ {
+ return $this->setOption('deny_ext', $ext);
+ }
+
+ /**
+ * 检查域名
+ * @access public
+ * @param string $domain 域名
+ * @return $this
+ */
+ public function domain(string $domain)
+ {
+ $this->domain = $domain;
+ return $this->setOption('domain', $domain);
+ }
+
+ /**
+ * 是否区分大小写
+ * @access public
+ * @param bool $case 是否区分
+ * @return $this
+ */
+ public function caseUrl(bool $case)
+ {
+ return $this->setOption('case_sensitive', $case);
+ }
+
+ /**
+ * 设置参数过滤检查
+ * @access public
+ * @param array $filter 参数过滤
+ * @return $this
+ */
+ public function filter(array $filter)
+ {
+ return $this->setOption('filter', $filter);
+ }
+
+ /**
+ * 检查Header信息
+ * @access public
+ * @param array $header
+ * @return $this
+ */
+ public function header(array $header = [])
+ {
+ return $this->setOption('header', $header);
+ }
+
+ /**
+ * 检查版本控制
+ * @access public
+ * @param string $version
+ * @return $this
+ */
+ public function version(string $version)
+ {
+ $key = $this->config('api_version');
+ return $this->header([$key => $version]);
+ }
+
+ /**
+ * 设置路由变量默认值
+ * @access public
+ * @param array $default 可选路由变量默认值
+ * @return $this
+ */
+ public function default(array $default)
+ {
+ return $this->setOption('default', $default);
+ }
+
+ /**
+ * 设置路由自动注册中间件
+ * @access public
+ * @param bool $auto
+ * @return $this
+ */
+ public function autoMiddleware(bool $auto = true)
+ {
+ return $this->setOption('auto_middleware', $auto);
+ }
+
+ /**
+ * 绑定模型
+ * @access public
+ * @param array|string|Closure $var 路由变量名 多个使用 & 分割
+ * @param string|Closure|null $model 绑定模型类
+ * @param bool $exception 是否抛出异常
+ * @return $this
+ */
+ public function model(array | string | Closure $var, string | Closure | null $model = null, bool $exception = true)
+ {
+ if ($var instanceof Closure) {
+ $this->option['model'][] = $var;
+ } elseif (is_array($var)) {
+ $this->option['model'] = $var;
+ } elseif (is_null($model)) {
+ $this->option['model']['id'] = [$var, true];
+ } else {
+ $this->option['model'][$var] = [$model, $exception];
+ }
+
+ return $this;
+ }
+
+ /**
+ * 附加路由隐式参数
+ * @access public
+ * @param array $append 追加参数
+ * @return $this
+ */
+ public function append(array $append = [])
+ {
+ $this->option['append'] = array_merge($this->option['append'] ?? [], $append);
+
+ return $this;
+ }
+
+ /**
+ * 绑定验证
+ * @access public
+ * @param mixed $validate 验证器类
+ * @param string|array $scene 验证场景
+ * @param array $message 验证提示
+ * @param bool $batch 批量验证
+ * @return $this
+ */
+ public function validate($validate, string | array $scene = '', array $message = [], bool $batch = false)
+ {
+ return $this->setOption('validate', [$validate, $scene, $message, $batch]);
+ }
+
+ /**
+ * 指定路由中间件
+ * @access public
+ * @param string|array|Closure $middleware 中间件
+ * @param mixed $params 参数
+ * @return $this
+ */
+ public function middleware(string | array | Closure $middleware, ...$params)
+ {
+ if (empty($params) && is_array($middleware)) {
+ $this->option['middleware'] = array_merge($this->option['middleware'] ?? [], $middleware);
+ } else {
+ foreach ((array) $middleware as $item) {
+ $this->option['middleware'][] = [$item, $params];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置不使用的中间件 留空则为全部不用
+ * @access public
+ * @param array $middleware 中间件
+ * @return $this
+ */
+ public function withoutMiddleware(array $middleware = [])
+ {
+ return $this->setOption('without_middleware', $middleware);
+ }
+
+ /**
+ * 允许跨域
+ * @access public
+ * @param array $header 自定义Header
+ * @return $this
+ */
+ public function allowCrossDomain(array $header = [])
+ {
+ return $this->middleware(AllowCrossDomain::class, $header);
+ }
+
+ /**
+ * 表单令牌验证
+ * @access public
+ * @param string $token 表单令牌token名称
+ * @return $this
+ */
+ public function token(string $token = '__token__')
+ {
+ return $this->middleware(FormTokenCheck::class, $token);
+ }
+
+ /**
+ * 设置路由缓存
+ * @access public
+ * @param array|string|int $cache 缓存
+ * @return $this
+ */
+ public function cache(array | string | int $cache)
+ {
+ return $this->middleware(CheckRequestCache::class, $cache);
+ }
+
+ /**
+ * 检查URL分隔符
+ * @access public
+ * @param string $depr URL分隔符
+ * @return $this
+ */
+ public function depr(string $depr)
+ {
+ return $this->setOption('param_depr', $depr);
+ }
+
+ /**
+ * 设置需要合并的路由参数
+ * @access public
+ * @param array $option 路由参数
+ * @return $this
+ */
+ public function mergeOptions(array $option = [])
+ {
+ $this->mergeOptions = array_merge($this->mergeOptions, $option);
+ return $this;
+ }
+
+ /**
+ * 检查是否为HTTPS请求
+ * @access public
+ * @param bool $https 是否为HTTPS
+ * @return $this
+ */
+ public function https(bool $https = true)
+ {
+ return $this->setOption('https', $https);
+ }
+
+ /**
+ * 检查是否为JSON请求
+ * @access public
+ * @param bool $json 是否为JSON
+ * @return $this
+ */
+ public function json(bool $json = true)
+ {
+ return $this->setOption('json', $json);
+ }
+
+ /**
+ * 检查是否为AJAX请求
+ * @access public
+ * @param bool $ajax 是否为AJAX
+ * @return $this
+ */
+ public function ajax(bool $ajax = true)
+ {
+ return $this->setOption('ajax', $ajax);
+ }
+
+ /**
+ * 检查是否为PJAX请求
+ * @access public
+ * @param bool $pjax 是否为PJAX
+ * @return $this
+ */
+ public function pjax(bool $pjax = true)
+ {
+ return $this->setOption('pjax', $pjax);
+ }
+
+ /**
+ * 路由到一个模板地址 需要额外传入的模板变量
+ * @access public
+ * @param array $view 视图
+ * @return $this
+ */
+ public function view(array $view = [])
+ {
+ return $this->setOption('view', $view);
+ }
+
+ /**
+ * 通过闭包检查路由是否匹配
+ * @access public
+ * @param callable $match 闭包
+ * @return $this
+ */
+ public function match(callable $match)
+ {
+ return $this->setOption('match', $match);
+ }
+
+ /**
+ * 设置路由完整匹配
+ * @access public
+ * @param bool $match 是否完整匹配
+ * @return $this
+ */
+ public function completeMatch(bool $match = true)
+ {
+ return $this->setOption('complete_match', $match);
+ }
+
+ /**
+ * 是否去除URL最后的斜线
+ * @access public
+ * @param bool $remove 是否去除最后斜线
+ * @return $this
+ */
+ public function removeSlash(bool $remove = true)
+ {
+ return $this->setOption('remove_slash', $remove);
+ }
+
+ /**
+ * 设置路由规则全局有效
+ * @access public
+ * @return $this
+ */
+ public function crossDomainRule()
+ {
+ $this->router->setCrossDomainRule($this);
+ return $this;
+ }
+
+ /**
+ * 解析匹配到的规则路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @param string $url URL地址
+ * @param array $option 路由参数
+ * @param array $matches 匹配的变量
+ * @return Dispatch
+ */
+ public function parseRule(Request $request, string $rule, $route, string $url, array $option = [], array $matches = []): Dispatch
+ {
+ if (is_string($route) && isset($option['prefix'])) {
+ // 路由地址前缀
+ $route = $option['prefix'] . $route;
+ }
+
+ // 替换路由地址中的变量
+ $extraParams = true;
+ $search = $replace = [];
+ $depr = $this->config('pathinfo_depr');
+
+ foreach ($matches as $key => $value) {
+ $search[] = '<' . $key . '>';
+ $replace[] = $value;
+ $search[] = '{' . $key . '}';
+ $replace[] = $value;
+ $search[] = ':' . $key;
+ $replace[] = $value;
+
+ if (str_contains($value, $depr)) {
+ $extraParams = false;
+ }
+ }
+
+ if (is_string($route)) {
+ $route = str_replace($search, $replace, $route);
+ }
+
+ // 解析额外参数
+ if ($extraParams) {
+ $count = substr_count($rule, '/');
+ $url = array_slice(explode('|', $url), $count + 1);
+ $this->parseUrlParams(implode('/', $url), $matches);
+ }
+
+ foreach ($matches as $key => &$val) {
+ if (isset($this->pattern[$key]) && in_array($this->pattern[$key], ['\d+', 'int', 'float'])) {
+ $val = match ($this->pattern[$key]) {
+ 'int', '\d+' => (int) $val,
+ 'float' => (float) $val,
+ default => $val,
+ };
+ } elseif (in_array($key, ['__module__','__controller__','__action__'])) {
+ unset($matches[$key]);
+ }
+ }
+
+ $this->vars = $matches;
+
+ // 发起路由调度
+ return $this->dispatch($request, $route, $option);
+ }
+
+ /**
+ * 发起路由调度
+ * @access protected
+ * @param Request $request Request对象
+ * @param mixed $route 路由地址
+ * @param array $option 路由参数
+ * @return Dispatch
+ */
+ protected function dispatch(Request $request, $route, array $option): Dispatch
+ {
+ if (isset($option['dispatcher']) && is_subclass_of($option['dispatcher'], Dispatch::class)) {
+ // 指定分组的调度处理对象
+ $result = new $option['dispatcher']($request, $this, $route, $this->vars, $option);
+ } elseif (is_subclass_of($route, Dispatch::class)) {
+ $result = new $route($request, $this, $route, $this->vars, $option);
+ } elseif ($route instanceof Closure) {
+ // 执行闭包
+ $result = new CallbackDispatch($request, $this, $route, $this->vars, $option);
+ } elseif (is_array($route)) {
+ // 路由到类的方法
+ $result = $this->dispatchMethod($request, $route, $option);
+ } elseif (str_contains($route, '@') || str_contains($route, '::') || str_contains($route, '\\')) {
+ // 路由到类的方法
+ $route = str_replace('::', '@', $route);
+ $result = $this->dispatchMethod($request, $route, $option);
+ } else {
+ // 路由到模块/控制器/操作
+ $result = $this->dispatchController($request, $route, $option);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 调度到类的方法
+ * @access protected
+ * @param Request $request Request对象
+ * @param string|array $route 路由地址
+ * @return CallbackDispatch
+ */
+ protected function dispatchMethod(Request $request, string | array $route, array $option = []): CallbackDispatch
+ {
+ if (is_string($route)) {
+ $path = $this->parseUrlPath($route);
+
+ $route = str_replace('/', '@', implode('/', $path));
+ $method = str_contains($route, '@') ? explode('@', $route) : $route;
+ } else {
+ $method = $route;
+ }
+
+ return new CallbackDispatch($request, $this, $method, $this->vars, $option);
+ }
+
+ /**
+ * 调度到控制器方法 规则:模块/控制器/操作
+ * @access protected
+ * @param Request $request Request对象
+ * @param string $route 路由地址
+ * @return ControllerDispatch
+ */
+ protected function dispatchController(Request $request, string $route, array $option = []): ControllerDispatch
+ {
+ $path = $this->parseUrlPath($route);
+
+ // 路由到模块/控制器/操作
+ return new ControllerDispatch($request, $this, $path, $this->vars, $option);
+ }
+
+ /**
+ * 路由检查
+ * @access protected
+ * @param array $option 路由参数
+ * @param Request $request Request对象
+ * @return bool
+ */
+ protected function checkOption(array $option, Request $request): bool
+ {
+ // 检查当前路由是否匹配
+ if (isset($option['match']) && is_callable($option['match'])) {
+ if (false === $option['match']($this, $request)) {
+ return false;
+ }
+ }
+
+ // 请求类型检测
+ if (!empty($option['method'])) {
+ if (is_string($option['method']) && false === stripos($option['method'], $request->method())) {
+ return false;
+ }
+ }
+
+ // AJAX PJAX 请求检查
+ foreach (['ajax', 'pjax', 'json'] as $item) {
+ if (isset($option[$item])) {
+ $call = 'is' . $item;
+ if ($option[$item] && !$request->$call() || !$option[$item] && $request->$call()) {
+ return false;
+ }
+ }
+ }
+
+ // 伪静态后缀检测
+ if ($request->url() != '/' && ((isset($option['ext']) && false === stripos('|' . $option['ext'] . '|', '|' . $request->ext() . '|'))
+ || (isset($option['deny_ext']) && false !== stripos('|' . $option['deny_ext'] . '|', '|' . $request->ext() . '|')))) {
+ return false;
+ }
+
+ // 域名检查
+ if ((isset($option['domain']) && !in_array($option['domain'], [$request->host(true), $request->subDomain()]))) {
+ return false;
+ }
+
+ // HTTPS检查
+ if ((isset($option['https']) && $option['https'] && !$request->isSsl())
+ || (isset($option['https']) && !$option['https'] && $request->isSsl())
+ ) {
+ return false;
+ }
+
+ // 请求参数过滤
+ if (isset($option['filter'])) {
+ foreach ($option['filter'] as $name => $value) {
+ if ($request->param($name, '') != $value) {
+ return false;
+ }
+ }
+ }
+
+ // 检查Header信息
+ if (isset($option['header'])) {
+ foreach ($option['header'] as $name => $value) {
+ if ($request->header($name, '') != $value) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 解析URL地址中的参数Request对象
+ * @access protected
+ * @param string $rule 路由规则
+ * @param array $var 变量
+ * @return void
+ */
+ protected function parseUrlParams(string $url, array &$var = []): void
+ {
+ if ($url) {
+ preg_replace_callback('/(\w+)\/([^\/]+)/', function ($match) use (&$var) {
+ $var[$match[1]] = strip_tags($match[2]);
+ }, $url);
+ }
+ }
+
+ /**
+ * 解析URL的pathinfo参数
+ * @access public
+ * @param string $url URL地址
+ * @return array
+ */
+ public function parseUrlPath(string $url): array
+ {
+ // 分隔符替换 确保路由定义使用统一的分隔符
+ $url = str_replace('|', '/', $url);
+ $url = trim($url, '/');
+
+ if (str_contains($url, '/')) {
+ // [模块/.../控制器/操作]
+ $path = explode('/', $url);
+ } else {
+ $path = [$url];
+ }
+
+ return $path;
+ }
+
+ /**
+ * 生成路由的正则规则
+ * @access protected
+ * @param string $rule 路由规则
+ * @param array $match 匹配的变量
+ * @param array $pattern 路由变量规则
+ * @param array $option 路由参数
+ * @param bool $completeMatch 路由是否完全匹配
+ * @param string $suffix 路由正则变量后缀
+ * @return string
+ */
+ protected function buildRuleRegex(string $rule, array $match, array $pattern = [], array $option = [], bool $completeMatch = false, string $suffix = ''): string
+ {
+ foreach ($match as $name) {
+ $value = $this->buildNameRegex($name, $pattern, $suffix);
+ if ($value) {
+ $origin[] = $name;
+ $replace[] = $value;
+ }
+ }
+
+ // 是否区分 / 地址访问
+ if ('/' != $rule) {
+ if (!empty($option['remove_slash'])) {
+ $rule = rtrim($rule, '/');
+ } elseif (str_ends_with($rule, '/')) {
+ $rule = rtrim($rule, '/');
+ $hasSlash = true;
+ }
+ }
+
+ $regex = isset($replace) ? str_replace($origin, $replace, $rule) : $rule;
+ $regex = str_replace([')?/', ')?-'], [')/', ')-'], $regex);
+
+ if (isset($hasSlash)) {
+ $regex .= '/';
+ }
+
+ return $regex . ($completeMatch ? '$' : '');
+ }
+
+ /**
+ * 生成路由变量的正则规则
+ * @access protected
+ * @param string $name 路由变量
+ * @param array $pattern 变量规则
+ * @param string $suffix 路由正则变量后缀
+ * @return string
+ */
+ protected function buildNameRegex(string $name, array $pattern, string $suffix): string
+ {
+ $optional = '';
+ $slash = substr($name, 0, 1);
+
+ if (in_array($slash, ['/', '-'])) {
+ $prefix = $slash;
+ $name = substr($name, 1);
+ $slash = substr($name, 0, 1);
+ } else {
+ $prefix = '';
+ }
+
+ if ('<' != $slash) {
+ return '';
+ }
+
+ if (str_contains($name, '?')) {
+ $name = substr($name, 1, -2);
+ $optional = '?';
+ } elseif (str_contains($name, '>')) {
+ $name = substr($name, 1, -1);
+ }
+
+ if (isset($pattern[$name])) {
+ $nameRule = $pattern[$name];
+ if (isset($this->regex[$nameRule])) {
+ $nameRule = $this->regex[$nameRule];
+ }
+
+ if (str_starts_with($nameRule, '/') && str_ends_with($nameRule, '/')) {
+ $nameRule = substr($nameRule, 1, -1);
+ }
+ } else {
+ $nameRule = $this->config('default_route_pattern');
+ }
+
+ return '(' . $prefix . '(?<' . $name . $suffix . '>' . $nameRule . '))' . $optional;
+ }
+
+ /**
+ * 设置路由参数
+ * @access public
+ * @param string $method 方法名
+ * @param array $args 调用参数
+ * @return $this
+ */
+ public function __call($method, $args)
+ {
+ if (count($args) > 1) {
+ $args[0] = $args;
+ }
+ array_unshift($args, $method);
+
+ return call_user_func_array([$this, 'setOption'], $args);
+ }
+
+ public function __sleep()
+ {
+ return ['name', 'rule', 'route', 'method', 'vars', 'option', 'pattern'];
+ }
+
+ public function __wakeup()
+ {
+ $this->router = Container::pull('route');
+ }
+
+ public function __debugInfo()
+ {
+ return [
+ 'name' => $this->name,
+ 'rule' => $this->rule,
+ 'route' => $this->route,
+ 'method' => $this->method,
+ 'vars' => $this->vars,
+ 'option' => $this->option,
+ 'pattern' => $this->pattern,
+ ];
+ }
+}
diff --git a/src/think/route/RuleGroup.php b/src/think/route/RuleGroup.php
new file mode 100644
index 0000000..d33b832
--- /dev/null
+++ b/src/think/route/RuleGroup.php
@@ -0,0 +1,782 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use Closure;
+use DirectoryIterator;
+use think\Container;
+use think\Exception;
+use think\helper\Str;
+use think\Request;
+use think\Route;
+use think\route\dispatch\Callback as CallbackDispatch;
+use think\route\dispatch\Controller as ControllerDispatch;
+
+/**
+ * 路由分组类
+ */
+class RuleGroup extends Rule
+{
+ /**
+ * 分组路由(包括子分组)
+ * @var Rule[]
+ */
+ protected $rules = [];
+
+ /**
+ * MISS路由
+ * @var RuleItem
+ */
+ protected $miss;
+
+ /**
+ * 完整名称
+ * @var string
+ */
+ protected $fullName;
+
+ /**
+ * 分组别名
+ * @var string
+ */
+ protected $alias;
+
+ /**
+ * 分组子目录
+ * @var string
+ */
+ protected $sub;
+
+ /**
+ * 分组绑定
+ * @var string
+ */
+ protected $bind;
+
+ /**
+ * 是否已经解析
+ * @var bool
+ */
+ protected $hasParsed;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由对象
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 分组名称
+ * @param mixed $rule 分组路由
+ * @param bool $lazy 延迟解析
+ */
+ public function __construct(Route $router, ?RuleGroup $parent = null, string $name = '', $rule = null, bool $lazy = false)
+ {
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->rule = $rule;
+ $this->name = trim($name, '/');
+
+ if ($name && is_string($rule) || is_null($rule)) {
+ if ($rule && is_subclass_of($rule, Dispatch::class, false)) {
+ $this->dispatcher($rule);
+ $this->rule = '';
+ } else {
+ $this->sub = $rule ?: $this->name;
+ }
+ }
+
+ $this->setFullName();
+
+ if ($this->parent) {
+ $this->domain = $this->parent->getDomain();
+ $this->parent->addRuleItem($this);
+ }
+
+ if (!$lazy) {
+ $this->parseGroupRule($rule);
+ }
+ }
+
+ /**
+ * 设置分组的路由规则
+ * @access public
+ * @return void
+ */
+ protected function setFullName(): void
+ {
+ if (str_contains($this->name, ':')) {
+ $this->name = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $this->name);
+ }
+
+ if ($this->parent && $this->parent->getFullName()) {
+ $this->fullName = $this->parent->getFullName() . ($this->name ? '/' . $this->name : '');
+ if ($this->sub) {
+ $this->sub = $this->parent->getFullName() . '/' . $this->sub;
+ }
+ } else {
+ $this->fullName = $this->name;
+ }
+
+ if ($this->name) {
+ $this->router->getRuleName()->setGroup($this->name, $this);
+ }
+ }
+
+ /**
+ * 获取所属域名
+ * @access public
+ * @return string
+ */
+ public function getDomain(): string
+ {
+ return $this->domain ?: '-';
+ }
+
+ /**
+ * 获取分组别名
+ * @access public
+ * @return string
+ */
+ public function getAlias(): string
+ {
+ return $this->alias ?: '';
+ }
+
+ /**
+ * 自动加载分组路由
+ * @access protected
+ * @param string $dir 目录名
+ * @return void
+ */
+ protected function loadRoutes(string $dir): void
+ {
+ $routePath = root_path('route' . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR);
+ if (is_dir($routePath)) {
+ // 动态加载分组路由
+ $files = glob($routePath . '*.php');
+ foreach ($files as $file) {
+ include_once $file;
+ }
+
+ // 自动扫描下级分组
+ $dirs = $this->config('route_auto_group') ? glob($routePath . '*', GLOB_ONLYDIR) : [];
+ foreach ($dirs as $dir) {
+ $groupName = str_replace('\\', '/', substr_replace($dir, '', 0, strlen($routePath)));
+ if (!$this->router->getRuleName()->hasGroup($groupName)) {
+ $this->router->group($groupName);
+ }
+ }
+ }
+ }
+
+ /**
+ * 检测分组路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function check(Request $request, string $url, bool $completeMatch = false)
+ {
+ // 检查分组有效性
+ if (!$this->checkOption($this->option, $request) || !$this->checkUrl($url)) {
+ return false;
+ }
+
+ // 解析分组路由
+ if (!$this->hasParsed) {
+ $this->parseGroupRule($this->rule);
+ }
+
+ // 获取当前路由规则
+ $method = strtolower($request->method());
+ $rules = $this->getRules($method);
+ $option = $this->getOption();
+
+ if (isset($option['complete_match'])) {
+ $completeMatch = $option['complete_match'];
+ }
+
+ if (!empty($option['merge_rule_regex'])) {
+ // 路由合并检查
+ $result = $this->checkMergeRuleRegex($request, $rules, $url, $completeMatch);
+
+ if (false !== $result) {
+ return $result;
+ }
+ } else {
+ // 检查分组路由
+ foreach ($rules as $item) {
+ $result = $item->check($request, $url, $completeMatch);
+
+ if (false !== $result) {
+ return $result;
+ }
+ }
+ }
+
+ $miss = $this->getMissRule($method);
+ if ($this->bind) {
+ // 检查分组绑定
+ return $this->checkBind($request, $url, $option, $miss);
+ }
+
+ if ($miss) {
+ // MISS路由
+ return $miss->parseRule($request, '', $miss->getRoute(), $url, $miss->getOption());
+ }
+
+ return false;
+ }
+
+ /**
+ * 分组URL匹配检查
+ * @access protected
+ * @param string $url URL
+ * @return bool
+ */
+ protected function checkUrl(string $url): bool
+ {
+ $url = str_replace('|', '/', $url);
+ if (!$this->config('url_route_must')) {
+ $item = $this->router->getRuleName()->getName($url);
+ if (!empty($item) && $item[0]['rule'] != $url){
+ // 定义过路由地址的 不支持访问
+ return false;
+ }
+ }
+
+ if ($this->fullName) {
+ $pos = strpos($this->fullName, '<');
+
+ if (false !== $pos) {
+ $str = substr($this->fullName, 0, $pos);
+ } else {
+ $str = $this->fullName;
+ }
+
+ if ($str && 0 !== stripos($url, $str)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 设置路由分组别名
+ * @access public
+ * @param string $alias 路由分组别名
+ * @return $this
+ */
+ public function alias(string $alias)
+ {
+ $this->alias = $alias;
+ $this->router->getRuleName()->setGroup($alias, $this);
+
+ return $this;
+ }
+
+ /**
+ * 解析分组和域名的路由规则及绑定
+ * @access public
+ * @param mixed $rule 路由规则
+ * @return void
+ */
+ public function parseGroupRule($rule): void
+ {
+ $origin = $this->router->getGroup();
+ $this->router->setGroup($this);
+
+ if ($rule instanceof Closure) {
+ Container::getInstance()->invokeFunction($rule);
+ } elseif ($this->sub) {
+ $this->loadRoutes($this->sub);
+ }
+
+ $this->router->setGroup($origin);
+ $this->hasParsed = true;
+ }
+
+ /**
+ * 检测分组路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param array $rules 路由规则
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ protected function checkMergeRuleRegex(Request $request, array &$rules, string $url, bool $completeMatch)
+ {
+ $depr = $this->config('pathinfo_depr');
+ $url = $depr . str_replace('|', $depr, $url);
+ $regex = [];
+ $items = [];
+
+ foreach ($rules as $key => $item) {
+ if ($item instanceof RuleItem) {
+ $rule = $depr . str_replace('/', $depr, $item->getRule());
+ if ($depr == $rule && $depr != $url) {
+ unset($rules[$key]);
+ continue;
+ }
+
+ $complete = $item->getOption('complete_match', $completeMatch);
+
+ if (!str_contains($rule, '<')) {
+ if (0 === strcasecmp($rule, $url) || (!$complete && 0 === strncasecmp($rule, $url, strlen($rule)))) {
+ return $item->checkRule($request, $url, []);
+ }
+
+ unset($rules[$key]);
+ continue;
+ }
+
+ $slash = preg_quote('/-' . $depr, '/');
+
+ if ($matchRule = preg_split('/[' . $slash . ']<\w+\??>/', $rule, 2)) {
+ if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) {
+ unset($rules[$key]);
+ continue;
+ }
+ }
+
+ if (preg_match_all('/[' . $slash . ']?\w+\??>?/', $rule, $matches)) {
+ unset($rules[$key]);
+ $pattern = array_merge($this->getPattern(), $item->getPattern());
+ $option = array_merge($this->getOption(), $item->getOption());
+
+ $regex[$key] = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $complete, '_THINK_' . $key);
+ $items[$key] = $item;
+ }
+ } elseif ($item instanceof RuleGroup) {
+ $array = $item->getrules();
+ return $this->checkMergeRuleRegex($request, $array, ltrim($url, $depr), $completeMatch);
+ }
+ }
+
+ if (empty($regex)) {
+ return false;
+ }
+
+ try {
+ $result = preg_match('~^(?:' . implode('|', $regex) . ')~u', $url, $match);
+ } catch (\Exception $e) {
+ throw new Exception('route pattern error');
+ }
+
+ if ($result) {
+ $var = [];
+ foreach ($match as $key => $val) {
+ if (is_string($key) && '' !== $val) {
+ [$name, $pos] = explode('_THINK_', $key);
+
+ $var[$name] = $val;
+ }
+ }
+
+ if (!isset($pos)) {
+ foreach ($regex as $key => $item) {
+ if (str_starts_with(str_replace(['\/', '\-', '\\' . $depr], ['/', '-', $depr], $item), $match[0])) {
+ $pos = $key;
+ break;
+ }
+ }
+ }
+
+ $rule = $items[$pos]->getRule();
+ $array = $this->router->getRule($rule);
+
+ foreach ($array as $item) {
+ if (in_array($item->getMethod(), ['*', strtolower($request->method())])) {
+ $result = $item->checkRule($request, $url, $var);
+
+ if (false !== $result) {
+ return $result;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 注册MISS路由
+ * @access public
+ * @param string|Closure $route 路由地址
+ * @param string $method 请求类型
+ * @return RuleItem
+ */
+ public function miss(string | Closure $route, string $method = '*'): RuleItem
+ {
+ // 创建路由规则实例
+ $method = strtolower($method);
+ $ruleItem = new RuleItem($this->router, $this, null, '', $route, $method);
+
+ $this->miss[$method] = $ruleItem->setMiss();
+
+ return $ruleItem;
+ }
+
+ /**
+ * 获取分组下的MISS路由
+ * @access public
+ * @param string $method 请求类型
+ * @return RuleItem|null
+ */
+ public function getMissRule(string $method = '*'): ?RuleItem
+ {
+ if (isset($this->miss[$method])) {
+ $miss = $this->miss[$method];
+ } elseif (isset($this->miss['*'])) {
+ $miss = $this->miss['*'];
+ }
+ return $miss ?? null;
+ }
+
+ /**
+ * 分组自动URL调度 默认绑定到当前分组名所在的控制器分级
+ * @access public
+ * @param string $bind 绑定资源 绑定规则 class @controller :namespace /layer
+ * @param string|array $middleware 中间件
+ * @return $this
+ */
+ public function auto(string $bind = '', string | array $middleware = '')
+ {
+ $this->bind = $bind ?: '/' . $this->getFullName();
+ if ($middleware) {
+ $this->middleware($middleware);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 分组绑定到类
+ * @access public
+ * @param string $class
+ * @param bool $prefix
+ * @return $this
+ */
+ public function class(string $class, bool $prefix = true)
+ {
+ $this->bind = '\\' . $class;
+ if ($prefix) {
+ $this->prefix('\\' . $class . '@');
+ }
+ return $this;
+ }
+
+ /**
+ * 分组绑定到控制器
+ * @access public
+ * @param string $controller
+ * @param bool $prefix
+ * @return $this
+ */
+ public function controller(string $controller, bool $prefix = true)
+ {
+ $this->bind = '@' . $controller;
+ if ($prefix) {
+ $this->prefix($controller . '/');
+ }
+ return $this;
+ }
+
+ /**
+ * 分组绑定到命名空间
+ * @access public
+ * @param string $namespace
+ * @param bool $prefix
+ * @return $this
+ */
+ public function namespace(string $namespace, bool $prefix = true)
+ {
+ $this->bind = ':' . $namespace;
+ if ($prefix) {
+ $this->prefix($namespace . '\\');
+ }
+ return $this;
+ }
+
+ /**
+ * 分组绑定到控制器分级
+ * @access public
+ * @param string $namespace
+ * @param bool $prefix
+ * @return $this
+ */
+ public function layer(string $layer, bool $prefix = true)
+ {
+ $this->bind = '/' . $layer;
+ if ($prefix) {
+ $this->prefix($layer . '/');
+ }
+ return $this;
+ }
+
+ /**
+ * 获取分组绑定信息
+ * @access public
+ * @return string
+ */
+ public function getBind()
+ {
+ return $this->bind ?? '';
+ }
+
+ /**
+ * 检测URL绑定
+ * @access private
+ * @param Request $request
+ * @param string $url URL地址
+ * @param array $option 分组参数
+ * @param RuleItem $miss
+ * @return Dispatch
+ */
+ public function checkBind(Request $request, string $url, array $option = [], ?RuleItem $miss = null): Dispatch
+ {
+ [$bind, $param] = $this->parseBindAppendParam($this->bind);
+
+ [$call, $bind] = match (substr($bind, 0, 1)) {
+ '\\' => ['bindToClass', substr($bind, 1)],
+ '@' => ['bindToController', substr($bind, 1)],
+ '/' => ['bindToLayer', substr($bind, 1)],
+ ':' => ['bindToNamespace', substr($bind, 1)],
+ default => ['bindToClass', $bind],
+ };
+
+ $name = $this->getFullName();
+ $url = trim(substr(str_replace('|', '/', $url), strlen($name)), '/');
+
+ return $this->$call($request, $url, $bind, $param, $option, $miss);
+ }
+
+ protected function parseBindAppendParam(string $bind)
+ {
+ $vars = [];
+ if (str_contains($bind, '?')) {
+ [$bind, $query] = explode('?', $bind);
+ parse_str($query, $vars);
+ }
+ return [$bind, $vars];
+ }
+
+ /**
+ * 绑定到类
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $class 类名(带命名空间)
+ * @param array $param 路由变量
+ * @param array $option 分组参数
+ * @param RuleItem $miss
+ * @return CallbackDispatch
+ */
+ protected function bindToClass(Request $request, string $url, string $class, array $param = [], array $option = [], ?RuleItem $miss = null): CallbackDispatch
+ {
+ $array = explode('/', $url, 2);
+ $action = !empty($array[0]) ? $array[0] : $this->config('default_action');
+
+ if (!empty($array[1])) {
+ $this->parseUrlParams($array[1], $param);
+ }
+
+ return new CallbackDispatch($request, $this, [$class, $action], $param, $option, $miss);
+ }
+
+ /**
+ * 绑定到命名空间
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $namespace 命名空间
+ * @param array $param 路由变量
+ * @param array $option 分组参数
+ * @param RuleItem $miss
+ * @return CallbackDispatch
+ */
+ protected function bindToNamespace(Request $request, string $url, string $namespace, array $param = [], array $option = [], ?RuleItem $miss = null): CallbackDispatch
+ {
+ $array = explode('/', $url, 3);
+ $class = !empty($array[0]) ? $array[0] : $this->config('default_controller');
+ $method = !empty($array[1]) ? $array[1] : $this->config('default_action');
+
+ if (!empty($array[2])) {
+ $this->parseUrlParams($array[2], $param);
+ }
+
+ return new CallbackDispatch($request, $this, [trim($namespace, '\\') . '\\' . Str::studly($class), $method], $param, $option, $miss);
+ }
+
+ /**
+ * 绑定到控制器
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $controller 控制器名
+ * @param array $param 路由变量
+ * @param array $option 分组参数
+ * @param RuleItem $miss
+ * @return ControllerDispatch
+ */
+ protected function bindToController(Request $request, string $url, string $controller, array $param = [], array $option = [], ?RuleItem $miss = null): ControllerDispatch
+ {
+ $array = explode('/', $url, 2);
+ $action = !empty($array[0]) ? $array[0] : $this->config('default_action');
+
+ if (!empty($array[1])) {
+ $this->parseUrlParams($array[1], $param);
+ }
+
+ return new ControllerDispatch($request, $this, [$controller, $action], $param, $option, $miss);
+ }
+
+ /**
+ * 绑定到控制器分级
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $controller 控制器名
+ * @param array $param 路由变量
+ * @param array $option 分组参数
+ * @param RuleItem $miss
+ * @return ControllerDispatch
+ */
+ protected function bindToLayer(Request $request, string $url, string $layer, array $param = [], array $option = [], ?RuleItem $miss = null): ControllerDispatch
+ {
+ $array = explode('/', $url, 3);
+ $controller = !empty($array[0]) ? $array[0] : $this->config('default_controller');
+ $action = !empty($array[1]) ? $array[1] : $this->config('default_action');
+
+ if (!empty($array[2])) {
+ $this->parseUrlParams($array[2], $param);
+ }
+
+ return new ControllerDispatch($request, $this, [$layer, $controller, $action], $param, $option, $miss);
+ }
+
+ /**
+ * 添加分组下的路由规则
+ * @access public
+ * @param string $rule 路由规则
+ * @param mixed $route 路由地址
+ * @param string $method 请求类型
+ * @return RuleItem
+ */
+ public function addRule(string $rule, $route = null, string $method = '*'): RuleItem
+ {
+ // 读取路由标识
+ $name = is_string($route) ? $route : null;
+ $method = strtolower($method);
+ if ('' === $rule || '/' === $rule) {
+ $rule .= '$';
+ }
+
+ // 创建路由规则实例
+ $ruleItem = new RuleItem($this->router, $this, $name, $rule, $route, $method);
+
+ $this->addRuleItem($ruleItem);
+
+ return $ruleItem;
+ }
+
+ /**
+ * 注册分组下的路由规则
+ * @access public
+ * @param Rule $rule 路由规则
+ * @return $this
+ */
+ public function addRuleItem(Rule $rule)
+ {
+ $this->rules[] = $rule;
+ return $this;
+ }
+
+ /**
+ * 设置分组的路由前缀
+ * @access public
+ * @param string $prefix 路由前缀
+ * @return $this
+ */
+ public function prefix(string $prefix)
+ {
+ if ($this->parent && $this->parent->getOption('prefix')) {
+ $prefix = $this->parent->getOption('prefix') . $prefix;
+ }
+
+ return $this->setOption('prefix', $prefix);
+ }
+
+ /**
+ * 合并分组的路由规则正则
+ * @access public
+ * @param bool $merge 是否合并
+ * @return $this
+ */
+ public function mergeRuleRegex(bool $merge = true)
+ {
+ return $this->setOption('merge_rule_regex', $merge);
+ }
+
+ /**
+ * 设置分组的Dispatch调度
+ * @access public
+ * @param string $dispatch 调度类
+ * @return $this
+ */
+ public function dispatcher(string $dispatch)
+ {
+ return $this->setOption('dispatcher', $dispatch);
+ }
+
+ /**
+ * 获取完整分组Name
+ * @access public
+ * @return string
+ */
+ public function getFullName(): string
+ {
+ return $this->fullName ?: '';
+ }
+
+ /**
+ * 获取分组的路由规则
+ * @access public
+ * @param string $method 请求类型
+ * @return array
+ */
+ public function getRules(string $method = ''): array
+ {
+ if ('' === $method) {
+ return $this->rules;
+ }
+
+ return array_filter($this->rules, function ($item) use ($method) {
+ $ruleMethod = $item->getMethod();
+ return '*' == $ruleMethod || str_contains($ruleMethod, $method);
+ });
+ }
+
+ /**
+ * 清空分组下的路由规则
+ * @access public
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->rules = [];
+ }
+}
diff --git a/src/think/route/RuleItem.php b/src/think/route/RuleItem.php
new file mode 100644
index 0000000..c53c77c
--- /dev/null
+++ b/src/think/route/RuleItem.php
@@ -0,0 +1,328 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route;
+
+use think\Exception;
+use think\facade\Validate;
+use think\Request;
+use think\Route;
+
+/**
+ * 路由规则类
+ */
+class RuleItem extends Rule
+{
+ /**
+ * 是否为MISS规则
+ * @var bool
+ */
+ protected $miss = false;
+
+ /**
+ * 是否为额外自动注册的OPTIONS规则
+ * @var bool
+ */
+ protected $autoOption = false;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由实例
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 路由标识
+ * @param string $rule 路由规则
+ * @param string|\Closure $route 路由地址
+ * @param string $method 请求类型
+ */
+ public function __construct(Route $router, RuleGroup $parent, ?string $name = null, string $rule = '', $route = null, string $method = '*')
+ {
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->name = $name;
+ $this->route = $route;
+ $this->method = $method;
+
+ $this->setRule($rule);
+ $this->router->setRule($this->rule, $this);
+ }
+
+ /**
+ * 设置当前路由规则为MISS路由
+ * @access public
+ * @return $this
+ */
+ public function setMiss()
+ {
+ $this->miss = true;
+ return $this;
+ }
+
+ /**
+ * 判断当前路由规则是否为MISS路由
+ * @access public
+ * @return bool
+ */
+ public function isMiss(): bool
+ {
+ return $this->miss;
+ }
+
+ /**
+ * 获取当前路由的URL后缀
+ * @access public
+ * @return string|null
+ */
+ public function getSuffix(): ?string
+ {
+ if (isset($this->option['ext'])) {
+ $suffix = $this->option['ext'];
+ } elseif ($this->parent->getOption('ext')) {
+ $suffix = $this->parent->getOption('ext');
+ }
+
+ return $suffix ?? null;
+ }
+
+ /**
+ * 路由规则预处理
+ * @access public
+ * @param string $rule 路由规则
+ * @return void
+ */
+ public function setRule(string $rule): void
+ {
+ if (str_ends_with($rule, '$')) {
+ // 是否完整匹配
+ $rule = substr($rule, 0, -1);
+
+ $this->option['complete_match'] = true;
+ }
+
+ $rule = '/' != $rule ? ltrim($rule, '/') : '';
+
+ if ($this->parent && $prefix = $this->parent->getFullName()) {
+ $rule = $prefix . ($rule ? '/' . ltrim($rule, '/') : '');
+ }
+
+ if (str_contains($rule, ':') || str_contains($rule, '{')) {
+ $this->rule = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/', '/\{(\w+)\}/', '/\{(\w+)\?\}/'], ['<\1?>', '<\1>', '<\1>', '<\1?>'], $rule);
+ } else {
+ $this->rule = $rule;
+ }
+
+ // 生成路由标识的快捷访问
+ $this->setRuleName();
+ }
+
+ /**
+ * 设置别名
+ * @access public
+ * @param string $name
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->name = $name;
+ $this->setRuleName(true);
+
+ return $this;
+ }
+
+ /**
+ * 设置路由标识 用于URL反解生成
+ * @access protected
+ * @param bool $first 是否插入开头
+ * @return void
+ */
+ protected function setRuleName(bool $first = false): void
+ {
+ if ($this->name) {
+ $this->router->setName($this->name, $this, $first);
+ }
+ }
+
+ /**
+ * 检测路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param array $match 匹配路由变量
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function checkRule(Request $request, string $url, ?array $match = null, bool $completeMatch = false)
+ {
+ // 检查参数有效性
+ if (!$this->checkOption($this->option, $request)) {
+ return false;
+ }
+
+ // 合并分组参数
+ $option = $this->getOption();
+ $pattern = $this->getPattern();
+ $url = $this->urlSuffixCheck($request, $url, $option);
+
+ if (is_null($match)) {
+ $match = $this->checkMatch($url, $option, $pattern, $completeMatch);
+ }
+
+ if (false !== $match) {
+ return $this->parseRule($request, $this->rule, $this->route, $url, $option, $match);
+ }
+
+ return false;
+ }
+
+ /**
+ * 检测路由(含路由匹配)
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function check(Request $request, string $url, bool $completeMatch = false)
+ {
+ return $this->checkRule($request, $url, null, $completeMatch);
+ }
+
+ /**
+ * URL后缀及Slash检查
+ * @access protected
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param array $option 路由参数
+ * @return string
+ */
+ protected function urlSuffixCheck(Request $request, string $url, array $option = []): string
+ {
+ // 是否区分 / 地址访问
+ if (!empty($option['remove_slash']) && '/' != $this->rule) {
+ $this->rule = rtrim($this->rule, '/');
+ $url = rtrim($url, '|');
+ }
+
+ if (isset($option['ext'])) {
+ // 路由ext参数 优先于系统配置的URL伪静态后缀参数
+ $url = preg_replace('/\.(' . $request->ext() . ')$/i', '', $url);
+ }
+
+ return $url;
+ }
+
+ /**
+ * 检测URL和规则路由是否匹配
+ * @access private
+ * @param string $url URL地址
+ * @param array $option 路由参数
+ * @param array $pattern 变量规则
+ * @param bool $completeMatch 是否完全匹配
+ * @return array|false
+ */
+ private function checkMatch(string $url, array $option, array $pattern, bool $completeMatch)
+ {
+ if (isset($option['complete_match'])) {
+ $completeMatch = $option['complete_match'];
+ }
+
+ $depr = $this->config('pathinfo_depr');
+ if (isset($option['case_sensitive'])) {
+ $case = $option['case_sensitive'];
+ } else {
+ $case = $this->config('url_case_sensitive');
+ }
+
+ // 检查完整规则定义
+ if (isset($pattern['__url__']) && !preg_match(str_starts_with($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . ($completeMatch ? '$' : '') . '/', str_replace('|', $depr, $url))) {
+ return false;
+ }
+
+ $var = [];
+ $url = $depr . str_replace('|', $depr, $url);
+ $rule = $depr . str_replace('/', $depr, $this->rule);
+
+ if ($depr == $rule && $depr != $url) {
+ return false;
+ }
+
+ if (!str_contains($rule, '<')) {
+ // 静态路由
+ if ($case && (0 === strcmp($rule, $url) || (!$completeMatch && 0 === strncmp($rule . $depr, $url . $depr, strlen($rule . $depr))))) {
+ return $var;
+ } elseif (!$case && (0 === strcasecmp($rule, $url) || (!$completeMatch && 0 === strncasecmp($rule . $depr, $url . $depr, strlen($rule . $depr))))) {
+ return $var;
+ }
+ return false;
+ }
+
+ $slash = preg_quote('/-' . $depr, '/');
+
+ if ($matchRule = preg_split('/[' . $slash . ']?<\w+\??>/', $rule, 2)) {
+ if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) {
+ return false;
+ }
+ }
+
+ if (preg_match_all('/[' . $slash . ']?\w+\??>?/', $rule, $matches)) {
+ $regex = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $completeMatch);
+
+ try {
+ if (!preg_match('~^' . $regex . '~u' . ($case ? '' : 'i'), $url, $match)) {
+ return false;
+ }
+ } catch (\Exception $e) {
+ throw new Exception('route pattern error');
+ }
+
+ foreach ($match as $key => $val) {
+ if (is_string($key)) {
+ if (isset($option['var_rule'][$key]) && !Validate::checkRule($val, $option['var_rule'][$key])) {
+ // 检查变量
+ return false;
+ }
+ $var[$key] = $val;
+ }
+ }
+ }
+
+ if (!empty($option['default'])) {
+ // 可选路由变量设置默认值
+ foreach ($option['default'] as $name => $default) {
+ if (!isset($var[$name])) {
+ $var[$name] = $default;
+ }
+ }
+ }
+
+ // 成功匹配后返回URL中的动态变量数组
+ return $var;
+ }
+
+ /**
+ * 设置路由所属分组(用于注解路由)
+ * @access public
+ * @param string $name 分组名称或者标识
+ * @return $this
+ */
+ public function group(string $name)
+ {
+ $group = $this->router->getRuleName()->getGroup($name);
+
+ if ($group) {
+ $this->parent = $group;
+ $this->setRule($this->rule);
+ }
+
+ return $this;
+ }
+}
diff --git a/src/think/route/RuleName.php b/src/think/route/RuleName.php
new file mode 100644
index 0000000..7cddc6b
--- /dev/null
+++ b/src/think/route/RuleName.php
@@ -0,0 +1,226 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\route;
+
+/**
+ * 路由标识管理类
+ */
+class RuleName
+{
+ /**
+ * 路由标识
+ * @var array
+ */
+ protected $item = [];
+
+ /**
+ * 路由规则
+ * @var array
+ */
+ protected $rule = [];
+
+ /**
+ * 路由分组
+ * @var array
+ */
+ protected $group = [];
+
+ /**
+ * 注册路由标识
+ * @access public
+ * @param string $name 路由标识
+ * @param RuleItem $ruleItem 路由规则
+ * @param bool $first 是否优先
+ * @return void
+ */
+ public function setName(string $name, RuleItem $ruleItem, bool $first = false): void
+ {
+ $name = strtolower($name);
+ $item = $this->getRuleItemInfo($ruleItem);
+ if ($first && isset($this->item[$name])) {
+ array_unshift($this->item[$name], $item);
+ } else {
+ $this->item[$name][] = $item;
+ }
+ }
+
+ /**
+ * 注册路由分组标识
+ * @access public
+ * @param string $name 路由分组标识
+ * @param RuleGroup $group 路由分组
+ * @return void
+ */
+ public function setGroup(string $name, RuleGroup $group): void
+ {
+ $this->group[strtolower($name)] = $group;
+ }
+
+ /**
+ * 注册路由规则
+ * @access public
+ * @param string $rule 路由规则
+ * @param RuleItem $ruleItem 路由
+ * @return void
+ */
+ public function setRule(string $rule, RuleItem $ruleItem): void
+ {
+ $route = $ruleItem->getRoute();
+
+ if (is_string($route)) {
+ $this->rule[$rule][$route] = $ruleItem;
+ } else {
+ $this->rule[$rule][] = $ruleItem;
+ }
+ }
+
+ /**
+ * 根据路由规则获取路由对象(列表)
+ * @access public
+ * @param string $rule 路由标识
+ * @return RuleItem[]
+ */
+ public function getRule(string $rule): array
+ {
+ return $this->rule[$rule] ?? [];
+ }
+
+ /**
+ * 根据路由分组标识获取分组
+ * @access public
+ * @param string $name 路由分组标识
+ * @return RuleGroup|null
+ */
+ public function getGroup(string $name): ?RuleGroup
+ {
+ return $this->group[strtolower($name)] ?? null;
+ }
+
+ /**
+ * 是否已经存在分组
+ * @access public
+ * @param string $name 路由分组标识
+ * @return bool
+ */
+ public function hasGroup(string $name): bool
+ {
+ return isset($this->group[strtolower($name)]);
+ }
+
+ /**
+ * 清空路由规则
+ * @access public
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->item = [];
+ $this->rule = [];
+ $this->group = [];
+ }
+
+ /**
+ * 获取全部路由列表
+ * @access public
+ * @return array
+ */
+ public function getRuleList(): array
+ {
+ $list = [];
+
+ foreach ($this->rule as $rule => $rules) {
+ foreach ($rules as $item) {
+ $val = [];
+ foreach (['method', 'rule', 'name', 'route', 'domain', 'pattern', 'option'] as $param) {
+ $call = 'get' . $param;
+ if ('rule' == $param) {
+ $val[$param] = $item->$call() ?: '/';
+ } else {
+ $val[$param] = $item->$call();
+ }
+ }
+
+ if ($item->isMiss()) {
+ $val['rule'] .= '';
+ }
+
+ $list[] = $val;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * 导入路由标识
+ * @access public
+ * @param array $item 路由标识
+ * @return void
+ */
+ public function import(array $item): void
+ {
+ $this->item = $item;
+ }
+
+ /**
+ * 根据路由标识获取路由信息(用于URL生成)
+ * @access public
+ * @param string $name 路由标识
+ * @param string $domain 域名
+ * @param string $method 请求类型
+ * @return array
+ */
+ public function getName(?string $name = null, ?string $domain = null, string $method = '*'): array
+ {
+ if (is_null($name)) {
+ return $this->item;
+ }
+
+ $name = strtolower($name);
+ $method = strtolower($method);
+ $result = [];
+
+ if (isset($this->item[$name])) {
+ if (is_null($domain)) {
+ $result = $this->item[$name];
+ } else {
+ foreach ($this->item[$name] as $item) {
+ $itemDomain = $item['domain'];
+ $itemMethod = $item['method'];
+
+ if (($itemDomain == $domain || '-' == $itemDomain) && ('*' == $itemMethod || '*' == $method || $method == $itemMethod)) {
+ $result[] = $item;
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取路由信息
+ * @access protected
+ * @param RuleItem $item 路由规则
+ * @return array
+ */
+ protected function getRuleItemInfo(RuleItem $item): array
+ {
+ return [
+ 'rule' => $item->getRule(),
+ 'domain' => $item->getDomain(),
+ 'method' => $item->getMethod(),
+ 'suffix' => $item->getSuffix(),
+ ];
+ }
+}
diff --git a/src/think/route/Url.php b/src/think/route/Url.php
new file mode 100644
index 0000000..6090c30
--- /dev/null
+++ b/src/think/route/Url.php
@@ -0,0 +1,478 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\route;
+
+use think\App;
+use think\Route;
+
+/**
+ * 路由地址生成
+ */
+class Url
+{
+ /**
+ * URL 根地址
+ * @var string
+ */
+ protected $root = '';
+
+ /**
+ * HTTPS
+ * @var bool
+ */
+ protected $https;
+
+ /**
+ * URL后缀
+ * @var string|bool
+ */
+ protected $suffix = true;
+
+ /**
+ * URL域名
+ * @var string|bool
+ */
+ protected $domain = false;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $route 路由对象
+ * @param App $app App对象
+ * @param string $url URL地址
+ * @param array $vars 参数
+ */
+ public function __construct(protected Route $route, protected App $app, protected string $url = '', protected array $vars = [])
+ {
+ }
+
+ /**
+ * 设置URL参数
+ * @access public
+ * @param array $vars URL参数
+ * @return $this
+ */
+ public function vars(array $vars = [])
+ {
+ $this->vars = $vars;
+ return $this;
+ }
+
+ /**
+ * 设置URL后缀
+ * @access public
+ * @param string|bool $suffix URL后缀
+ * @return $this
+ */
+ public function suffix(string|bool $suffix)
+ {
+ $this->suffix = $suffix;
+ return $this;
+ }
+
+ /**
+ * 设置URL域名(或者子域名)
+ * @access public
+ * @param string|bool $domain URL域名
+ * @return $this
+ */
+ public function domain(string|bool $domain)
+ {
+ $this->domain = $domain;
+ return $this;
+ }
+
+ /**
+ * 设置URL 根地址
+ * @access public
+ * @param string $root URL root
+ * @return $this
+ */
+ public function root(string $root)
+ {
+ $this->root = $root;
+ return $this;
+ }
+
+ /**
+ * 设置是否使用HTTPS
+ * @access public
+ * @param bool $https
+ * @return $this
+ */
+ public function https(bool $https = true)
+ {
+ $this->https = $https;
+ return $this;
+ }
+
+ /**
+ * 检测域名
+ * @access protected
+ * @param string $url URL
+ * @param string|true $domain 域名
+ * @return string
+ */
+ protected function parseDomain(string &$url, string|bool $domain): string
+ {
+ if (!$domain) {
+ return '';
+ }
+
+ $request = $this->app->request;
+ $rootDomain = $request->rootDomain();
+
+ if (true === $domain) {
+ // 自动判断域名
+ $domain = $request->host();
+ $domains = $this->route->getDomains();
+
+ if (!empty($domains)) {
+ $routeDomain = array_keys($domains);
+ foreach ($routeDomain as $domainPrefix) {
+ if (str_starts_with($domainPrefix, '*.') && str_contains($domain, ltrim($domainPrefix, '*.')) !== false) {
+ foreach ($domains as $key => $rule) {
+ $rule = is_array($rule) ? $rule[0] : $rule;
+ if (is_string($rule) && !str_contains($key, '*') && str_starts_with($url, $rule)) {
+ $url = ltrim($url, $rule);
+ $domain = $key;
+
+ // 生成对应子域名
+ if (!empty($rootDomain)) {
+ $domain .= $rootDomain;
+ }
+ break;
+ } elseif (str_contains($key, '*')) {
+ if (!empty($rootDomain)) {
+ $domain .= $rootDomain;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ }
+ } elseif (!str_contains($domain, '.') && !str_starts_with($domain, $rootDomain)) {
+ $domain .= '.' . $rootDomain;
+ }
+
+ if (str_contains($domain, '://')) {
+ $scheme = '';
+ } else {
+ $scheme = $this->https || $request->isSsl() ? 'https://' : 'http://';
+ }
+
+ return $scheme . $domain;
+ }
+
+ /**
+ * 解析URL后缀
+ * @access protected
+ * @param string|bool $suffix 后缀
+ * @return string
+ */
+ protected function parseSuffix(string|bool $suffix): string
+ {
+ if ($suffix) {
+ $suffix = true === $suffix ? $this->route->config('url_html_suffix') : $suffix;
+
+ if (is_string($suffix) && $pos = strpos($suffix, '|')) {
+ $suffix = substr($suffix, 0, $pos);
+ }
+ }
+
+ return (empty($suffix) || str_starts_with($suffix, '.')) ? (string) $suffix : '.' . $suffix;
+ }
+
+ /**
+ * 直接解析URL地址
+ * @access protected
+ * @param string $url URL
+ * @param string|bool $domain Domain
+ * @return string
+ */
+ protected function parseUrl(string $url, string | bool &$domain): string
+ {
+ $request = $this->app->request;
+
+ if (str_starts_with($url, '/')) {
+ // 直接作为路由地址解析
+ $url = substr($url, 1);
+ } elseif ('' === $url) {
+ $url = $request->pathinfo();
+ } else {
+ $controller = $request->controller();
+ $path = explode('/', $url);
+ $action = array_pop($path);
+ $controller = empty($path) ? $controller : array_pop($path);
+ $url = $controller . '/' . $action;
+ $auto = $this->route->getName('__think_auto_route__');
+ if (!empty($auto) && !strpos($controller,'.')) {
+ $module = empty($path) ? $request->layer() : array_pop($path);
+ $url = $module . '/' . $url;
+ }
+ }
+
+ return $url;
+ }
+
+ /**
+ * 分析路由规则中的变量
+ * @access protected
+ * @param string $rule 路由规则
+ * @return array
+ */
+ protected function parseVar(string $rule): array
+ {
+ // 提取路由规则中的变量
+ $var = [];
+
+ if (preg_match_all('/<\w+\??>/', $rule, $matches)) {
+ foreach ($matches[0] as $name) {
+ $optional = false;
+
+ if (str_contains($name, '?')) {
+ $name = substr($name, 1, -2);
+ $optional = true;
+ } else {
+ $name = substr($name, 1, -1);
+ }
+
+ $var[$name] = $optional ? 2 : 1;
+ }
+ }
+
+ return $var;
+ }
+
+ /**
+ * 匹配路由地址
+ * @access protected
+ * @param array $rule 路由规则
+ * @param array $vars 路由变量
+ * @param string|bool $allowDomain 允许域名
+ * @return array
+ */
+ protected function getRuleUrl(array $rule, array &$vars = [], string|bool $allowDomain = ''): array
+ {
+ $request = $this->app->request;
+ if (is_string($allowDomain) && !str_contains($allowDomain, '.')) {
+ $allowDomain .= '.' . $request->rootDomain();
+ }
+ $port = $request->port();
+
+ foreach ($rule as $item) {
+ $url = $item['rule'];
+ $pattern = $this->parseVar($url);
+ $domain = $item['domain'];
+ $suffix = $item['suffix'];
+
+ if ('-' == $domain) {
+ $domain = is_string($allowDomain) ? $allowDomain : $request->host(true);
+ }
+
+ if (is_string($allowDomain) && $domain != $allowDomain) {
+ continue;
+ }
+
+ if ($port && !in_array($port, [80, 443])) {
+ $domain .= ':' . $port;
+ }
+
+ if (empty($pattern)) {
+ return [rtrim($url, '?-'), $domain, $suffix];
+ }
+
+ $type = $this->route->config('url_common_param');
+ $keys = [];
+
+ foreach ($pattern as $key => $val) {
+ if (isset($vars[$key])) {
+ $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key, '<' . $key . '>'], $type ? (string) $vars[$key] : urlencode((string) $vars[$key]), $url);
+ $keys[] = $key;
+ $url = str_replace(['/?', '-?'], ['/', '-'], $url);
+ $result = [rtrim($url, '?-'), $domain, $suffix];
+ } elseif (2 == $val) {
+ $url = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url);
+ $url = str_replace(['/?', '-?'], ['/', '-'], $url);
+ $result = [rtrim($url, '?-'), $domain, $suffix];
+ } else {
+ $result = null;
+ $keys = [];
+ break;
+ }
+ }
+
+ $vars = array_diff_key($vars, array_flip($keys));
+
+ if (isset($result)) {
+ return $result;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * 生成URL地址
+ * @access public
+ * @return string
+ */
+ public function build(): string
+ {
+ // 解析URL
+ $url = $this->url;
+ $suffix = $this->suffix;
+ $domain = $this->domain;
+ $request = $this->app->request;
+ $vars = $this->vars;
+
+ if (str_starts_with($url, '[') && $pos = strpos($url, ']')) {
+ // [name] 表示使用路由命名标识生成URL
+ $name = substr($url, 1, $pos - 1);
+ $url = 'name' . substr($url, $pos + 1);
+ }
+
+ if (!str_contains($url, '://') && !str_starts_with($url, '/')) {
+ $info = parse_url($url);
+ $url = !empty($info['path']) ? $info['path'] : '';
+
+ if (isset($info['fragment'])) {
+ // 解析锚点
+ $anchor = $info['fragment'];
+
+ if (str_contains($anchor, '?')) {
+ // 解析参数
+ [$anchor, $info['query']] = explode('?', $anchor, 2);
+ }
+
+ if (str_contains($anchor, '@')) {
+ // 解析域名
+ [$anchor, $domain] = explode('@', $anchor, 2);
+ }
+ } elseif (str_contains($url, '@') && !str_contains($url, '\\')) {
+ // 解析域名
+ [$url, $domain] = explode('@', $url, 2);
+ }
+ }
+
+ if ($url) {
+ $checkName = isset($name) ? $name : $url . (isset($info['query']) ? '?' . $info['query'] : '');
+ $checkDomain = $domain && is_string($domain) ? $domain : null;
+
+ $rule = $this->route->getName($checkName, $checkDomain);
+
+ if (empty($rule) && isset($info['query'])) {
+ $rule = $this->route->getName($url, $checkDomain);
+ // 解析地址里面参数 合并到vars
+ parse_str($info['query'], $params);
+ $vars = array_merge($params, $vars);
+ unset($info['query']);
+ }
+ }
+
+ if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) {
+ // 匹配路由命名标识
+ $url = $match[0];
+
+ if ($domain && !empty($match[1])) {
+ $domain = $match[1];
+ }
+
+ if (!is_null($match[2])) {
+ $suffix = $match[2];
+ }
+ } elseif (!empty($rule) && isset($name)) {
+ throw new \InvalidArgumentException('route name not exists:' . $name);
+ } else {
+ // 检测URL绑定
+ $bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
+
+ if ($bind && str_starts_with($url, $bind)) {
+ $url = substr($url, strlen($bind) + 1);
+ }
+
+ // 路由标识不存在 直接解析
+ $url = $this->parseUrl($url, $domain);
+
+ if (isset($info['query'])) {
+ // 解析地址里面参数 合并到vars
+ parse_str($info['query'], $params);
+ $vars = array_merge($params, $vars);
+ }
+ }
+
+ // 还原URL分隔符
+ $depr = $this->route->config('pathinfo_depr');
+ $url = str_replace('/', $depr, $url);
+
+ $file = $request->baseFile();
+ if ($file && !str_starts_with($request->url(), $file)) {
+ $file = str_replace('\\', '/', dirname($file));
+ }
+
+ $url = rtrim($file, '/') . '/' . $url;
+
+ // URL后缀
+ if (str_ends_with($url, '/') || '' == $url) {
+ $suffix = '';
+ } else {
+ $suffix = $this->parseSuffix($suffix);
+ }
+
+ // 锚点
+ $anchor = !empty($anchor) ? '#' . $anchor : '';
+
+ // 参数组装
+ if (!empty($vars)) {
+ // 添加参数
+ if ($this->route->config('url_common_param')) {
+ $vars = http_build_query($vars);
+ $url .= $suffix . ($vars ? '?' . $vars : '') . $anchor;
+ } else {
+ foreach ($vars as $var => $val) {
+ $val = (string) $val;
+ if ('' !== $val) {
+ $url .= $depr . $var . $depr . urlencode($val);
+ }
+ }
+
+ $url .= $suffix . $anchor;
+ }
+ } else {
+ $url .= $suffix . $anchor;
+ }
+
+ // 检测域名
+ $domain = $this->parseDomain($url, $domain);
+
+ // URL组装
+ return $domain . rtrim($this->root, '/') . '/' . ltrim($url, '/');
+ }
+
+ public function __toString()
+ {
+ return $this->build();
+ }
+
+ public function __debugInfo()
+ {
+ return [
+ 'url' => $this->url,
+ 'vars' => $this->vars,
+ 'suffix' => $this->suffix,
+ 'domain' => $this->domain,
+ ];
+ }
+}
diff --git a/src/think/route/dispatch/Callback.php b/src/think/route/dispatch/Callback.php
new file mode 100644
index 0000000..068daa1
--- /dev/null
+++ b/src/think/route/dispatch/Callback.php
@@ -0,0 +1,117 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route\dispatch;
+
+use think\App;
+use think\exception\ClassNotFoundException;
+use think\helper\Str;
+use think\route\Dispatch;
+
+/**
+ * Callback Dispatcher
+ */
+class Callback extends Dispatch
+{
+ /**
+ * 类名
+ * @var string
+ */
+ protected $class;
+
+ /**
+ * 操作名
+ * @var string
+ */
+ protected $action;
+
+ public function init(App $app)
+ {
+ $this->app = $app;
+ if (is_array($this->dispatch)) {
+ $this->parseDispatch();
+ }
+ $this->doRouteAfter();
+ }
+
+ protected function parseDispatch()
+ {
+ // 执行回调方法
+ [$class, $action] = $this->dispatch;
+ if ($this->miss && !method_exists($class, $action . $this->rule->config('action_suffix'))) {
+ $route = $this->miss->getRoute();
+ if (is_string($route)) {
+ $route = explode('/', $route, 3);
+ }
+ if (is_array($route)) {
+ // 检查分组命名空间绑定
+ $bind = $this->rule->getBind();
+ $type = substr($bind, 0, 1);
+ if ('\\' == $type) {
+ $class = substr($bind, 1);
+ $action = $route[0];
+ } elseif (':' == $type) {
+ [$class, $action] = $route;
+
+ $namespace = substr($bind, 1);
+ $class = trim($namespace, '\\') . '\\' . Str::studly($class);
+ }
+ } else {
+ $vars = $this->getActionBindVars();
+ return $this->app->invoke($route, $vars);
+ }
+ }
+
+ // 设置当前请求的控制器、操作
+ $controllerLayer = $this->rule->config('controller_layer') ?: 'controller';
+ if (str_contains($class, '\\' . $controllerLayer . '\\')) {
+ [$layer, $controller] = explode('/' . $controllerLayer . '/', trim(str_replace('\\', '/', $class), '/'));
+ $layer = trim(str_replace('app', '', $layer), '/');
+ } else {
+ $layer = '';
+ $controller = trim(str_replace('\\', '/', $class), '/');
+ }
+
+ if ($layer && !empty($this->option['auto_middleware'])) {
+ // 自动为顶层layer注册中间件
+ $alias = $this->app->config->get('middleware.alias', []);
+
+ if (isset($alias[$layer])) {
+ $this->option['middleware'] = array_merge($this->option['middleware'] ?? [], [$layer]);
+ }
+ }
+
+ $this->action = $action;
+ $this->class = $class;
+
+ $this->request
+ ->setLayer($layer)
+ ->setController($controller)
+ ->setAction($action);
+ }
+
+ public function exec()
+ {
+ if (is_array($this->dispatch)) {
+ if (class_exists($this->class)) {
+ $instance = $this->app->invokeClass($this->class);
+ } else {
+ throw new ClassNotFoundException('class not exists:' . $this->class, $this->class);
+ }
+
+ return $this->responseWithMiddlewarePipeline($instance, $this->action);
+ }
+
+ $vars = $this->getActionBindVars();
+ return $this->app->invoke($this->dispatch, $vars);
+ }
+}
diff --git a/src/think/route/dispatch/Controller.php b/src/think/route/dispatch/Controller.php
new file mode 100644
index 0000000..b5ab698
--- /dev/null
+++ b/src/think/route/dispatch/Controller.php
@@ -0,0 +1,139 @@
+
+// +----------------------------------------------------------------------
+declare (strict_types = 1);
+
+namespace think\route\dispatch;
+
+use Closure;
+use think\App;
+use think\exception\ClassNotFoundException;
+use think\exception\HttpException;
+use think\helper\Str;
+use think\route\Dispatch;
+
+/**
+ * Controller Dispatcher
+ */
+class Controller extends Dispatch
+{
+ /**
+ * 控制器名
+ * @var string
+ */
+ protected $controller;
+
+ /**
+ * 操作名
+ * @var string
+ */
+ protected $actionName;
+
+ public function init(App $app)
+ {
+ $this->app = $app;
+ $this->parseDispatch();
+ $this->doRouteAfter();
+ }
+
+ protected function parseDispatch()
+ {
+ $path = $this->dispatch;
+ if (is_string($path)) {
+ $path = explode('/', $path);
+ }
+
+ $action = !empty($path) ? array_pop($path) : $this->rule->config('default_action');
+ $controller = !empty($path) ? array_pop($path) : $this->rule->config('default_controller');
+ $layer = !empty($path) ? implode('/', $path) : '';
+
+ if ($layer && !empty($this->option['auto_middleware'])) {
+ // 自动为顶层layer注册中间件
+ $alias = $this->app->config->get('middleware.alias', []);
+
+ if (isset($alias[$layer])) {
+ $this->option['middleware'] = array_merge($this->option['middleware'] ?? [], [$layer]);
+ }
+ }
+
+ // 获取控制器名和分层(目录)名
+ if (str_contains($controller, '.')) {
+ $pos = strrpos($controller, '.');
+ $layer = ($layer ? $layer . '.' : '') . substr($controller, 0, $pos);
+ $controller = Str::studly(substr($controller, $pos + 1));
+ } else {
+ $controller = Str::studly($controller);
+ }
+
+ $this->actionName = strip_tags($action);
+ $this->controller = strip_tags(($layer ? $layer . '.' : '') . $controller);
+
+ // 设置当前请求的控制器、操作
+ $this->request
+ ->setLayer(strip_tags($layer))
+ ->setController($this->controller)
+ ->setAction($this->actionName);
+ }
+
+ public function exec()
+ {
+ try {
+ // 实例化控制器
+ $instance = $this->controller($this->controller);
+ if ($this->miss && !method_exists($instance, $this->actionName . $this->rule->config('action_suffix'))) {
+ throw new ClassNotFoundException('class not exists:');
+ }
+ } catch (ClassNotFoundException $e) {
+ if ($this->miss) {
+ $route = $this->miss->getRoute();
+ if ($route instanceof Closure) {
+ $vars = $this->getActionBindVars();
+ return $this->app->invoke($route, $vars);
+ }
+ // 检查分组绑定
+ $prefix = $this->rule->getOption('prefix');
+ if (!str_starts_with($route, $prefix)) {
+ $route = $prefix . $route;
+ }
+ $this->parseDispatch($route);
+ $instance = $this->controller($this->controller);
+ } else {
+ throw new HttpException(404, 'controller not exists:' . $e->getClass());
+ }
+ }
+
+ return $this->responseWithMiddlewarePipeline($instance, $this->actionName);
+ }
+
+ /**
+ * 实例化访问控制器
+ * @access public
+ * @param string $name 资源地址
+ * @return object
+ * @throws ClassNotFoundException
+ */
+ public function controller(string $name)
+ {
+ $suffix = $this->rule->config('controller_suffix') ? 'Controller' : '';
+
+ $controllerLayer = $this->rule->config('controller_layer') ?: 'controller';
+ $emptyController = $this->rule->config('empty_controller') ?: 'Error';
+
+ $class = $this->app->parseClass($controllerLayer, $name . $suffix);
+
+ if (class_exists($class)) {
+ return $this->app->make($class, [], true);
+ } elseif ($emptyController && class_exists($emptyClass = $this->app->parseClass($controllerLayer, $emptyController . $suffix))) {
+ return $this->app->make($emptyClass, [], true);
+ }
+
+ throw new ClassNotFoundException('class not exists:' . $class, $class);
+ }
+}
diff --git a/src/think/service/ModelService.php b/src/think/service/ModelService.php
new file mode 100644
index 0000000..f5971ae
--- /dev/null
+++ b/src/think/service/ModelService.php
@@ -0,0 +1,58 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\service;
+
+use think\Model;
+use think\Service;
+
+/**
+ * 模型服务类
+ */
+class ModelService extends Service
+{
+ public function boot()
+ {
+ Model::setDb($this->app->db);
+ Model::setEvent($this->app->event);
+ Model::setInvoker([$this->app, 'invoke']);
+ Model::maker(function (Model $model) {
+ if (method_exists($model, 'setOption')) {
+ // 兼容ORM4.0
+ $model->setOption('db', $this->app->db);
+ $model->setOption('event', $this->app->event);
+ $model->setOption('invoker', [$this->app, 'invoke']);
+ }
+ $config = $this->app->config;
+
+ $isAutoWriteTimestamp = $model->getAutoWriteTimestamp();
+
+ if (is_null($isAutoWriteTimestamp)) {
+ // 自动写入时间戳
+ $model->isAutoWriteTimestamp($config->get('database.auto_timestamp', 'timestamp'));
+ }
+
+ $dateFormat = $model->getDateFormat();
+
+ if (is_null($dateFormat)) {
+ // 设置时间戳格式
+ $model->setDateFormat($config->get('database.datetime_format', 'Y-m-d H:i:s'));
+ }
+
+ $timeField = $config->get('database.datetime_field');
+ if (!empty($timeField)) {
+ [$createTime, $updateTime] = explode(',', $timeField);
+ $model->setTimeField($createTime, $updateTime);
+ }
+ });
+ }
+}
diff --git a/src/think/service/PaginatorService.php b/src/think/service/PaginatorService.php
new file mode 100644
index 0000000..ab10a0c
--- /dev/null
+++ b/src/think/service/PaginatorService.php
@@ -0,0 +1,52 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\service;
+
+use think\Paginator;
+use think\paginator\driver\Bootstrap;
+use think\Service;
+
+/**
+ * 分页服务类
+ */
+class PaginatorService extends Service
+{
+ public function register()
+ {
+ if (!$this->app->bound(Paginator::class)) {
+ $this->app->bind(Paginator::class, Bootstrap::class);
+ }
+ }
+
+ public function boot()
+ {
+ Paginator::maker(function (...$args) {
+ return $this->app->make(Paginator::class, $args, true);
+ });
+
+ Paginator::currentPathResolver(function () {
+ return $this->app->request->baseUrl();
+ });
+
+ Paginator::currentPageResolver(function ($varPage = 'page') {
+
+ $page = $this->app->request->param($varPage);
+
+ if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
+ return (int) $page;
+ }
+
+ return 1;
+ });
+ }
+}
diff --git a/src/think/service/ValidateService.php b/src/think/service/ValidateService.php
new file mode 100644
index 0000000..e5c7dc7
--- /dev/null
+++ b/src/think/service/ValidateService.php
@@ -0,0 +1,31 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\service;
+
+use think\Service;
+use think\Validate;
+
+/**
+ * 验证服务类
+ */
+class ValidateService extends Service
+{
+ public function boot()
+ {
+ Validate::maker(function (Validate $validate) {
+ $validate->setLang($this->app->lang);
+ $validate->setDb($this->app->db);
+ $validate->setRequest($this->app->request);
+ });
+ }
+}
diff --git a/src/think/session/Store.php b/src/think/session/Store.php
new file mode 100644
index 0000000..1e28caf
--- /dev/null
+++ b/src/think/session/Store.php
@@ -0,0 +1,325 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\session;
+
+use think\contract\SessionHandlerInterface;
+use think\helper\Arr;
+
+class Store
+{
+ /**
+ * Session数据
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * 是否初始化
+ * @var bool
+ */
+ protected $init = null;
+
+ /**
+ * 记录Session Id
+ * @var string
+ */
+ protected $id;
+
+ /** @var array */
+ protected $serialize = [];
+
+ public function __construct(protected string $name, protected SessionHandlerInterface $handler, ?array $serialize = null)
+ {
+ if (!empty($serialize)) {
+ $this->serialize = $serialize;
+ }
+
+ $this->setId();
+ }
+
+ /**
+ * 设置数据
+ * @access public
+ * @param array $data
+ * @return void
+ */
+ public function setData(array $data): void
+ {
+ $this->data = $data;
+ }
+
+ /**
+ * session初始化
+ * @access public
+ * @return void
+ */
+ public function init(): void
+ {
+ // 读取缓存数据
+ $data = $this->handler->read($this->getId());
+
+ if (!empty($data)) {
+ $this->data = array_merge($this->data, $this->unserialize($data));
+ }
+
+ $this->init = true;
+ }
+
+ /**
+ * 设置SessionName
+ * @access public
+ * @param string $name session_name
+ * @return void
+ */
+ public function setName(string $name): void
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * 获取sessionName
+ * @access public
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * session_id设置
+ * @access public
+ * @param string $id session_id
+ * @return void
+ */
+ public function setId(?string $id = null): void
+ {
+ $this->id = is_string($id) && strlen($id) === 32 && ctype_alnum($id) ? $id : md5(microtime(true) . session_create_id());
+ }
+
+ /**
+ * 获取session_id
+ * @access public
+ * @return string
+ */
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ /**
+ * 获取所有数据
+ * @return array
+ */
+ public function all(): array
+ {
+ return $this->data;
+ }
+
+ /**
+ * session设置
+ * @access public
+ * @param string $name session名称
+ * @param mixed $value session值
+ * @return void
+ */
+ public function set(string $name, $value): void
+ {
+ Arr::set($this->data, $name, $value);
+ }
+
+ /**
+ * session获取
+ * @access public
+ * @param string $name session名称
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function get(string $name, $default = null)
+ {
+ return Arr::get($this->data, $name, $default);
+ }
+
+ /**
+ * session获取并删除
+ * @access public
+ * @param string $name session名称
+ * @param mixed $default 默认值
+ * @return mixed
+ */
+ public function pull(string $name, $default = null)
+ {
+ return Arr::pull($this->data, $name, $default);
+ }
+
+ /**
+ * 添加数据到一个session数组
+ * @access public
+ * @param string $key
+ * @param mixed $value
+ * @return void
+ */
+ public function push(string $key, $value): void
+ {
+ $array = $this->get($key, []);
+
+ $array[] = $value;
+
+ $this->set($key, $array);
+ }
+
+ /**
+ * 判断session数据
+ * @access public
+ * @param string $name session名称
+ * @return bool
+ */
+ public function has(string $name): bool
+ {
+ return Arr::has($this->data, $name);
+ }
+
+ /**
+ * 删除session数据
+ * @access public
+ * @param string $name session名称
+ * @return void
+ */
+ public function delete(string $name): void
+ {
+ Arr::forget($this->data, $name);
+ }
+
+ /**
+ * 清空session数据
+ * @access public
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->data = [];
+ }
+
+ /**
+ * 销毁session
+ */
+ public function destroy(): void
+ {
+ $this->clear();
+
+ $this->regenerate(true);
+ }
+
+ /**
+ * 重新生成session id
+ * @param bool $destroy
+ */
+ public function regenerate(bool $destroy = false): void
+ {
+ if ($destroy) {
+ $this->handler->delete($this->getId());
+ }
+
+ $this->setId();
+ }
+
+ /**
+ * 保存session数据
+ * @access public
+ * @return void
+ */
+ public function save(): void
+ {
+ $this->clearFlashData();
+
+ $sessionId = $this->getId();
+
+ if (!empty($this->data)) {
+ $data = $this->serialize($this->data);
+
+ $this->handler->write($sessionId, $data);
+ } else {
+ $this->handler->delete($sessionId);
+ }
+
+ $this->init = false;
+ }
+
+ /**
+ * session设置 下一次请求有效
+ * @access public
+ * @param string $name session名称
+ * @param mixed $value session值
+ * @return void
+ */
+ public function flash(string $name, $value): void
+ {
+ $this->set($name, $value);
+ $this->push('__flash__.__next__', $name);
+ $this->set('__flash__.__current__', Arr::except($this->get('__flash__.__current__', []), $name));
+ }
+
+ /**
+ * 将本次闪存数据推迟到下次请求
+ *
+ * @return void
+ */
+ public function reflash(): void
+ {
+ $keys = $this->get('__flash__.__current__', []);
+ $values = array_unique(array_merge($this->get('__flash__.__next__', []), $keys));
+ $this->set('__flash__.__next__', $values);
+ $this->set('__flash__.__current__', []);
+ }
+
+ /**
+ * 清空当前请求的session数据
+ * @access public
+ * @return void
+ */
+ public function clearFlashData(): void
+ {
+ Arr::forget($this->data, $this->get('__flash__.__current__', []));
+ if (!empty($next = $this->get('__flash__.__next__', []))) {
+ $this->set('__flash__.__current__', $next);
+ } else {
+ $this->delete('__flash__.__current__');
+ }
+ $this->delete('__flash__.__next__');
+ }
+
+ /**
+ * 序列化数据
+ * @access protected
+ * @param mixed $data
+ * @return string
+ */
+ protected function serialize($data): string
+ {
+ $serialize = $this->serialize[0] ?? 'serialize';
+
+ return $serialize($data);
+ }
+
+ /**
+ * 反序列化数据
+ * @access protected
+ * @param string $data
+ * @return array
+ */
+ protected function unserialize(string $data): array
+ {
+ $unserialize = $this->serialize[1] ?? 'unserialize';
+
+ return (array) $unserialize($data);
+ }
+}
diff --git a/src/think/session/driver/Cache.php b/src/think/session/driver/Cache.php
new file mode 100644
index 0000000..c5560f0
--- /dev/null
+++ b/src/think/session/driver/Cache.php
@@ -0,0 +1,50 @@
+
+// +----------------------------------------------------------------------
+namespace think\session\driver;
+
+use Psr\SimpleCache\CacheInterface;
+use think\contract\SessionHandlerInterface;
+use think\helper\Arr;
+
+class Cache implements SessionHandlerInterface
+{
+
+ /** @var CacheInterface */
+ protected $handler;
+
+ /** @var integer */
+ protected $expire;
+
+ /** @var string */
+ protected $prefix;
+
+ public function __construct(\think\Cache $cache, array $config = [])
+ {
+ $this->handler = $cache->store(Arr::get($config, 'store'));
+ $this->expire = Arr::get($config, 'expire', 1440);
+ $this->prefix = Arr::get($config, 'prefix', '');
+ }
+
+ public function read(string $sessionId): string
+ {
+ return (string) $this->handler->get($this->prefix . $sessionId);
+ }
+
+ public function delete(string $sessionId): bool
+ {
+ return $this->handler->delete($this->prefix . $sessionId);
+ }
+
+ public function write(string $sessionId, string $data): bool
+ {
+ return $this->handler->set($this->prefix . $sessionId, $data, $this->expire);
+ }
+}
diff --git a/src/think/session/driver/File.php b/src/think/session/driver/File.php
new file mode 100644
index 0000000..84bc374
--- /dev/null
+++ b/src/think/session/driver/File.php
@@ -0,0 +1,248 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\session\driver;
+
+use Closure;
+use Exception;
+use FilesystemIterator;
+use Generator;
+use SplFileInfo;
+use think\App;
+use think\contract\SessionHandlerInterface;
+
+/**
+ * Session 文件驱动
+ */
+class File implements SessionHandlerInterface
+{
+ protected $config = [
+ 'path' => '',
+ 'expire' => 1440,
+ 'prefix' => '',
+ 'data_compress' => false,
+ 'gc_probability' => 1,
+ 'gc_divisor' => 100,
+ ];
+
+ public function __construct(App $app, array $config = [])
+ {
+ $this->config = array_merge($this->config, $config);
+
+ if (empty($this->config['path'])) {
+ $this->config['path'] = $app->getRuntimePath() . 'session' . DIRECTORY_SEPARATOR;
+ } elseif (substr($this->config['path'], -1) != DIRECTORY_SEPARATOR) {
+ $this->config['path'] .= DIRECTORY_SEPARATOR;
+ }
+
+ $this->init();
+ }
+
+ /**
+ * 打开Session
+ * @access protected
+ * @throws Exception
+ */
+ protected function init(): void
+ {
+ try {
+ !is_dir($this->config['path']) && mkdir($this->config['path'], 0755, true);
+ } catch (Exception $e) {
+ // 写入失败
+ }
+
+ // 垃圾回收
+ if (random_int(1, $this->config['gc_divisor']) <= $this->config['gc_probability']) {
+ $this->gc();
+ }
+ }
+
+ /**
+ * Session 垃圾回收
+ * @access public
+ * @return void
+ */
+ public function gc(): void
+ {
+ $lifetime = $this->config['expire'];
+ $now = time();
+
+ $files = $this->findFiles($this->config['path'], function (SplFileInfo $item) use ($lifetime, $now) {
+ return $now - $lifetime > $item->getMTime();
+ });
+
+ foreach ($files as $file) {
+ $this->unlink($file->getPathname());
+ }
+ }
+
+ /**
+ * 查找文件
+ * @param string $root
+ * @param Closure $filter
+ * @return Generator
+ */
+ protected function findFiles(string $root, Closure $filter)
+ {
+ $items = new FilesystemIterator($root);
+
+ /** @var SplFileInfo $item */
+ foreach ($items as $item) {
+ if ($item->isDir() && !$item->isLink()) {
+ yield from $this->findFiles($item->getPathname(), $filter);
+ } else {
+ if ($filter($item)) {
+ yield $item;
+ }
+ }
+ }
+ }
+
+ /**
+ * 取得变量的存储文件名
+ * @access protected
+ * @param string $name 缓存变量名
+ * @param bool $auto 是否自动创建目录
+ * @return string
+ */
+ protected function getFileName(string $name, bool $auto = false): string
+ {
+ if ($this->config['prefix']) {
+ // 使用子目录
+ $name = $this->config['prefix'] . DIRECTORY_SEPARATOR . 'sess_' . $name;
+ } else {
+ $name = 'sess_' . $name;
+ }
+
+ $filename = $this->config['path'] . $name;
+ $dir = dirname($filename);
+
+ if ($auto && !is_dir($dir)) {
+ try {
+ mkdir($dir, 0755, true);
+ } catch (Exception $e) {
+ // 创建失败
+ }
+ }
+
+ return $filename;
+ }
+
+ /**
+ * 读取Session
+ * @access public
+ * @param string $sessID
+ * @return string
+ */
+ public function read(string $sessID): string
+ {
+ $filename = $this->getFileName($sessID);
+
+ if (is_file($filename) && filemtime($filename) >= time() - $this->config['expire']) {
+ $content = $this->readFile($filename);
+
+ if ($this->config['data_compress'] && function_exists('gzcompress')) {
+ //启用数据压缩
+ $content = (string) gzuncompress($content);
+ }
+
+ return $content;
+ }
+
+ return '';
+ }
+
+ /**
+ * 写文件(加锁)
+ * @param $path
+ * @param $content
+ * @return bool
+ */
+ protected function writeFile($path, $content): bool
+ {
+ return (bool) file_put_contents($path, $content, LOCK_EX);
+ }
+
+ /**
+ * 读取文件内容(加锁)
+ * @param $path
+ * @return string
+ */
+ protected function readFile($path): string
+ {
+ $contents = '';
+
+ $handle = fopen($path, 'rb');
+
+ if ($handle) {
+ try {
+ if (flock($handle, LOCK_SH)) {
+ clearstatcache(true, $path);
+
+ $contents = fread($handle, filesize($path) ?: 1);
+
+ flock($handle, LOCK_UN);
+ }
+ } finally {
+ fclose($handle);
+ }
+ }
+
+ return $contents;
+ }
+
+ /**
+ * 写入Session
+ * @access public
+ * @param string $sessID
+ * @param string $sessData
+ * @return bool
+ */
+ public function write(string $sessID, string $sessData): bool
+ {
+ $filename = $this->getFileName($sessID, true);
+ $data = $sessData;
+
+ if ($this->config['data_compress'] && function_exists('gzcompress')) {
+ //数据压缩
+ $data = gzcompress($data, 3);
+ }
+
+ return $this->writeFile($filename, $data);
+ }
+
+ /**
+ * 删除Session
+ * @access public
+ * @param string $sessID
+ * @return bool
+ */
+ public function delete(string $sessID): bool
+ {
+ try {
+ return $this->unlink($this->getFileName($sessID));
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * 判断文件是否存在后,删除
+ * @access private
+ * @param string $file
+ * @return bool
+ */
+ private function unlink(string $file): bool
+ {
+ return is_file($file) && unlink($file);
+ }
+}
diff --git a/src/think/view/driver/Php.php b/src/think/view/driver/Php.php
new file mode 100644
index 0000000..6cb00ab
--- /dev/null
+++ b/src/think/view/driver/Php.php
@@ -0,0 +1,209 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think\view\driver;
+
+use RuntimeException;
+use think\App;
+use think\contract\TemplateHandlerInterface;
+use think\helper\Str;
+
+/**
+ * PHP原生模板驱动
+ */
+class Php implements TemplateHandlerInterface
+{
+ protected $template;
+ protected $content;
+ protected $app;
+
+ // 模板引擎参数
+ protected $config = [
+ // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
+ 'auto_rule' => 1,
+ // 视图目录名
+ 'view_dir_name' => 'view',
+ // 应用模板路径
+ 'view_path' => '',
+ // 模板文件后缀
+ 'view_suffix' => 'php',
+ // 模板文件名分隔符
+ 'view_depr' => DIRECTORY_SEPARATOR,
+ ];
+
+ public function __construct(App $app, array $config = [])
+ {
+ $this->app = $app;
+ $this->config = array_merge($this->config, (array) $config);
+ }
+
+ /**
+ * 检测是否存在模板文件
+ * @param string $template 模板文件或者模板规则
+ * @return bool
+ */
+ public function exists(string $template): bool
+ {
+ $template = $this->getTemplateFile($template);
+
+ return is_file($template);
+ }
+
+ protected function getTemplateFile(string $template): string
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ // 获取模板文件名
+ $template = $this->parseTemplate($template);
+ } elseif (!is_file($template)) {
+ $path = $this->config['view_path'] ?: $this->getViewPath($this->app->http->getName());
+ $template = $path . $template;
+ }
+
+ return $template;
+ }
+
+ /**
+ * 渲染模板文件
+ * @param string $template 模板文件
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function fetch(string $template, array $data = []): void
+ {
+ $template = $this->getTemplateFile($template);
+
+ // 模板不存在 抛出异常
+ if (!is_file($template)) {
+ throw new RuntimeException('template not exists:' . $template);
+ }
+
+ $this->template = $template;
+
+ extract($data, EXTR_OVERWRITE);
+
+ include $this->template;
+ }
+
+ /**
+ * 渲染模板内容
+ * @param string $content 模板内容
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function display(string $content, array $data = []): void
+ {
+ $this->content = $content;
+
+ extract($data, EXTR_OVERWRITE);
+ eval('?>' . $this->content);
+ }
+
+ protected function getViewPath(string $app): string
+ {
+ $view = $this->config['view_dir_name'] . DIRECTORY_SEPARATOR;
+ $app = $app ? str_replace('.', DIRECTORY_SEPARATOR, $app) . DIRECTORY_SEPARATOR : '';
+ $paths = [
+ $this->app->getBasePath() . $app . $view,
+ $this->app->getBasePath() . $view . $app,
+ $this->app->getRootPath() . $view . $app
+ ];
+
+ foreach ($paths as $path) {
+ if (is_dir($path)) {
+ return $path;
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * 自动定位模板文件
+ * @param string $template 模板文件规则
+ * @return string
+ */
+ private function parseTemplate(string $template): string
+ {
+ $request = $this->app->request;
+
+ // 获取视图根目录
+ if (str_contains($template, '@')) {
+ // 跨应用调用
+ [$app, $template] = explode('@', $template);
+ } elseif ($this->app->http->getName()) {
+ $app = $this->app->http->getName();
+ } elseif ($request->layer()) {
+ $app = $request->layer();
+ $controller = $request->controller(false, true);
+ }
+
+ if ($this->config['view_path']) {
+ $path = $this->config['view_path'];
+ } else {
+ $path = $this->getViewPath($app ?? $this->app->http->getName());
+ }
+
+ $depr = $this->config['view_depr'];
+
+ if (!str_starts_with($template, '/')) {
+ $template = str_replace(['/', ':'], $depr, $template);
+ $controller = $controller ?? $request->controller();
+ if (str_contains($controller, '.')) {
+ $pos = strrpos($controller, '.');
+ $controller = substr($controller, 0, $pos) . '.' . Str::snake(substr($controller, $pos + 1));
+ } else {
+ $controller = Str::snake($controller);
+ }
+
+ if ($controller) {
+ if ('' == $template) {
+ // 如果模板文件名为空 按照默认规则定位
+ if (2 == $this->config['auto_rule']) {
+ $template = $request->action(true);
+ } elseif (3 == $this->config['auto_rule']) {
+ $template = $request->action();
+ } else {
+ $template = Str::snake($request->action());
+ }
+
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template;
+ } elseif (!str_contains($template, $depr)) {
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template;
+ }
+ }
+ } else {
+ $template = str_replace(['/', ':'], $depr, substr($template, 1));
+ }
+
+ return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.');
+ }
+
+ /**
+ * 配置模板引擎
+ * @param array $config 参数
+ * @return void
+ */
+ public function config(array $config): void
+ {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ /**
+ * 获取模板引擎配置
+ * @param string $name 参数名
+ * @return mixed
+ */
+ public function getConfig(string $name)
+ {
+ return $this->config[$name] ?? null;
+ }
+}
diff --git a/src/tpl/think_exception.tpl b/src/tpl/think_exception.tpl
new file mode 100644
index 0000000..bb32aa2
--- /dev/null
+++ b/src/tpl/think_exception.tpl
@@ -0,0 +1,502 @@
+'.end($names).'';
+ }
+}
+
+if (!function_exists('parse_file')) {
+ function parse_file($file, $line)
+ {
+ return ''.basename($file)." line {$line}".'';
+ }
+}
+
+if (!function_exists('parse_args')) {
+ function parse_args($args)
+ {
+ $result = [];
+ foreach ($args as $key => $item) {
+ switch (true) {
+ case is_object($item):
+ $value = sprintf('object(%s)', parse_class(get_class($item)));
+ break;
+ case is_array($item):
+ if (count($item) > 3) {
+ $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3)));
+ } else {
+ $value = sprintf('[%s]', parse_args($item));
+ }
+ break;
+ case is_string($item):
+ if (strlen($item) > 20) {
+ $value = sprintf(
+ '\'%s...\'',
+ htmlentities($item),
+ htmlentities(substr($item, 0, 20))
+ );
+ } else {
+ $value = sprintf("'%s'", htmlentities($item));
+ }
+ break;
+ case is_int($item):
+ case is_float($item):
+ $value = $item;
+ break;
+ case is_null($item):
+ $value = 'null';
+ break;
+ case is_bool($item):
+ $value = '' . ($item ? 'true' : 'false') . '';
+ break;
+ case is_resource($item):
+ $value = 'resource';
+ break;
+ default:
+ $value = htmlentities(str_replace("\n", '', var_export(strval($item), true)));
+ break;
+ }
+
+ $result[] = is_int($key) ? $value : sprintf('\'%s\' => %s', htmlentities($key), $value);
+ }
+
+ return implode(', ', $result);
+ }
+}
+if (!function_exists('echo_value')) {
+ function echo_value($val)
+ {
+ if (is_array($val) || is_object($val)) {
+ echo htmlentities(json_encode($val, JSON_PRETTY_PRINT));
+ } elseif (is_bool($val)) {
+ echo $val ? 'true' : 'false';
+ } elseif (is_scalar($val)) {
+ echo htmlentities($val);
+ } else {
+ echo 'Resource';
+ }
+ }
+}
+?>
+
+
+
+
+ 系统发生错误
+
+
+
+
+
+ $trace) { ?>
+
+
+
+
+
+
+
Call Stack
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Exception Datas
+ $value) { ?>
+
+
+ empty
+
+
+
+ $val) { ?>
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
Environment Variables
+ $value) { ?>
+
+
+ empty
+
+
+
+ $val) { ?>
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ApiVersionTest.php b/tests/ApiVersionTest.php
new file mode 100644
index 0000000..9dcdab4
--- /dev/null
+++ b/tests/ApiVersionTest.php
@@ -0,0 +1,74 @@
+prepareApp();
+ $this->route = new Route($this->app);
+ }
+
+ protected function tearDown(): void
+ {
+ m::close();
+ }
+
+ protected function makeRequest($path, $method = 'GET', $version = null)
+ {
+ $request = m::mock(Request::class)->makePartial();
+ $request->shouldReceive('host')->andReturn('localhost');
+ $request->shouldReceive('pathinfo')->andReturn($path);
+ $request->shouldReceive('url')->andReturn('/' . $path);
+ $request->shouldReceive('method')->andReturn(strtoupper($method));
+
+ // 修改header方法的mock
+ if ($version !== null) {
+ $request->shouldReceive('header')->andReturnUsing(function($name) use ($version) {
+ return $name === 'Api-Version' ? $version : null;
+ });
+ }
+
+ return $request;
+ }
+
+ public function testApiVersionFromHeader()
+ {
+ $this->route->group('api', function () {
+ $this->route->get('products', function () {
+ return 'v1 products';
+ })->version('1.0');
+
+ $this->route->get('products', function () {
+ return 'v2 products';
+ })->version('2.0');
+ });
+
+ // 测试请求头版本1.0
+ $request = $this->makeRequest('api/products', 'GET', '1.0');
+ // 添加调试信息
+ try {
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('v1 products', $response->getContent());
+ } catch (\think\exception\RouteNotFoundException $e) {
+ var_dump($request->header('Api-Version')); // 检查版本号是否正确传入
+ throw $e;
+ }
+
+ // 测试请求头版本2.0
+ $request = $this->makeRequest('api/products', 'GET', '2.0');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('v2 products', $response->getContent());
+ }
+
+}
diff --git a/tests/AppTest.php b/tests/AppTest.php
new file mode 100644
index 0000000..2f73075
--- /dev/null
+++ b/tests/AppTest.php
@@ -0,0 +1,207 @@
+ 'class',
+ ];
+
+ public function register()
+ {
+
+ }
+
+ public function boot()
+ {
+
+ }
+}
+
+/**
+ * @property array initializers
+ */
+class AppTest extends TestCase
+{
+ /** @var App */
+ protected $app;
+
+ protected function setUp(): void
+ {
+ $this->app = new App();
+ }
+
+ protected function tearDown(): void
+ {
+ m::close();
+ }
+
+ public function testService()
+ {
+ $service = m::mock(SomeService::class);
+
+ $service->shouldReceive('register')->once();
+
+ $this->app->register($service);
+
+ $this->assertEquals($service, $this->app->getService(SomeService::class));
+
+ $service2 = m::mock(SomeService::class);
+
+ $service2->shouldReceive('register')->once();
+
+ $this->app->register($service2);
+
+ $this->assertEquals($service, $this->app->getService(SomeService::class));
+
+ $this->app->register($service2, true);
+
+ $this->assertEquals($service2, $this->app->getService(SomeService::class));
+
+ $service->shouldReceive('boot')->once();
+ $service2->shouldReceive('boot')->once();
+
+ $this->app->boot();
+ }
+
+ public function testDebug()
+ {
+ $this->app->debug(false);
+
+ $this->assertFalse($this->app->isDebug());
+
+ $this->app->debug(true);
+
+ $this->assertTrue($this->app->isDebug());
+ }
+
+ public function testNamespace()
+ {
+ $namespace = 'test';
+
+ $this->app->setNamespace($namespace);
+
+ $this->assertEquals($namespace, $this->app->getNamespace());
+ }
+
+ public function testPath()
+ {
+ $rootPath = __DIR__ . DIRECTORY_SEPARATOR;
+
+ $app = new App($rootPath);
+
+ $this->assertEquals($rootPath, $app->getRootPath());
+
+ $this->assertEquals(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $app->getThinkPath());
+
+ $this->assertEquals($rootPath . 'app' . DIRECTORY_SEPARATOR, $app->getAppPath());
+
+ $appPath = $rootPath . 'app' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR;
+ $app->setAppPath($appPath);
+ $this->assertEquals($appPath, $app->getAppPath());
+
+ $this->assertEquals($rootPath . 'app' . DIRECTORY_SEPARATOR, $app->getBasePath());
+
+ $this->assertEquals($rootPath . 'config' . DIRECTORY_SEPARATOR, $app->getConfigPath());
+
+ $this->assertEquals($rootPath . 'runtime' . DIRECTORY_SEPARATOR, $app->getRuntimePath());
+
+ $runtimePath = $rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR;
+ $app->setRuntimePath($runtimePath);
+ $this->assertEquals($runtimePath, $app->getRuntimePath());
+ }
+
+ /**
+ * @param vfsStreamDirectory $root
+ * @param bool $debug
+ * @return App
+ */
+ protected function prepareAppForInitialize(vfsStreamDirectory $root, $debug = true)
+ {
+ $rootPath = $root->url() . DIRECTORY_SEPARATOR;
+
+ $app = new App($rootPath);
+
+ $initializer = m::mock();
+ $initializer->shouldReceive('init')->once()->with($app);
+
+ $app->instance($initializer->mockery_getName(), $initializer);
+
+ (function () use ($initializer) {
+ $this->initializers = [$initializer->mockery_getName()];
+ })->call($app);
+
+ $env = m::mock(Env::class);
+ $env->shouldReceive('load')->once()->with($rootPath . '.env');
+ $env->shouldReceive('get')->once()->with('config_ext', '.php')->andReturn('.php');
+ $env->shouldReceive('get')->once()->with('app_debug')->andReturn($debug);
+ $env->shouldReceive('get')->once()->with('env_name', '')->andReturn('');
+
+ $event = m::mock(Event::class);
+ $event->shouldReceive('trigger')->once()->with(AppInit::class);
+ $event->shouldReceive('bind')->once()->with([]);
+ $event->shouldReceive('listenEvents')->once()->with([]);
+ $event->shouldReceive('subscribe')->once()->with([]);
+
+ $app->instance('env', $env);
+ $app->instance('event', $event);
+
+ return $app;
+ }
+
+ public function testInitialize()
+ {
+ $root = vfsStream::setup('rootDir', null, [
+ '.env' => '',
+ 'app' => [
+ 'common.php' => '',
+ 'event.php' => '[],"listen"=>[],"subscribe"=>[]];',
+ 'provider.php' => ' [
+ 'app.php' => 'prepareAppForInitialize($root, true);
+
+ $app->debug(false);
+
+ $app->initialize();
+
+ $this->assertIsInt($app->getBeginMem());
+ $this->assertIsFloat($app->getBeginTime());
+
+ $this->assertTrue($app->initialized());
+ }
+
+ public function testFactory()
+ {
+ $this->assertInstanceOf(stdClass::class, App::factory(stdClass::class));
+
+ $this->expectException(ClassNotFoundException::class);
+
+ App::factory('SomeClass');
+ }
+
+ public function testParseClass()
+ {
+ $this->assertEquals('app\\controller\\SomeClass', $this->app->parseClass('controller', 'some_class'));
+ $this->app->setNamespace('app2');
+ $this->assertEquals('app2\\controller\\SomeClass', $this->app->parseClass('controller', 'some_class'));
+ }
+
+}
diff --git a/tests/CacheTest.php b/tests/CacheTest.php
new file mode 100644
index 0000000..a469ec1
--- /dev/null
+++ b/tests/CacheTest.php
@@ -0,0 +1,154 @@
+app = m::mock(App::class)->makePartial();
+
+ Container::setInstance($this->app);
+ $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
+ $this->config = m::mock(Config::class)->makePartial();
+ $this->app->shouldReceive('get')->with('config')->andReturn($this->config);
+
+ $this->cache = new Cache($this->app);
+ }
+
+ public function testGetConfig()
+ {
+ $config = [
+ 'default' => 'file',
+ ];
+
+ $this->config->shouldReceive('get')->with('cache')->andReturn($config);
+
+ $this->assertEquals($config, $this->cache->getConfig());
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->cache->getStoreConfig('foo');
+ }
+
+ public function testCacheManagerInstances()
+ {
+ $this->config->shouldReceive('get')->with("cache.stores.single", null)->andReturn(['type' => 'file']);
+
+ $channel1 = $this->cache->store('single');
+ $channel2 = $this->cache->store('single');
+
+ $this->assertSame($channel1, $channel2);
+ }
+
+ public function testFileCache()
+ {
+ $root = vfsStream::setup();
+
+ $this->config->shouldReceive('get')->with("cache.default", null)->andReturn('file');
+
+ $this->config->shouldReceive('get')->with("cache.stores.file", null)
+ ->andReturn(['type' => 'file', 'path' => $root->url()]);
+
+ $this->cache->set('foo', 5);
+ $this->cache->inc('foo');
+ $this->assertEquals(6, $this->cache->get('foo'));
+ $this->cache->dec('foo', 2);
+ $this->assertEquals(4, $this->cache->get('foo'));
+
+ $this->cache->set('bar', true);
+ $this->assertTrue($this->cache->get('bar'));
+
+ $this->cache->set('baz', null);
+ $this->assertTrue($this->cache->has('baz'));
+ $this->assertNull($this->cache->get('baz'));
+
+ $this->cache->delete('baz');
+ $this->assertFalse($this->cache->has('baz'));
+ $this->assertNull($this->cache->get('baz'));
+ $this->assertFalse($this->cache->get('baz', false));
+
+ $this->assertTrue($root->hasChildren());
+ $this->cache->clear();
+ $this->assertFalse($root->hasChildren());
+
+ //tags
+ $this->cache->tag('foo')->set('bar', 'foobar');
+ $this->assertEquals('foobar', $this->cache->get('bar'));
+ $this->cache->tag('foo')->remember('baz', 'foobar');
+ $this->assertEquals('foobar', $this->cache->get('baz'));
+ $this->cache->tag('foo')->clear();
+ $this->assertFalse($this->cache->has('bar'));
+
+ //multiple
+ $this->cache->setMultiple(['foo' => ['foobar', 'bar'], 'foobar' => ['foo', 'bar']]);
+ $this->cache->tag('foo')->setMultiple(['foo' => ['foobar', 'bar'], 'foobar' => ['foo', 'bar']]);
+ $this->assertEquals(['foo' => ['foobar', 'bar'], 'foobar' => ['foo', 'bar']], $this->cache->getMultiple(['foo', 'foobar']));
+ $this->assertIsInt($this->cache->getWriteTimes());
+ $this->assertTrue($this->cache->deleteMultiple(['foo', 'foobar']));
+ }
+
+ public function testRedisCache()
+ {
+ if (extension_loaded('redis')) {
+ return;
+ }
+ $this->config->shouldReceive('get')->with("cache.default", null)->andReturn('redis');
+ $this->config->shouldReceive('get')->with("cache.stores.redis", null)->andReturn(['type' => 'redis']);
+
+ $redis = m::mock('overload:\Predis\Client');
+
+ $redis->shouldReceive("set")->once()->with('foo', 5)->andReturnTrue();
+ $redis->shouldReceive("incrby")->once()->with('foo', 1)->andReturnTrue();
+ $redis->shouldReceive("decrby")->once()->with('foo', 2)->andReturnTrue();
+ $redis->shouldReceive("get")->once()->with('foo')->andReturn('6');
+ $redis->shouldReceive("get")->once()->with('foo')->andReturn('4');
+ $redis->shouldReceive("set")->once()->with('bar', serialize(true))->andReturnTrue();
+ $redis->shouldReceive("set")->once()->with('baz', serialize(null))->andReturnTrue();
+ $redis->shouldReceive("del")->once()->with('baz')->andReturnTrue();
+ $redis->shouldReceive("flushDB")->once()->andReturnTrue();
+ $redis->shouldReceive("set")->once()->with('bar', serialize('foobar'))->andReturnTrue();
+ $redis->shouldReceive("sAdd")->once()->with('tag:' . md5('foo'), 'bar')->andReturnTrue();
+ $redis->shouldReceive("sMembers")->once()->with('tag:' . md5('foo'))->andReturn(['bar']);
+ $redis->shouldReceive("del")->once()->with(['bar'])->andReturnTrue();
+ $redis->shouldReceive("del")->once()->with('tag:' . md5('foo'))->andReturnTrue();
+
+ $this->cache->set('foo', 5);
+ $this->cache->inc('foo');
+ $this->assertEquals(6, $this->cache->get('foo'));
+ $this->cache->dec('foo', 2);
+ $this->assertEquals(4, $this->cache->get('foo'));
+
+ $this->cache->set('bar', true);
+ $this->cache->set('baz', null);
+ $this->cache->delete('baz');
+ $this->cache->clear();
+
+ //tags
+ $this->cache->tag('foo')->set('bar', 'foobar');
+ $this->cache->tag('foo')->clear();
+ }
+}
diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php
new file mode 100644
index 0000000..444fc97
--- /dev/null
+++ b/tests/ConfigTest.php
@@ -0,0 +1,205 @@
+setContent(" 'value1','key2'=>'value2'];");
+ $root->addChild($file);
+
+ $config = new Config();
+
+ $config->load($file->url(), 'test');
+
+ $this->assertEquals('value1', $config->get('test.key1'));
+ $this->assertEquals('value2', $config->get('test.key2'));
+
+ $this->assertSame(['key1' => 'value1', 'key2' => 'value2'], $config->get('test'));
+ }
+
+ public function testSetAndGet()
+ {
+ $config = new Config();
+
+ $config->set([
+ 'key1' => 'value1',
+ 'key2' => [
+ 'key3' => 'value3',
+ ],
+ ], 'test');
+
+ $this->assertTrue($config->has('test.key1'));
+ $this->assertEquals('value1', $config->get('test.key1'));
+ $this->assertEquals('value3', $config->get('test.key2.key3'));
+
+ $this->assertEquals(['key3' => 'value3'], $config->get('test.key2'));
+ $this->assertFalse($config->has('test.key3'));
+ $this->assertEquals('none', $config->get('test.key3', 'none'));
+ }
+
+ public function testGlobalHook()
+ {
+ $config = new Config();
+
+ // Set initial config
+ $config->set(['key1' => 'original_value'], 'test');
+
+ // Register global hook
+ $config->hook(function ($name, $value) {
+ if ($name === 'test.key1') {
+ return 'hooked_value';
+ }
+ if ($name === 'test.key2' && is_null($value)) {
+ return 'default_from_hook';
+ }
+ return $value;
+ });
+
+ // Test hook modifies existing value
+ $this->assertEquals('hooked_value', $config->get('test.key1'));
+
+ // Test hook provides default for non-existent key
+ $this->assertEquals('default_from_hook', $config->get('test.key2'));
+
+ // Test hook returns original value for other keys
+ $config->set(['key3' => 'unchanged'], 'test');
+ $this->assertEquals('unchanged', $config->get('test.key3'));
+ }
+
+ public function testSpecificKeyHook()
+ {
+ $config = new Config();
+
+ // Set initial config
+ $config->set([
+ 'key1' => 'value1',
+ 'key2' => 'value2'
+ ], 'test');
+
+ $config->set([
+ 'key1' => 'value1'
+ ], 'other');
+
+ // Register hook for specific key 'test'
+ $config->hook(function ($name, $value) {
+ if (str_contains($name, 'key1')) {
+ return 'test_hooked_' . $value;
+ }
+ return $value;
+ }, 'test');
+
+ // Register hook for specific key 'other'
+ $config->hook(function ($name, $value) {
+ if (str_contains($name, 'key1')) {
+ return 'other_hooked_' . $value;
+ }
+ return $value;
+ }, 'other');
+
+ // Test specific hook for 'test' configuration
+ $this->assertEquals('test_hooked_value1', $config->get('test.key1'));
+ $this->assertEquals('value2', $config->get('test.key2')); // No hook for key2
+
+ // Test specific hook for 'other' configuration
+ $this->assertEquals('other_hooked_value1', $config->get('other.key1'));
+ }
+
+ public function testHookPriority()
+ {
+ $config = new Config();
+
+ // Set initial config
+ $config->set(['key1' => 'value1'], 'test');
+
+ // Register global hook first
+ $config->hook(function ($name, $value) {
+ return 'global_' . $value;
+ });
+
+ // Register specific key hook (should override global)
+ $config->hook(function ($name, $value) {
+ return 'specific_' . $value;
+ }, 'test');
+
+ // Specific hook should take priority over global hook
+ $this->assertEquals('specific_value1', $config->get('test.key1'));
+ }
+
+ public function testHookWithNullReturn()
+ {
+ $config = new Config();
+
+ // Register hook that returns null
+ $config->hook(function ($name, $value) {
+ if ($name === 'test.nonexistent') {
+ return null; // This should trigger default value
+ }
+ return $value;
+ });
+
+ // Test with default value when hook returns null
+ $this->assertEquals('default_value', $config->get('test.nonexistent', 'default_value'));
+ }
+
+ public function testHookWithTopLevelConfig()
+ {
+ $config = new Config();
+
+ // Set top-level config
+ $config->set(['key1' => 'value1', 'key2' => 'value2'], 'database');
+
+ // Register hook for database config
+ $config->hook(function ($name, $value) {
+ if ($name === 'database') {
+ return array_merge($value, ['key3' => 'added_by_hook']);
+ }
+ return $value;
+ }, 'database');
+
+ // Test hook modifies entire config section
+ $result = $config->get('database');
+ $this->assertIsArray($result);
+ $this->assertEquals('value1', $result['key1']);
+ $this->assertEquals('value2', $result['key2']);
+ $this->assertEquals('added_by_hook', $result['key3']);
+ }
+
+ public function testLazyLoadingBehavior()
+ {
+ $config = new Config();
+
+ // Counter to verify hook is called
+ $hookCallCount = 0;
+
+ // Register hook with counter
+ $config->hook(function ($name, $value) use (&$hookCallCount) {
+ $hookCallCount++;
+ return $value ? $value . '_processed' : 'processed_default';
+ });
+
+ // Set config value
+ $config->set(['key1' => 'value1'], 'test');
+
+ // First call should trigger hook
+ $result1 = $config->get('test.key1');
+ $this->assertEquals('value1_processed', $result1);
+ $this->assertEquals(1, $hookCallCount);
+
+ // Second call should also trigger hook (no caching)
+ $result2 = $config->get('test.key1');
+ $this->assertEquals('value1_processed', $result2);
+ $this->assertEquals(2, $hookCallCount);
+
+ // Test with non-existent key
+ $result3 = $config->get('test.nonexistent', 'default');
+ $this->assertEquals('processed_default', $result3);
+ $this->assertEquals(3, $hookCallCount);
+ }
+}
diff --git a/tests/ConsoleTest.php b/tests/ConsoleTest.php
new file mode 100644
index 0000000..639eda2
--- /dev/null
+++ b/tests/ConsoleTest.php
@@ -0,0 +1,292 @@
+setName('test:command')
+ ->setDescription('Test command for unit testing');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $output->writeln('Test command executed');
+ return 0;
+ }
+}
+
+class ConsoleTest extends TestCase
+{
+ use InteractsWithApp;
+
+ /** @var Console */
+ protected $console;
+
+ /** @var Config|MockInterface */
+ protected $config;
+
+ protected function setUp(): void
+ {
+ $this->prepareApp();
+
+ $this->config = m::mock(Config::class);
+ $this->app->shouldReceive('get')->with('config')->andReturn($this->config);
+ $this->app->config = $this->config;
+
+ // Mock initialization
+ $this->app->shouldReceive('initialized')->andReturn(false);
+ $this->app->shouldReceive('initialize')->once();
+
+ // Mock config get calls
+ $this->config->shouldReceive('get')->with('app.url', 'http://localhost')->andReturn('http://localhost');
+ $this->config->shouldReceive('get')->with('console.user')->andReturn(null);
+ $this->config->shouldReceive('get')->with('console.commands', [])->andReturn([]);
+
+ // Mock starting callbacks
+ Console::starting(function () {
+ // Empty callback for testing
+ });
+ }
+
+ protected function tearDown(): void
+ {
+ m::close();
+ }
+
+ public function testConstructor()
+ {
+ $console = new Console($this->app);
+ $this->assertInstanceOf(Console::class, $console);
+ }
+
+ public function testStartingCallbacks()
+ {
+ $callbackExecuted = false;
+
+ Console::starting(function (Console $console) use (&$callbackExecuted) {
+ $callbackExecuted = true;
+ $this->assertInstanceOf(Console::class, $console);
+ });
+
+ $console = new Console($this->app);
+ $this->assertTrue($callbackExecuted);
+ }
+
+ public function testAddCommand()
+ {
+ $console = new Console($this->app);
+ $command = new TestConsoleCommand();
+
+ $console->addCommand($command);
+
+ $this->assertTrue($console->hasCommand('test:command'));
+ }
+
+ public function testAddCommands()
+ {
+ $console = new Console($this->app);
+ $commands = [
+ new TestConsoleCommand(),
+ 'help' => Help::class
+ ];
+
+ $console->addCommands($commands);
+
+ $this->assertTrue($console->hasCommand('test:command'));
+ $this->assertTrue($console->hasCommand('help'));
+ }
+
+ public function testHasCommand()
+ {
+ $console = new Console($this->app);
+
+ // Test default commands
+ $this->assertTrue($console->hasCommand('help'));
+ $this->assertTrue($console->hasCommand('list'));
+ $this->assertFalse($console->hasCommand('nonexistent'));
+ }
+
+ public function testGetCommand()
+ {
+ $console = new Console($this->app);
+
+ $helpCommand = $console->getCommand('help');
+ $this->assertInstanceOf(Help::class, $helpCommand);
+
+ $listCommand = $console->getCommand('list');
+ $this->assertInstanceOf(Lists::class, $listCommand);
+ }
+
+ public function testGetNonexistentCommand()
+ {
+ $console = new Console($this->app);
+
+ $this->expectException(\InvalidArgumentException::class);
+ $console->getCommand('nonexistent');
+ }
+
+ public function testAllCommands()
+ {
+ $console = new Console($this->app);
+
+ $commands = $console->all();
+ $this->assertIsArray($commands);
+ $this->assertArrayHasKey('help', $commands);
+ $this->assertArrayHasKey('list', $commands);
+ }
+
+ public function testGetNamespace()
+ {
+ $console = new Console($this->app);
+
+ $makeCommands = $console->all('make');
+ $this->assertIsArray($makeCommands);
+
+ // Check if make commands exist
+ $commandNames = array_keys($makeCommands);
+ $makeCommandNames = array_filter($commandNames, function ($name) {
+ return strpos($name, 'make:') === 0;
+ });
+ $this->assertNotEmpty($makeCommandNames);
+ }
+
+ public function testFindCommand()
+ {
+ $console = new Console($this->app);
+
+ // Test exact match
+ $command = $console->find('help');
+ $this->assertInstanceOf(Help::class, $command);
+
+ // Test partial match
+ $command = $console->find('hel');
+ $this->assertInstanceOf(Help::class, $command);
+ }
+
+ public function testFindAmbiguousCommand()
+ {
+ $console = new Console($this->app);
+
+ // Add commands that could be ambiguous
+ $console->addCommand(new class extends Command {
+ protected function configure()
+ {
+ $this->setName('test:one');
+ }
+ });
+
+ $console->addCommand(new class extends Command {
+ protected function configure()
+ {
+ $this->setName('test:two');
+ }
+ });
+
+ $this->expectException(\InvalidArgumentException::class);
+ $console->find('test');
+ }
+
+ public function testSetCatchExceptions()
+ {
+ $console = new Console($this->app);
+
+ // setCatchExceptions doesn't return value, just test it doesn't throw
+ $console->setCatchExceptions(false);
+ $console->setCatchExceptions(true);
+
+ $this->assertTrue(true); // Test passes if no exception thrown
+ }
+
+ public function testSetAutoExit()
+ {
+ $console = new Console($this->app);
+
+ // setAutoExit doesn't return value, just test it doesn't throw
+ $console->setAutoExit(false);
+
+ $this->assertTrue(true); // Test passes if no exception thrown
+ }
+
+ // Note: getDefaultCommand and setDefaultCommand methods don't exist in this Console implementation
+
+ public function testGetDefinition()
+ {
+ $console = new Console($this->app);
+
+ $definition = $console->getDefinition();
+ $this->assertInstanceOf(\think\console\input\Definition::class, $definition);
+ }
+
+ public function testGetHelp()
+ {
+ $console = new Console($this->app);
+
+ $help = $console->getHelp();
+ $this->assertIsString($help);
+ // Just test that help returns a string, don't check specific content
+ }
+
+ public function testSetUser()
+ {
+ $console = new Console($this->app);
+
+ // Test setting user (this would normally change process user)
+ // We just test that the method exists and doesn't throw
+ $console->setUser('www-data');
+ $this->assertTrue(true); // If we get here, no exception was thrown
+ }
+
+ public function testCall()
+ {
+ $console = new Console($this->app);
+
+ // call() returns Output object, just test it doesn't throw
+ $result = $console->call('help');
+ $this->assertInstanceOf(\think\console\Output::class, $result);
+ }
+
+ public function testCallWithParameters()
+ {
+ $console = new Console($this->app);
+
+ // call() returns Output object, just test it doesn't throw
+ $result = $console->call('help', ['command_name' => 'list']);
+ $this->assertInstanceOf(\think\console\Output::class, $result);
+ }
+
+ // Note: output() method doesn't exist in this Console implementation
+
+ public function testAddCommandWithString()
+ {
+ $console = new Console($this->app);
+
+ // Test adding command by class name
+ $console->addCommand(TestConsoleCommand::class);
+ $this->assertTrue($console->hasCommand('test:command'));
+ }
+
+ // Note: Custom commands config loading might not work as expected, removing this test
+
+ public function testMakeRequestWithCustomUrl()
+ {
+ // Test with custom URL configuration
+ $this->config->shouldReceive('get')->with('app.url', 'http://localhost')->andReturn('https://example.com/app');
+
+ $console = new Console($this->app);
+ $this->assertInstanceOf(Console::class, $console);
+ }
+}
\ No newline at end of file
diff --git a/tests/CookieTest.php b/tests/CookieTest.php
new file mode 100644
index 0000000..3db699c
--- /dev/null
+++ b/tests/CookieTest.php
@@ -0,0 +1,347 @@
+request = m::mock(Request::class);
+ $this->config = m::mock(Config::class);
+
+ $this->cookie = new Cookie($this->request, [
+ 'expire' => 3600,
+ 'path' => '/',
+ 'domain' => 'test.com',
+ 'secure' => false,
+ 'httponly' => true,
+ 'samesite' => 'lax'
+ ]);
+ }
+
+ protected function tearDown(): void
+ {
+ m::close();
+ }
+
+ public function testMakeMethod()
+ {
+ $this->config->shouldReceive('get')
+ ->with('cookie')
+ ->andReturn(['expire' => 7200]);
+
+ $cookie = Cookie::__make($this->request, $this->config);
+
+ $this->assertInstanceOf(Cookie::class, $cookie);
+ }
+
+ public function testGet()
+ {
+ $this->request->shouldReceive('cookie')
+ ->with('test_cookie', 'default')
+ ->andReturn('cookie_value');
+
+ $result = $this->cookie->get('test_cookie', 'default');
+
+ $this->assertEquals('cookie_value', $result);
+ }
+
+ public function testGetAll()
+ {
+ $this->request->shouldReceive('cookie')
+ ->with('', null)
+ ->andReturn(['cookie1' => 'value1', 'cookie2' => 'value2']);
+
+ $result = $this->cookie->get();
+
+ $this->assertEquals(['cookie1' => 'value1', 'cookie2' => 'value2'], $result);
+ }
+
+ public function testHas()
+ {
+ $this->request->shouldReceive('has')
+ ->with('test_cookie', 'cookie')
+ ->andReturn(true);
+
+ $result = $this->cookie->has('test_cookie');
+
+ $this->assertTrue($result);
+ }
+
+ public function testHasReturnsFalse()
+ {
+ $this->request->shouldReceive('has')
+ ->with('nonexistent_cookie', 'cookie')
+ ->andReturn(false);
+
+ $result = $this->cookie->has('nonexistent_cookie');
+
+ $this->assertFalse($result);
+ }
+
+ public function testSetBasic()
+ {
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', 'test_value');
+
+ $this->cookie->set('test_cookie', 'test_value');
+
+ $cookies = $this->cookie->getCookie();
+ $this->assertArrayHasKey('test_cookie', $cookies);
+ $this->assertEquals('test_value', $cookies['test_cookie'][0]);
+ }
+
+ public function testSetWithNumericExpire()
+ {
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', 'test_value');
+
+ $this->cookie->set('test_cookie', 'test_value', 7200);
+
+ $cookies = $this->cookie->getCookie();
+ $this->assertArrayHasKey('test_cookie', $cookies);
+ $this->assertGreaterThan(time(), $cookies['test_cookie'][1]);
+ }
+
+ public function testSetWithDateTimeExpire()
+ {
+ $expire = new DateTime('+1 hour');
+
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', 'test_value');
+
+ $this->cookie->set('test_cookie', 'test_value', $expire);
+
+ $cookies = $this->cookie->getCookie();
+ $this->assertEquals($expire->getTimestamp(), $cookies['test_cookie'][1]);
+ }
+
+ public function testSetWithArrayOptions()
+ {
+ $options = [
+ 'expire' => 1800,
+ 'path' => '/test',
+ 'domain' => 'example.com',
+ 'secure' => true,
+ 'httponly' => false,
+ 'samesite' => 'strict'
+ ];
+
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', 'test_value');
+
+ $this->cookie->set('test_cookie', 'test_value', $options);
+
+ $cookies = $this->cookie->getCookie();
+ $cookieData = $cookies['test_cookie'];
+
+ $this->assertEquals('test_value', $cookieData[0]);
+ $this->assertGreaterThan(time(), $cookieData[1]);
+ $this->assertEquals('/test', $cookieData[2]['path']);
+ $this->assertEquals('example.com', $cookieData[2]['domain']);
+ $this->assertTrue($cookieData[2]['secure']);
+ $this->assertFalse($cookieData[2]['httponly']);
+ $this->assertEquals('strict', $cookieData[2]['samesite']);
+ }
+
+ public function testSetWithDateTimeInOptions()
+ {
+ $expire = new DateTime('+2 hours');
+ $options = ['expire' => $expire];
+
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', 'test_value');
+
+ $this->cookie->set('test_cookie', 'test_value', $options);
+
+ $cookies = $this->cookie->getCookie();
+ $this->assertEquals($expire->getTimestamp(), $cookies['test_cookie'][1]);
+ }
+
+ public function testForever()
+ {
+ $this->request->shouldReceive('setCookie')
+ ->with('forever_cookie', 'forever_value');
+
+ $this->cookie->forever('forever_cookie', 'forever_value');
+
+ $cookies = $this->cookie->getCookie();
+ $this->assertArrayHasKey('forever_cookie', $cookies);
+ $this->assertEquals('forever_value', $cookies['forever_cookie'][0]);
+ $this->assertGreaterThan(time() + 315360000 - 10, $cookies['forever_cookie'][1]);
+ }
+
+ public function testForeverWithOptions()
+ {
+ $options = ['path' => '/forever', 'secure' => true];
+
+ $this->request->shouldReceive('setCookie')
+ ->with('forever_cookie', 'forever_value');
+
+ $this->cookie->forever('forever_cookie', 'forever_value', $options);
+
+ $cookies = $this->cookie->getCookie();
+ $cookieData = $cookies['forever_cookie'];
+
+ $this->assertEquals('/forever', $cookieData[2]['path']);
+ $this->assertTrue($cookieData[2]['secure']);
+ $this->assertGreaterThan(time() + 315360000 - 10, $cookieData[1]);
+ }
+
+ public function testForeverWithNullOptions()
+ {
+ $this->request->shouldReceive('setCookie')
+ ->with('forever_cookie', 'forever_value');
+
+ $this->cookie->forever('forever_cookie', 'forever_value', null);
+
+ $cookies = $this->cookie->getCookie();
+ $this->assertArrayHasKey('forever_cookie', $cookies);
+ }
+
+ public function testForeverWithNumericOptions()
+ {
+ $this->request->shouldReceive('setCookie')
+ ->with('forever_cookie', 'forever_value');
+
+ $this->cookie->forever('forever_cookie', 'forever_value', 123);
+
+ $cookies = $this->cookie->getCookie();
+ $this->assertArrayHasKey('forever_cookie', $cookies);
+ }
+
+ public function testDelete()
+ {
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', null);
+
+ $this->cookie->delete('test_cookie');
+
+ $cookies = $this->cookie->getCookie();
+ $this->assertArrayHasKey('test_cookie', $cookies);
+ $this->assertEquals('', $cookies['test_cookie'][0]);
+ $this->assertLessThan(time(), $cookies['test_cookie'][1]);
+ }
+
+ public function testDeleteWithOptions()
+ {
+ $options = ['path' => '/test', 'domain' => 'example.com'];
+
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', null);
+
+ $this->cookie->delete('test_cookie', $options);
+
+ $cookies = $this->cookie->getCookie();
+ $cookieData = $cookies['test_cookie'];
+
+ $this->assertEquals('', $cookieData[0]);
+ $this->assertEquals('/test', $cookieData[2]['path']);
+ $this->assertEquals('example.com', $cookieData[2]['domain']);
+ }
+
+ public function testGetCookie()
+ {
+ $this->request->shouldReceive('setCookie')
+ ->with('cookie1', 'value1');
+ $this->request->shouldReceive('setCookie')
+ ->with('cookie2', 'value2');
+
+ $this->cookie->set('cookie1', 'value1');
+ $this->cookie->set('cookie2', 'value2');
+
+ $cookies = $this->cookie->getCookie();
+
+ $this->assertArrayHasKey('cookie1', $cookies);
+ $this->assertArrayHasKey('cookie2', $cookies);
+ $this->assertEquals('value1', $cookies['cookie1'][0]);
+ $this->assertEquals('value2', $cookies['cookie2'][0]);
+ }
+
+ public function testSave()
+ {
+ // Mock the protected saveCookie method by extending the class
+ $cookie = new class($this->request) extends Cookie {
+ public $savedCookies = [];
+
+ protected function saveCookie(string $name, string $value, int $expire, string $path, string $domain, bool $secure, bool $httponly, string $samesite): void
+ {
+ $this->savedCookies[] = [
+ 'name' => $name,
+ 'value' => $value,
+ 'expire' => $expire,
+ 'path' => $path,
+ 'domain' => $domain,
+ 'secure' => $secure,
+ 'httponly' => $httponly,
+ 'samesite' => $samesite,
+ ];
+ }
+ };
+
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', 'test_value');
+
+ $cookie->set('test_cookie', 'test_value');
+ $cookie->save();
+
+ $this->assertCount(1, $cookie->savedCookies);
+ $this->assertEquals('test_cookie', $cookie->savedCookies[0]['name']);
+ $this->assertEquals('test_value', $cookie->savedCookies[0]['value']);
+ }
+
+ public function testCaseInsensitiveConfig()
+ {
+ $cookie = new Cookie($this->request, [
+ 'EXPIRE' => 1800,
+ 'PATH' => '/test',
+ 'DOMAIN' => 'TEST.COM'
+ ]);
+
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', 'test_value');
+
+ $cookie->set('test_cookie', 'test_value');
+
+ $cookies = $cookie->getCookie();
+ $cookieData = $cookies['test_cookie'];
+
+ $this->assertEquals('/test', $cookieData[2]['path']);
+ $this->assertEquals('TEST.COM', $cookieData[2]['domain']);
+ }
+
+ public function testDefaultConfig()
+ {
+ $cookie = new Cookie($this->request);
+
+ $this->request->shouldReceive('setCookie')
+ ->with('test_cookie', 'test_value');
+
+ $cookie->set('test_cookie', 'test_value');
+
+ $cookies = $cookie->getCookie();
+ $cookieData = $cookies['test_cookie'];
+
+ $this->assertEquals('/', $cookieData[2]['path']);
+ $this->assertEquals('', $cookieData[2]['domain']);
+ $this->assertFalse($cookieData[2]['secure']);
+ $this->assertFalse($cookieData[2]['httponly']);
+ }
+}
\ No newline at end of file
diff --git a/tests/DbTest.php b/tests/DbTest.php
new file mode 100644
index 0000000..3bd0c1e
--- /dev/null
+++ b/tests/DbTest.php
@@ -0,0 +1,49 @@
+shouldReceive('get')->with('database.cache_store', null)->andReturn(null);
+ $cache->shouldReceive('store')->with(null)->andReturn($store);
+
+ $db = Db::__make($event, $config, $log, $cache);
+
+ $config->shouldReceive('get')->with('database.foo', null)->andReturn('foo');
+ $this->assertEquals('foo', $db->getConfig('foo'));
+
+ $config->shouldReceive('get')->with('database', [])->andReturn([]);
+ $this->assertEquals([], $db->getConfig());
+
+ $callback = function () {
+ };
+ $event->shouldReceive('listen')->with('db.some', $callback);
+ $db->event('some', $callback);
+
+ $event->shouldReceive('trigger')->with('db.some', null, false);
+ $db->trigger('some');
+ }
+
+}
diff --git a/tests/DispatchTest.php b/tests/DispatchTest.php
new file mode 100644
index 0000000..06a22b9
--- /dev/null
+++ b/tests/DispatchTest.php
@@ -0,0 +1,60 @@
+prepareApp();
+
+ // Mock config for Cookie dependency
+ $this->config->shouldReceive('get')->with('cookie')->andReturn([
+ 'expire' => 0,
+ 'path' => '/',
+ 'domain' => '',
+ 'secure' => false,
+ 'httponly' => false,
+ 'samesite' => ''
+ ]);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ }
+
+ public function testPsr7Response()
+ {
+ $request = Mockery::mock(Request::class);
+ $rule = Mockery::mock(Rule::class);
+ $dispatch = new class($request, $rule, '') extends Dispatch {
+ public function exec()
+ {
+ return new Response(200, ['framework' => ['tp', 'thinkphp'], 'psr' => 'psr-7'], '123');
+ }
+ };
+
+ // Mock request methods that might be called
+ $request->shouldReceive('isJson')->andReturn(false);
+
+ $response = $dispatch->run();
+
+ $this->assertInstanceOf(\think\Response::class, $response);
+ $this->assertEquals('123', $response->getContent());
+ $this->assertEquals('tp, thinkphp', $response->getHeader('framework'));
+ $this->assertEquals('psr-7', $response->getHeader('psr'));
+ }
+}
diff --git a/tests/EnvTest.php b/tests/EnvTest.php
new file mode 100644
index 0000000..9b3b615
--- /dev/null
+++ b/tests/EnvTest.php
@@ -0,0 +1,80 @@
+setContent("key1=value1\nkey2=value2");
+ $root->addChild($envFile);
+
+ $env = new Env();
+
+ $env->load($envFile->url());
+
+ $this->assertEquals('value1', $env->get('key1'));
+ $this->assertEquals('value2', $env->get('key2'));
+ }
+
+ public function testServerEnv()
+ {
+ $env = new Env();
+
+ $this->assertEquals('value2', $env->get('key2', 'value2'));
+
+ putenv('PHP_KEY7=value7');
+ putenv('PHP_KEY8=false');
+ putenv('PHP_KEY9=true');
+
+ $this->assertEquals('value7', $env->get('key7'));
+ $this->assertFalse($env->get('KEY8'));
+ $this->assertTrue($env->get('key9'));
+ }
+
+ public function testSetEnv()
+ {
+ $env = new Env();
+
+ $env->set([
+ 'key1' => 'value1',
+ 'key2' => [
+ 'key1' => 'value1-2',
+ ],
+ ]);
+
+ $env->set('key3', 'value3');
+
+ $env->key4 = 'value4';
+
+ $env['key5'] = 'value5';
+
+ $this->assertEquals('value1', $env->get('key1'));
+ $this->assertEquals('value1-2', $env->get('key2.key1'));
+
+ $this->assertEquals('value3', $env->get('key3'));
+
+ $this->assertEquals('value4', $env->key4);
+
+ $this->assertEquals('value5', $env['key5']);
+
+ $this->expectException(Exception::class);
+
+ unset($env['key5']);
+ }
+
+ public function testHasEnv()
+ {
+ $env = new Env();
+ $env->set(['foo' => 'bar']);
+ $this->assertTrue($env->has('foo'));
+ $this->assertTrue(isset($env->foo));
+ $this->assertTrue($env->offsetExists('foo'));
+ }
+}
diff --git a/tests/EventTest.php b/tests/EventTest.php
new file mode 100644
index 0000000..797c4d6
--- /dev/null
+++ b/tests/EventTest.php
@@ -0,0 +1,134 @@
+app = m::mock(App::class)->makePartial();
+
+ Container::setInstance($this->app);
+ $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
+ $this->config = m::mock(Config::class)->makePartial();
+ $this->app->shouldReceive('get')->with('config')->andReturn($this->config);
+
+ $this->event = new Event($this->app);
+ }
+
+ public function testBasic()
+ {
+ $this->event->bind(['foo' => 'baz']);
+
+ $this->event->listen('foo', function ($bar) {
+ $this->assertEquals('bar', $bar);
+ });
+
+ $this->assertTrue($this->event->hasListener('foo'));
+
+ $this->event->trigger('baz', 'bar');
+
+ $this->event->remove('foo');
+
+ $this->assertFalse($this->event->hasListener('foo'));
+ }
+
+ public function testOnceEvent()
+ {
+ $this->event->listen('AppInit', function ($bar) {
+ $this->assertEquals('bar', $bar);
+ return 'foo';
+ });
+
+ $this->assertEquals('foo', $this->event->trigger('AppInit', 'bar', true));
+ $this->assertEquals(['foo'], $this->event->trigger('AppInit', 'bar'));
+ }
+
+ public function testClassListener()
+ {
+ $listener = m::mock("overload:SomeListener", TestListener::class);
+
+ $listener->shouldReceive('handle')->andReturnTrue();
+
+ $this->event->listen('some', "SomeListener");
+
+ $this->assertTrue($this->event->until('some'));
+ }
+
+ public function testSubscribe()
+ {
+ $listener = m::mock("overload:SomeListener", TestListener::class);
+
+ $listener->shouldReceive('subscribe')->andReturnUsing(function (Event $event) use ($listener) {
+
+ $listener->shouldReceive('onBar')->once()->andReturnFalse();
+
+ $event->listenEvents(['SomeListener::onBar' => [[$listener, 'onBar']]]);
+ });
+
+ $this->event->subscribe('SomeListener');
+
+ $this->assertTrue($this->event->hasListener('SomeListener::onBar'));
+
+ $this->event->trigger('SomeListener::onBar');
+ }
+
+ public function testAutoObserve()
+ {
+ $listener = m::mock("overload:SomeListener", TestListener::class);
+
+ $listener->shouldReceive('onBar')->once();
+
+ $this->app->shouldReceive('make')->with('SomeListener')->andReturn($listener);
+
+ $this->event->observe('SomeListener');
+
+ $this->event->trigger('bar');
+ }
+
+}
+
+class TestListener
+{
+ public function handle()
+ {
+
+ }
+
+ public function onBar()
+ {
+
+ }
+
+ public function onFoo()
+ {
+
+ }
+
+ public function subscribe()
+ {
+
+ }
+}
diff --git a/tests/HttpTest.php b/tests/HttpTest.php
new file mode 100644
index 0000000..49b509d
--- /dev/null
+++ b/tests/HttpTest.php
@@ -0,0 +1,153 @@
+app = m::mock(App::class)->makePartial();
+
+ $this->http = m::mock(Http::class, [$this->app])->shouldAllowMockingProtectedMethods()->makePartial();
+ }
+
+ protected function prepareApp($request, $response)
+ {
+ $this->app->shouldReceive('instance')->once()->with('request', $request);
+ $this->app->shouldReceive('initialized')->once()->andReturnFalse();
+ $this->app->shouldReceive('initialize')->once();
+ $this->app->shouldReceive('get')->with('request')->andReturn($request);
+
+ $route = m::mock(Route::class);
+
+ $route->shouldReceive('dispatch')->withArgs(function ($req, $withRoute) use ($request) {
+ if ($withRoute) {
+ $withRoute();
+ }
+ return $req === $request;
+ })->andReturn($response);
+
+ $this->app->shouldReceive('get')->with('route')->andReturn($route);
+
+ $console = m::mock(Console::class);
+
+ $console->shouldReceive('call');
+
+ $this->app->shouldReceive('get')->with('console')->andReturn($console);
+ }
+
+ public function testRun()
+ {
+ $root = vfsStream::setup('rootDir', null, [
+ 'app' => [
+ 'controller' => [],
+ 'middleware.php' => ' [
+ 'route.php' => 'app->shouldReceive('getBasePath')->andReturn($root->getChild('app')->url() . DIRECTORY_SEPARATOR);
+ $this->app->shouldReceive('getRootPath')->andReturn($root->url() . DIRECTORY_SEPARATOR);
+
+ $request = m::mock(Request::class)->makePartial();
+ $response = m::mock(Response::class)->makePartial();
+
+ $this->prepareApp($request, $response);
+
+ $this->assertEquals($response, $this->http->run($request));
+ }
+
+ public function multiAppRunProvider()
+ {
+ $request1 = m::mock(Request::class)->makePartial();
+ $request1->shouldReceive('subDomain')->andReturn('www');
+ $request1->shouldReceive('host')->andReturn('www.domain.com');
+
+ $request2 = m::mock(Request::class)->makePartial();
+ $request2->shouldReceive('subDomain')->andReturn('app2');
+ $request2->shouldReceive('host')->andReturn('app2.domain.com');
+
+ $request3 = m::mock(Request::class)->makePartial();
+ $request3->shouldReceive('pathinfo')->andReturn('some1/a/b/c');
+
+ $request4 = m::mock(Request::class)->makePartial();
+ $request4->shouldReceive('pathinfo')->andReturn('app3/a/b/c');
+
+ $request5 = m::mock(Request::class)->makePartial();
+ $request5->shouldReceive('pathinfo')->andReturn('some2/a/b/c');
+
+ return [
+ [$request1, true, 'app1'],
+ [$request2, true, 'app2'],
+ [$request3, true, 'app3'],
+ [$request4, true, null],
+ [$request5, true, 'some2', 'path'],
+ [$request1, false, 'some3'],
+ ];
+ }
+
+ public function testRunWithException()
+ {
+ $request = m::mock(Request::class);
+ $response = m::mock(Response::class);
+
+ $this->app->shouldReceive('instance')->once()->with('request', $request);
+ $this->app->shouldReceive('initialize')->once();
+
+ $exception = new Exception();
+
+ $this->http->shouldReceive('runWithRequest')->once()->with($request)->andThrow($exception);
+
+ $handle = m::mock(Handle::class);
+
+ $handle->shouldReceive('report')->once()->with($exception);
+ $handle->shouldReceive('render')->once()->with($request, $exception)->andReturn($response);
+
+ $this->app->shouldReceive('make')->with(Handle::class)->andReturn($handle);
+
+ $this->assertEquals($response, $this->http->run($request));
+ }
+
+ public function testEnd()
+ {
+ $response = m::mock(Response::class);
+ $event = m::mock(Event::class);
+ $event->shouldReceive('trigger')->once()->with(HttpEnd::class, $response);
+ $this->app->shouldReceive('get')->once()->with('event')->andReturn($event);
+ $log = m::mock(Log::class);
+ $log->shouldReceive('save')->once();
+ $this->app->shouldReceive('get')->once()->with('log')->andReturn($log);
+
+ $this->http->end($response);
+ }
+
+}
diff --git a/tests/InteractsWithApp.php b/tests/InteractsWithApp.php
new file mode 100644
index 0000000..f4fcf73
--- /dev/null
+++ b/tests/InteractsWithApp.php
@@ -0,0 +1,30 @@
+app = m::mock(App::class)->makePartial();
+ Container::setInstance($this->app);
+ $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
+ $this->app->shouldReceive('isDebug')->andReturnTrue();
+ $this->config = m::mock(Config::class)->makePartial();
+ $this->config->shouldReceive('get')->with('app.show_error_msg')->andReturnTrue();
+ $this->app->shouldReceive('get')->with('config')->andReturn($this->config);
+ $this->app->shouldReceive('runningInConsole')->andReturn(false);
+ }
+}
diff --git a/tests/LogTest.php b/tests/LogTest.php
new file mode 100644
index 0000000..4e9d250
--- /dev/null
+++ b/tests/LogTest.php
@@ -0,0 +1,127 @@
+prepareApp();
+
+ $this->log = new Log($this->app);
+ }
+
+ public function testGetConfig()
+ {
+ $config = [
+ 'default' => 'file',
+ ];
+
+ $this->config->shouldReceive('get')->with('log')->andReturn($config);
+
+ $this->assertEquals($config, $this->log->getConfig());
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->log->getChannelConfig('foo');
+ }
+
+ public function testChannel()
+ {
+ $this->assertInstanceOf(ChannelSet::class, $this->log->channel(['file', 'mail']));
+ }
+
+ public function testLogManagerInstances()
+ {
+ $this->config->shouldReceive('get')->with("log.channels.single", null)->andReturn(['type' => 'file']);
+
+ $channel1 = $this->log->channel('single');
+ $channel2 = $this->log->channel('single');
+
+ $this->assertSame($channel1, $channel2);
+ }
+
+ public function testFileLog()
+ {
+ $root = vfsStream::setup();
+
+ $this->config->shouldReceive('get')->with("log.default", null)->andReturn('file');
+
+ $this->config->shouldReceive('get')->with("log.channels.file", null)
+ ->andReturn(['type' => 'file', 'path' => $root->url()]);
+
+ $this->log->info('foo');
+
+ $this->assertEquals([['info', 'foo']], array_map(fn($log) => [$log->type, $log->message], $this->log->getLog()));
+
+ $this->log->clear();
+
+ $this->assertEmpty($this->log->getLog());
+
+ $this->log->error('foo');
+ $this->log->emergency('foo');
+ $this->log->alert('foo');
+ $this->log->critical('foo');
+ $this->log->warning('foo');
+ $this->log->notice('foo');
+ $this->log->debug('foo');
+ $this->log->sql('foo');
+ $this->log->custom('foo');
+
+ $this->assertEquals([
+ ['error', 'foo'],
+ ['emergency', 'foo'],
+ ['alert', 'foo'],
+ ['critical', 'foo'],
+ ['warning', 'foo'],
+ ['notice', 'foo'],
+ ['debug', 'foo'],
+ ['sql', 'foo'],
+ ['custom', 'foo'],
+ ], array_map(fn($log) => [$log->type, $log->message], $this->log->getLog()));
+
+ $this->log->write('foo');
+ $this->assertTrue($root->hasChildren());
+ $this->assertEmpty($this->log->getLog());
+
+ $this->log->close();
+
+ $this->log->info('foo');
+
+ $this->assertEmpty($this->log->getLog());
+ }
+
+ public function testSave()
+ {
+ $root = vfsStream::setup();
+
+ $this->config->shouldReceive('get')->with("log.default", null)->andReturn('file');
+
+ $this->config->shouldReceive('get')->with("log.channels.file", null)
+ ->andReturn(['type' => 'file', 'path' => $root->url()]);
+
+ $this->log->info('foo');
+
+ $this->log->save();
+
+ $this->assertTrue($root->hasChildren());
+ }
+
+}
diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php
new file mode 100644
index 0000000..e57aecf
--- /dev/null
+++ b/tests/MiddlewareTest.php
@@ -0,0 +1,110 @@
+prepareApp();
+
+ $this->middleware = new Middleware($this->app);
+ }
+
+ public function testSetMiddleware()
+ {
+ $this->middleware->add('BarMiddleware', 'bar');
+
+ $this->assertEquals(1, count($this->middleware->all('bar')));
+
+ $this->middleware->controller('BarMiddleware');
+ $this->assertEquals(1, count($this->middleware->all('controller')));
+
+ $this->middleware->import(['FooMiddleware']);
+ $this->assertEquals(1, count($this->middleware->all()));
+
+ $this->middleware->unshift(['BazMiddleware', 'baz']);
+ $this->assertEquals(2, count($this->middleware->all()));
+ $this->assertEquals([['BazMiddleware', 'handle'], 'baz'], $this->middleware->all()[0]);
+
+ $this->config->shouldReceive('get')->with('middleware.alias', [])
+ ->andReturn(['foo' => ['FooMiddleware', 'FarMiddleware']]);
+
+ $this->middleware->add('foo');
+ $this->assertEquals(3, count($this->middleware->all()));
+ $this->middleware->add(function () {
+ });
+ $this->middleware->add(function () {
+ });
+ $this->assertEquals(5, count($this->middleware->all()));
+ }
+
+ public function testPipelineAndEnd()
+ {
+ $bar = m::mock("overload:BarMiddleware");
+ $foo = m::mock("overload:FooMiddleware", Foo::class);
+
+ $request = m::mock(Request::class);
+ $response = m::mock(Response::class);
+
+ $e = new Exception();
+
+ $handle = m::mock(Handle::class);
+ $handle->shouldReceive('report')->with($e)->andReturnNull();
+ $handle->shouldReceive('render')->with($request, $e)->andReturn($response);
+
+ $foo->shouldReceive('handle')->once()->andReturnUsing(function ($request, $next) {
+ return $next($request);
+ });
+ $bar->shouldReceive('handle')->once()->andReturnUsing(function ($request, $next) use ($e) {
+ $next($request);
+ throw $e;
+ });
+
+ $foo->shouldReceive('end')->once()->with($response)->andReturnNull();
+
+ $this->app->shouldReceive('make')->with(Handle::class)->andReturn($handle);
+
+ $this->config->shouldReceive('get')->once()->with('middleware.priority', [])
+ ->andReturn(['FooMiddleware', 'BarMiddleware']);
+
+ $this->middleware->import([function ($request, $next) {
+ return $next($request);
+ }, 'BarMiddleware', 'FooMiddleware']);
+
+ $this->assertInstanceOf(Pipeline::class, $pipeline = $this->middleware->pipeline());
+
+ $pipeline->send($request)->then(function ($request) use ($e, $response) {
+ throw $e;
+ });
+
+ $this->middleware->end($response);
+ }
+}
+
+class Foo
+{
+ public function end(Response $response)
+ {
+ }
+}
diff --git a/tests/PipelineTest.php b/tests/PipelineTest.php
new file mode 100644
index 0000000..90f26d3
--- /dev/null
+++ b/tests/PipelineTest.php
@@ -0,0 +1,352 @@
+pipeline = new Pipeline();
+ }
+
+ public function testSend()
+ {
+ $data = 'test data';
+ $result = $this->pipeline->send($data);
+
+ $this->assertSame($this->pipeline, $result);
+ }
+
+ public function testThroughWithArray()
+ {
+ $pipes = [
+ function ($passable, $next) {
+ return $next($passable . ' pipe1');
+ },
+ function ($passable, $next) {
+ return $next($passable . ' pipe2');
+ }
+ ];
+
+ $result = $this->pipeline->through($pipes);
+
+ $this->assertSame($this->pipeline, $result);
+ }
+
+ public function testThroughWithArguments()
+ {
+ $pipe1 = function ($passable, $next) {
+ return $next($passable . ' pipe1');
+ };
+ $pipe2 = function ($passable, $next) {
+ return $next($passable . ' pipe2');
+ };
+
+ $result = $this->pipeline->through($pipe1, $pipe2);
+
+ $this->assertSame($this->pipeline, $result);
+ }
+
+ public function testThenExecutesPipeline()
+ {
+ $pipes = [
+ function ($passable, $next) {
+ return $next($passable . ' pipe1');
+ },
+ function ($passable, $next) {
+ return $next($passable . ' pipe2');
+ }
+ ];
+
+ $destination = function ($passable) {
+ return $passable . ' destination';
+ };
+
+ $result = $this->pipeline
+ ->send('start')
+ ->through($pipes)
+ ->then($destination);
+
+ $this->assertEquals('start pipe1 pipe2 destination', $result);
+ }
+
+ public function testPipelineExecutesInCorrectOrder()
+ {
+ $order = [];
+
+ $pipes = [
+ function ($passable, $next) use (&$order) {
+ $order[] = 'pipe1_before';
+ $result = $next($passable);
+ $order[] = 'pipe1_after';
+ return $result;
+ },
+ function ($passable, $next) use (&$order) {
+ $order[] = 'pipe2_before';
+ $result = $next($passable);
+ $order[] = 'pipe2_after';
+ return $result;
+ }
+ ];
+
+ $destination = function ($passable) use (&$order) {
+ $order[] = 'destination';
+ return $passable;
+ };
+
+ $this->pipeline
+ ->send('test')
+ ->through($pipes)
+ ->then($destination);
+
+ $expected = ['pipe1_before', 'pipe2_before', 'destination', 'pipe2_after', 'pipe1_after'];
+ $this->assertEquals($expected, $order);
+ }
+
+ public function testEmptyPipelineExecutesDestination()
+ {
+ $destination = function ($passable) {
+ return $passable . ' processed';
+ };
+
+ $result = $this->pipeline
+ ->send('test')
+ ->through([])
+ ->then($destination);
+
+ $this->assertEquals('test processed', $result);
+ }
+
+ public function testPipelineCanModifyData()
+ {
+ $pipes = [
+ function ($passable, $next) {
+ $passable['pipe1'] = true;
+ return $next($passable);
+ },
+ function ($passable, $next) {
+ $passable['pipe2'] = true;
+ return $next($passable);
+ }
+ ];
+
+ $destination = function ($passable) {
+ $passable['destination'] = true;
+ return $passable;
+ };
+
+ $result = $this->pipeline
+ ->send([])
+ ->through($pipes)
+ ->then($destination);
+
+ $expected = [
+ 'pipe1' => true,
+ 'pipe2' => true,
+ 'destination' => true
+ ];
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testPipelineCanShortCircuit()
+ {
+ $pipes = [
+ function ($passable, $next) {
+ if ($passable === 'stop') {
+ return 'stopped';
+ }
+ return $next($passable);
+ },
+ function ($passable, $next) {
+ return $next($passable . ' pipe2');
+ }
+ ];
+
+ $destination = function ($passable) {
+ return $passable . ' destination';
+ };
+
+ $result = $this->pipeline
+ ->send('stop')
+ ->through($pipes)
+ ->then($destination);
+
+ $this->assertEquals('stopped', $result);
+ }
+
+ public function testExceptionInDestinationIsHandled()
+ {
+ $pipes = [
+ function ($passable, $next) {
+ return $next($passable);
+ }
+ ];
+
+ $destination = function ($passable) {
+ throw new Exception('Destination error');
+ };
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Destination error');
+
+ $this->pipeline
+ ->send('test')
+ ->through($pipes)
+ ->then($destination);
+ }
+
+ public function testExceptionInPipeIsHandled()
+ {
+ $pipes = [
+ function ($passable, $next) {
+ throw new Exception('Pipe error');
+ }
+ ];
+
+ $destination = function ($passable) {
+ return $passable;
+ };
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Pipe error');
+
+ $this->pipeline
+ ->send('test')
+ ->through($pipes)
+ ->then($destination);
+ }
+
+ public function testWhenExceptionSetsHandler()
+ {
+ $result = $this->pipeline->whenException(function ($passable, $e) {
+ return 'handled';
+ });
+
+ $this->assertSame($this->pipeline, $result);
+ }
+
+ public function testExceptionHandlerIsCalledOnException()
+ {
+ $pipes = [
+ function ($passable, $next) {
+ throw new Exception('Test exception');
+ }
+ ];
+
+ $destination = function ($passable) {
+ return $passable;
+ };
+
+ $result = $this->pipeline
+ ->send('test')
+ ->through($pipes)
+ ->whenException(function ($passable, $e) {
+ return 'handled: ' . $e->getMessage();
+ })
+ ->then($destination);
+
+ $this->assertEquals('handled: Test exception', $result);
+ }
+
+ public function testExceptionHandlerReceivesCorrectParameters()
+ {
+ $pipes = [
+ function ($passable, $next) {
+ throw new Exception('Test exception');
+ }
+ ];
+
+ $destination = function ($passable) {
+ return $passable;
+ };
+
+ $handlerCalled = false;
+ $receivedPassable = null;
+ $receivedException = null;
+
+ $this->pipeline
+ ->send('original data')
+ ->through($pipes)
+ ->whenException(function ($passable, $e) use (&$handlerCalled, &$receivedPassable, &$receivedException) {
+ $handlerCalled = true;
+ $receivedPassable = $passable;
+ $receivedException = $e;
+ return 'handled';
+ })
+ ->then($destination);
+
+ $this->assertTrue($handlerCalled);
+ $this->assertEquals('original data', $receivedPassable);
+ $this->assertInstanceOf(Exception::class, $receivedException);
+ $this->assertEquals('Test exception', $receivedException->getMessage());
+ }
+
+ public function testExceptionInDestinationWithHandler()
+ {
+ $destination = function ($passable) {
+ throw new Exception('Destination exception');
+ };
+
+ $result = $this->pipeline
+ ->send('test')
+ ->through([])
+ ->whenException(function ($passable, $e) {
+ return 'destination handled: ' . $e->getMessage();
+ })
+ ->then($destination);
+
+ $this->assertEquals('destination handled: Destination exception', $result);
+ }
+
+ public function testComplexPipelineWithExceptions()
+ {
+ $pipes = [
+ function ($passable, $next) {
+ $passable[] = 'pipe1';
+ return $next($passable);
+ },
+ function ($passable, $next) {
+ if (in_array('error', $passable)) {
+ throw new Exception('Pipe2 error');
+ }
+ $passable[] = 'pipe2';
+ return $next($passable);
+ },
+ function ($passable, $next) {
+ $passable[] = 'pipe3';
+ return $next($passable);
+ }
+ ];
+
+ $destination = function ($passable) {
+ $passable[] = 'destination';
+ return $passable;
+ };
+
+ // Normal execution
+ $result1 = $this->pipeline
+ ->send(['start'])
+ ->through($pipes)
+ ->then($destination);
+
+ $this->assertEquals(['start', 'pipe1', 'pipe2', 'pipe3', 'destination'], $result1);
+
+ // With exception handling
+ $result2 = $this->pipeline
+ ->send(['start', 'error'])
+ ->through($pipes)
+ ->whenException(function ($passable, $e) {
+ return ['error_handled', $e->getMessage()];
+ })
+ ->then($destination);
+
+ $this->assertEquals(['error_handled', 'Pipe2 error'], $result2);
+ }
+}
\ No newline at end of file
diff --git a/tests/RequestTest.php b/tests/RequestTest.php
new file mode 100644
index 0000000..3193f19
--- /dev/null
+++ b/tests/RequestTest.php
@@ -0,0 +1,569 @@
+prepareApp();
+ $this->request = new Request();
+ }
+
+ protected function tearDown(): void
+ {
+ m::close();
+ }
+
+ public function testConstructor()
+ {
+ $request = new Request();
+ $this->assertInstanceOf(Request::class, $request);
+ }
+
+ public function testMake()
+ {
+ $request = Request::__make($this->app);
+ $this->assertInstanceOf(Request::class, $request);
+ }
+
+ public function testGetMethod()
+ {
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $this->assertEquals('GET', $this->request->method());
+ $this->assertTrue($this->request->isGet());
+ $this->assertFalse($this->request->isPost());
+ }
+
+ public function testPostMethod()
+ {
+ $request = new Request();
+ $request->withServer(['REQUEST_METHOD' => 'POST']);
+ $this->assertEquals('POST', $request->method());
+ $this->assertTrue($request->isPost());
+ $this->assertFalse($request->isGet());
+ }
+
+ public function testPutMethod()
+ {
+ $request = new Request();
+ $request->withServer(['REQUEST_METHOD' => 'PUT']);
+ $this->assertEquals('PUT', $request->method());
+ $this->assertTrue($request->isPut());
+ $this->assertFalse($request->isGet());
+ }
+
+ public function testDeleteMethod()
+ {
+ $request = new Request();
+ $request->withServer(['REQUEST_METHOD' => 'DELETE']);
+ $this->assertEquals('DELETE', $request->method());
+ $this->assertTrue($request->isDelete());
+ $this->assertFalse($request->isGet());
+ }
+
+ public function testHeadMethod()
+ {
+ $request = new Request();
+ $request->withServer(['REQUEST_METHOD' => 'HEAD']);
+ $this->assertEquals('HEAD', $request->method());
+ $this->assertTrue($request->isHead());
+ $this->assertFalse($request->isGet());
+ }
+
+ public function testPatchMethod()
+ {
+ $request = new Request();
+ $request->withServer(['REQUEST_METHOD' => 'PATCH']);
+ $this->assertEquals('PATCH', $request->method());
+ $this->assertTrue($request->isPatch());
+ $this->assertFalse($request->isGet());
+ }
+
+ public function testOptionsMethod()
+ {
+ $request = new Request();
+ $request->withServer(['REQUEST_METHOD' => 'OPTIONS']);
+ $this->assertEquals('OPTIONS', $request->method());
+ $this->assertTrue($request->isOptions());
+ $this->assertFalse($request->isGet());
+ }
+
+ public function testGetParameter()
+ {
+ $request = new Request();
+ $request->withGet(['test' => 'value']);
+ $this->assertEquals('value', $request->get('test'));
+ $this->assertEquals('default', $request->get('missing', 'default'));
+ $this->assertEquals(['test' => 'value'], $request->get());
+ }
+
+ public function testPostParameter()
+ {
+ $request = new Request();
+ $request->withPost(['test' => 'value']);
+ $this->assertEquals('value', $request->post('test'));
+ $this->assertEquals('default', $request->post('missing', 'default'));
+ $this->assertEquals(['test' => 'value'], $request->post());
+ }
+
+ public function testParamMethod()
+ {
+ $request = new Request();
+ $request->withGet(['get_param' => 'get_value'])
+ ->withPost(['post_param' => 'post_value'])
+ ->withServer(['REQUEST_METHOD' => 'POST']);
+
+ $this->assertEquals('get_value', $request->param('get_param'));
+ $this->assertEquals('post_value', $request->param('post_param'));
+ $this->assertEquals('default', $request->param('missing', 'default'));
+ }
+
+ public function testHasMethod()
+ {
+ $request = new Request();
+ $request->withGet(['test' => 'value'])
+ ->withPost(['post_test' => 'post_value'])
+ ->withServer(['REQUEST_METHOD' => 'POST']);
+
+ $this->assertTrue($request->has('test'));
+ $this->assertTrue($request->has('post_test'));
+ $this->assertFalse($request->has('missing'));
+ }
+
+ public function testOnlyMethod()
+ {
+ $request = new Request();
+ $request->withGet(['param1' => 'value1', 'param2' => 'value2', 'param3' => 'value3']);
+
+ $result = $request->only(['param1', 'param3']);
+ $this->assertEquals(['param1' => 'value1', 'param3' => 'value3'], $result);
+ }
+
+ public function testExceptMethod()
+ {
+ $request = new Request();
+ $request->withGet(['param1' => 'value1', 'param2' => 'value2', 'param3' => 'value3']);
+
+ $result = $request->except(['param2']);
+ $this->assertEquals(['param1' => 'value1', 'param3' => 'value3'], $result);
+ }
+
+ public function testHeader()
+ {
+ $request = new Request();
+ $request->withHeader(['content-type' => 'application/json', 'authorization' => 'Bearer token123']);
+
+ $this->assertEquals('application/json', $request->header('content-type'));
+ $this->assertEquals('Bearer token123', $request->header('authorization'));
+ $this->assertEquals('default', $request->header('missing', 'default'));
+ }
+
+ public function testServer()
+ {
+ $request = new Request();
+ $request->withServer(['HTTP_HOST' => 'example.com', 'REQUEST_URI' => '/test']);
+
+ $this->assertEquals('example.com', $request->server('HTTP_HOST'));
+ $this->assertEquals('/test', $request->server('REQUEST_URI'));
+ $this->assertEquals('default', $request->server('missing', 'default'));
+ }
+
+ public function testCookie()
+ {
+ $request = new Request();
+ $request->withCookie(['test_cookie' => 'cookie_value']);
+
+ $this->assertEquals('cookie_value', $request->cookie('test_cookie'));
+ $this->assertEquals('default', $request->cookie('missing', 'default'));
+ }
+
+ public function testIsAjax()
+ {
+ $request = new Request();
+ $request->withServer(['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest']);
+ $this->assertTrue($request->isAjax());
+
+ $request2 = new Request();
+ $this->assertFalse($request2->isAjax());
+ }
+
+ public function testIsPjax()
+ {
+ $request = new Request();
+ $request->withServer(['HTTP_X_PJAX' => 'true']);
+ $this->assertTrue($request->isPjax());
+
+ $request2 = new Request();
+ $this->assertFalse($request2->isPjax());
+ }
+
+ public function testIsJson()
+ {
+ $request = new Request();
+ $request->withServer(['HTTP_ACCEPT' => 'application/json']);
+ $this->assertTrue($request->isJson());
+
+ $request2 = new Request();
+ $request2->withServer(['HTTP_ACCEPT' => 'text/html']);
+ $this->assertFalse($request2->isJson());
+ }
+
+ public function testIsSsl()
+ {
+ $request = new Request();
+ $request->withServer(['HTTPS' => 'on']);
+ $this->assertTrue($request->isSsl());
+
+ $request2 = new Request();
+ $request2->withServer(['HTTPS' => 'off']);
+ $this->assertFalse($request2->isSsl());
+
+ $request3 = new Request();
+ $request3->withServer(['REQUEST_SCHEME' => 'https']);
+ $this->assertTrue($request3->isSsl());
+ }
+
+ public function testScheme()
+ {
+ $request = new Request();
+ $request->withServer(['HTTPS' => 'on']);
+ $this->assertEquals('https', $request->scheme());
+
+ $request2 = new Request();
+ $request2->withServer(['HTTPS' => 'off']);
+ $this->assertEquals('http', $request2->scheme());
+ }
+
+ public function testHost()
+ {
+ $request = new Request();
+ $request->withServer(['HTTP_HOST' => 'example.com:8080']);
+ $this->assertEquals('example.com:8080', $request->host());
+
+ $request2 = new Request();
+ $request2->withServer(['HTTP_HOST' => 'sub.example.com']);
+ $this->assertEquals('sub.example.com', $request2->host());
+ }
+
+ public function testPort()
+ {
+ $request = new Request();
+ $request->withServer(['SERVER_PORT' => '8080']);
+ $this->assertEquals(8080, $request->port());
+
+ $request2 = new Request();
+ $request2->withServer(['SERVER_PORT' => '80']);
+ $this->assertEquals(80, $request2->port());
+ }
+
+ public function testDomain()
+ {
+ $request = new Request();
+ $request->withServer(['HTTP_HOST' => 'www.example.com', 'HTTPS' => 'on']);
+ $this->assertEquals('https://www.example.com', $request->domain());
+
+ $request2 = new Request();
+ $request2->withServer(['HTTP_HOST' => 'www.example.com', 'HTTPS' => 'off']);
+ $this->assertEquals('http://www.example.com', $request2->domain());
+ }
+
+ public function testSubDomain()
+ {
+ $request = new Request();
+ $request->withServer(['HTTP_HOST' => 'sub.example.com']);
+ $this->assertEquals('sub', $request->subDomain());
+
+ $request2 = new Request();
+ $request2->withServer(['HTTP_HOST' => 'www.sub.example.com']);
+ $this->assertEquals('www.sub', $request2->subDomain());
+
+ $request3 = new Request();
+ $request3->withServer(['HTTP_HOST' => 'example.com']);
+ $this->assertEquals('', $request3->subDomain());
+ }
+
+ public function testRootDomain()
+ {
+ $request = new Request();
+ $request->withServer(['HTTP_HOST' => 'www.example.com']);
+ $this->assertEquals('example.com', $request->rootDomain());
+
+ $request2 = new Request();
+ $request2->withServer(['HTTP_HOST' => 'sub.example.co.uk']);
+ $this->assertEquals('example.co.uk', $request2->rootDomain());
+ }
+
+ public function testPathinfo()
+ {
+ $request = new Request();
+ $request->withServer(['PATH_INFO' => '/user/profile']);
+ $this->assertEquals('user/profile', $request->pathinfo());
+
+ $request2 = new Request();
+ $request2->withServer(['REQUEST_URI' => '/app.php/user/profile?id=1', 'SCRIPT_NAME' => '/app.php']);
+ $this->assertEquals('app.php/user/profile', $request2->pathinfo());
+ }
+
+ public function testUrl()
+ {
+ $request = new Request();
+ $request->withServer(['HTTP_HOST' => 'example.com', 'REQUEST_URI' => '/path/to/resource?param=value']);
+
+ $result = $request->url();
+ $this->assertStringContainsString('/path/to/resource', $result);
+ }
+
+ public function testBaseUrl()
+ {
+ $request = new Request();
+ $request->withServer(['SCRIPT_NAME' => '/app/index.php', 'REQUEST_URI' => '/app/user/profile']);
+ $this->assertEquals('/app/user/profile', $request->baseUrl());
+ }
+
+ public function testRoot()
+ {
+ $request = new Request();
+ $request->withServer(['SCRIPT_NAME' => '/app/public/index.php']);
+ $this->assertEquals('', $request->root());
+ }
+
+ public function testQuery()
+ {
+ $request = new Request();
+ $request->withServer(['QUERY_STRING' => 'param1=value1¶m2=value2']);
+ $this->assertEquals('param1=value1¶m2=value2', $request->query());
+ }
+
+ public function testIp()
+ {
+ $request = new Request();
+ $request->withServer(['REMOTE_ADDR' => '192.168.1.100']);
+ $this->assertEquals('192.168.1.100', $request->ip());
+
+ // Test with proxy - need to configure proxy IPs first
+ $request2 = new Request();
+ $request2->withServer(['REMOTE_ADDR' => '192.168.1.100'])
+ ->withHeader(['x-forwarded-for' => '203.0.113.1, 192.168.1.100']);
+ // Without proper proxy configuration, it returns REMOTE_ADDR
+ $this->assertEquals('192.168.1.100', $request2->ip());
+ }
+
+ public function testIsValidIP()
+ {
+ $this->assertTrue($this->request->isValidIP('192.168.1.1'));
+ $this->assertTrue($this->request->isValidIP('2001:db8::1'));
+ $this->assertFalse($this->request->isValidIP('invalid.ip'));
+ $this->assertFalse($this->request->isValidIP('999.999.999.999'));
+ }
+
+ public function testTime()
+ {
+ $request = new Request();
+ $request->withServer(['REQUEST_TIME_FLOAT' => 1234567890.123]);
+ $this->assertEquals(1234567890.123, $request->time(true));
+
+ $request2 = new Request();
+ $request2->withServer(['REQUEST_TIME' => 1234567890]);
+ $this->assertEquals(1234567890, $request2->time());
+ }
+
+ public function testType()
+ {
+ $request = new Request();
+ $request->withHeader(['content-type' => 'application/json']);
+ // Type method may return empty if not configured properly
+ $type = $request->type();
+ $this->assertIsString($type);
+
+ $request2 = new Request();
+ $request2->withHeader(['content-type' => 'text/html; charset=utf-8']);
+ $type2 = $request2->type();
+ $this->assertIsString($type2);
+ }
+
+ public function testContentType()
+ {
+ $request = new Request();
+ $request->withHeader(['content-type' => 'application/json; charset=utf-8']);
+ $this->assertEquals('application/json', $request->contentType());
+ }
+
+ public function testArrayAccess()
+ {
+ $request = new Request();
+ $request->withGet(['test' => 'value']);
+
+ $this->assertTrue(isset($request['test']));
+ $this->assertEquals('value', $request['test']);
+
+ // offsetSet is empty in Request class, so setting values has no effect
+ $request['new'] = 'new_value';
+ $this->assertNull($request['new']);
+
+ // offsetUnset is also empty, so unset has no effect
+ unset($request['test']);
+ $this->assertTrue(isset($request['test']));
+ }
+
+ public function testWithMethods()
+ {
+ $request = new Request();
+
+ $newRequest = $request->withGet(['key' => 'value']);
+ $this->assertEquals('value', $newRequest->get('key'));
+
+ $newRequest = $request->withPost(['post_key' => 'post_value']);
+ $this->assertEquals('post_value', $newRequest->post('post_key'));
+
+ $newRequest = $request->withHeader(['Content-Type' => 'application/json']);
+ $this->assertEquals('application/json', $newRequest->header('content-type'));
+
+ $newRequest = $request->withServer(['HTTP_HOST' => 'test.com']);
+ $this->assertEquals('test.com', $newRequest->server('HTTP_HOST'));
+
+ $newRequest = $request->withCookie(['session' => 'abc123']);
+ $this->assertEquals('abc123', $newRequest->cookie('session'));
+ }
+
+ public function testFilter()
+ {
+ $request = new Request();
+ $request->withGet(['email' => ' test@example.com ', 'number' => '123.45']);
+
+ $this->assertEquals('test@example.com', $request->get('email', '', 'trim'));
+ $this->assertEquals(123, $request->get('number', 0, 'intval'));
+ $this->assertEquals(123.45, $request->get('number', 0, 'floatval'));
+ }
+
+ public function testParam()
+ {
+ $request = new Request();
+ $request->withGet(['test' => 'get_value'])
+ ->withPost(['test' => 'post_value']);
+
+ // Test basic param access
+ $this->assertEquals('get_value', $request->param('test'));
+ }
+
+ public function testRoute()
+ {
+ $request = new Request();
+ $request->setRoute(['controller' => 'User', 'action' => 'profile']);
+
+ $this->assertEquals('User', $request->route('controller'));
+ $this->assertEquals('profile', $request->route('action'));
+ $this->assertEquals(['controller' => 'User', 'action' => 'profile'], $request->route());
+ }
+
+ public function testControllerAndAction()
+ {
+ $request = new Request();
+ $request->setController('User');
+ $request->setAction('profile');
+
+ $this->assertEquals('User', $request->controller());
+ $this->assertEquals('profile', $request->action());
+ }
+
+ public function testMiddlewareProperty()
+ {
+ $request = new Request();
+ $request->withMiddleware(['auth', 'throttle']);
+
+ $this->assertEquals(['auth', 'throttle'], $request->middleware());
+ }
+
+ public function testSecureKey()
+ {
+ $key = $this->request->secureKey();
+ $this->assertIsString($key);
+ $this->assertGreaterThan(0, strlen($key));
+ }
+
+ public function testTokenGeneration()
+ {
+ // Test secure key generation
+ $key = $this->request->secureKey();
+ $this->assertIsString($key);
+ $this->assertGreaterThan(0, strlen($key));
+ }
+
+ public function testIsCli()
+ {
+ // In test context, this returns true
+ $this->assertTrue($this->request->isCli());
+ }
+
+ public function testIsCgi()
+ {
+ // isCgi() checks PHP_SAPI, not server variables
+ // In CLI test environment, this will return false
+ $request = new Request();
+ $this->assertFalse($request->isCgi());
+
+ // Test the method returns boolean
+ $this->assertIsBool($request->isCgi());
+ }
+
+ public function testProtocol()
+ {
+ $request = new Request();
+ $request->withServer(['SERVER_PROTOCOL' => 'HTTP/1.1']);
+ $this->assertEquals('HTTP/1.1', $request->protocol());
+
+ $request2 = new Request();
+ $request2->withServer(['SERVER_PROTOCOL' => 'HTTP/2.0']);
+ $this->assertEquals('HTTP/2.0', $request2->protocol());
+ }
+
+ public function testRemotePort()
+ {
+ $request = new Request();
+ $request->withServer(['REMOTE_PORT' => '12345']);
+ $this->assertEquals(12345, $request->remotePort());
+ }
+
+ public function testAll()
+ {
+ $request = new Request();
+ $request->withGet(['get_param' => 'get_value'])
+ ->withPost(['post_param' => 'post_value'])
+ ->withServer(['REQUEST_METHOD' => 'POST']);
+
+ $all = $request->all();
+ $this->assertIsArray($all);
+ $this->assertArrayHasKey('get_param', $all);
+ // POST params might not appear in all() depending on implementation
+ $this->assertEquals('get_value', $all['get_param']);
+ }
+
+ public function testExt()
+ {
+ $request = new Request();
+ $request->withServer(['PATH_INFO' => '/user/profile.json']);
+ $this->assertEquals('json', $request->ext());
+
+ $request2 = new Request();
+ $request2->withServer(['PATH_INFO' => '/user/profile.xml']);
+ $this->assertEquals('xml', $request2->ext());
+
+ $request3 = new Request();
+ $request3->withServer(['PATH_INFO' => '/user/profile']);
+ $this->assertEquals('', $request3->ext());
+ }
+}
\ No newline at end of file
diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php
new file mode 100644
index 0000000..24cbc83
--- /dev/null
+++ b/tests/ResponseTest.php
@@ -0,0 +1,113 @@
+assertInstanceOf(Html::class, $response);
+ $this->assertEquals('test content', $response->getData());
+ }
+
+ public function testJsonResponseCreation()
+ {
+ $cookie = m::mock(Cookie::class);
+ $data = ['key' => 'value'];
+ $response = new Json($cookie, $data);
+ $this->assertInstanceOf(Json::class, $response);
+ $this->assertEquals($data, $response->getData());
+ }
+
+ public function testResponseCode()
+ {
+ $cookie = m::mock(Cookie::class);
+ $response = new Html($cookie, 'test', 200);
+ $this->assertEquals(200, $response->getCode());
+
+ $response->code(404);
+ $this->assertEquals(404, $response->getCode());
+ }
+
+ public function testResponseHeaders()
+ {
+ $cookie = m::mock(Cookie::class);
+ $response = new Html($cookie, 'test');
+
+ $response->header(['Content-Type' => 'text/html']);
+ $headers = $response->getHeader();
+ $this->assertEquals('text/html', $headers['Content-Type']);
+
+ $response->header(['X-Custom' => 'value']);
+ $headers = $response->getHeader();
+ $this->assertEquals('value', $headers['X-Custom']);
+ }
+
+ public function testResponseData()
+ {
+ $cookie = m::mock(Cookie::class);
+ $response = new Html($cookie, 'initial');
+ $this->assertEquals('initial', $response->getData());
+
+ $response->data('updated');
+ $this->assertEquals('updated', $response->getData());
+ }
+
+ public function testResponseStatusMethods()
+ {
+ $cookie = m::mock(Cookie::class);
+ $response = new Html($cookie, '', 200);
+ $this->assertEquals(200, $response->getCode());
+
+ $response->code(404);
+ $this->assertEquals(404, $response->getCode());
+
+ $response->code(500);
+ $this->assertEquals(500, $response->getCode());
+ }
+
+ public function testContentTypeMethod()
+ {
+ $cookie = m::mock(Cookie::class);
+ $response = new Html($cookie, 'test');
+
+ $response->contentType('application/json', 'utf-8');
+ $headers = $response->getHeader();
+ $this->assertEquals('application/json; charset=utf-8', $headers['Content-Type']);
+ }
+
+ public function testLastModified()
+ {
+ $cookie = m::mock(Cookie::class);
+ $response = new Html($cookie, 'test');
+ $time = '2025-01-01 10:00:00';
+
+ $response->lastModified($time);
+ $headers = $response->getHeader();
+ $this->assertArrayHasKey('Last-Modified', $headers);
+ }
+
+ public function testETag()
+ {
+ $cookie = m::mock(Cookie::class);
+ $response = new Html($cookie, 'test');
+ $etag = 'test-etag';
+
+ $response->eTag($etag);
+ $headers = $response->getHeader();
+ $this->assertEquals('test-etag', $headers['ETag']);
+ }
+}
\ No newline at end of file
diff --git a/tests/RouteTest.php b/tests/RouteTest.php
new file mode 100644
index 0000000..fc8a3df
--- /dev/null
+++ b/tests/RouteTest.php
@@ -0,0 +1,369 @@
+prepareApp();
+
+ $this->config->shouldReceive('get')->with('route')->andReturn(['url_route_must' => true]);
+ $this->route = new Route($this->app);
+ }
+
+ /**
+ * @param $path
+ * @param string $method
+ * @param string $host
+ * @return m\Mock|Request
+ */
+ protected function makeRequest($path, $method = 'GET', $host = 'localhost')
+ {
+ $request = m::mock(Request::class)->makePartial();
+ $request->shouldReceive('host')->andReturn($host);
+ $request->shouldReceive('pathinfo')->andReturn($path);
+ $request->shouldReceive('url')->andReturn('/' . $path);
+ $request->shouldReceive('method')->andReturn(strtoupper($method));
+ return $request;
+ }
+
+ public function testSimpleRequest()
+ {
+ $this->route->get('foo', function () {
+ return 'get-foo';
+ });
+
+ $this->route->put('foo', function () {
+ return 'put-foo';
+ });
+
+ $this->route->group(function () {
+ $this->route->post('foo', function () {
+ return 'post-foo';
+ });
+ });
+
+ $request = $this->makeRequest('foo', 'post');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals(200, $response->getCode());
+ $this->assertEquals('post-foo', $response->getContent());
+
+ $request = $this->makeRequest('foo', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals(200, $response->getCode());
+ $this->assertEquals('get-foo', $response->getContent());
+ }
+
+ public function testGroup()
+ {
+ $this->route->group(function () {
+ $this->route->group('foo', function () {
+ $this->route->post('bar', function () {
+ return 'hello,world!';
+ });
+ });
+ });
+
+ $request = $this->makeRequest('foo/bar', 'post');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals(200, $response->getCode());
+ $this->assertEquals('hello,world!', $response->getContent());
+ }
+
+ public function testAllowCrossDomain()
+ {
+ $this->route->get('foo', function () {
+ return 'get-foo';
+ })->allowCrossDomain(['some' => 'bar']);
+
+ $request = $this->makeRequest('foo', 'get');
+ $response = $this->route->dispatch($request);
+
+ $this->assertEquals('bar', $response->getHeader('some'));
+ $this->assertArrayHasKey('Access-Control-Allow-Credentials', $response->getHeader());
+
+ //$this->expectException(RouteNotFoundException::class);
+ $request = $this->makeRequest('foo2', 'options');
+ $this->route->dispatch($request);
+ }
+
+ public function testControllerDispatch()
+ {
+ $this->route->get('foo', 'foo/bar');
+
+ $controller = m::Mock(\stdClass::class);
+
+ $this->app->shouldReceive('parseClass')->with('controller', 'Foo')->andReturn($controller->mockery_getName());
+ $this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
+
+ $controller->shouldReceive('bar')->andReturn('bar');
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('bar', $response->getContent());
+ }
+
+ public function testEmptyControllerDispatch()
+ {
+ $this->route->get('foo', 'foo/bar');
+
+ $controller = m::Mock(\stdClass::class);
+
+ $this->app->shouldReceive('parseClass')->with('controller', 'Error')->andReturn($controller->mockery_getName());
+ $this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
+
+ $controller->shouldReceive('bar')->andReturn('bar');
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('bar', $response->getContent());
+ }
+
+ protected function createMiddleware($times = 1)
+ {
+ $middleware = m::mock(Str::random(5));
+ $middleware->shouldReceive('handle')->times($times)->andReturnUsing(function ($request, Closure $next) {
+ return $next($request);
+ });
+ $this->app->shouldReceive('make')->with($middleware->mockery_getName())->andReturn($middleware);
+
+ return $middleware;
+ }
+
+ public function testControllerWithMiddleware()
+ {
+ $this->route->get('foo', 'foo/bar');
+
+ $controller = m::mock(FooClass::class);
+
+ $controller->middleware = [
+ $this->createMiddleware()->mockery_getName() . ":params1:params2",
+ $this->createMiddleware(0)->mockery_getName() => ['except' => 'bar'],
+ $this->createMiddleware()->mockery_getName() => ['only' => 'bar'],
+ [
+ 'middleware' => [$this->createMiddleware()->mockery_getName(), [new \stdClass()]],
+ 'options' => ['only' => 'bar'],
+ ],
+ ];
+
+ $this->app->shouldReceive('parseClass')->with('controller', 'Foo')->andReturn($controller->mockery_getName());
+ $this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
+
+ $controller->shouldReceive('bar')->once()->andReturn('bar');
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('bar', $response->getContent());
+ }
+
+ public function testRedirectDispatch()
+ {
+ $this->route->redirect('foo', 'http://localhost', 302);
+
+ $request = $this->makeRequest('foo');
+ $this->app->shouldReceive('make')->with(Request::class)->andReturn($request);
+ $response = $this->route->dispatch($request);
+
+ $this->assertInstanceOf(Redirect::class, $response);
+ $this->assertEquals(302, $response->getCode());
+ $this->assertEquals('http://localhost', $response->getData());
+ }
+
+ public function testViewDispatch()
+ {
+ $this->route->view('foo', 'index/hello', ['city' => 'shanghai']);
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+
+ $this->assertInstanceOf(View::class, $response);
+ $this->assertEquals(['city' => 'shanghai'], $response->getVars());
+ $this->assertEquals('index/hello', $response->getData());
+ }
+
+ public function testDomainBindResponse()
+ {
+ $this->route->domain('test', function () {
+ $this->route->get('/', function () {
+ return 'Hello,ThinkPHP';
+ });
+ });
+
+ $request = $this->makeRequest('', 'get', 'test.domain.com');
+ $response = $this->route->dispatch($request);
+
+ $this->assertEquals('Hello,ThinkPHP', $response->getContent());
+ $this->assertEquals(200, $response->getCode());
+ }
+
+ public function testResourceRouting()
+ {
+ // Test basic resource registration (returns ResourceRegister when not lazy)
+ $resource = $this->route->resource('users', 'Users');
+ $this->assertTrue($resource instanceof \think\route\Resource || $resource instanceof \think\route\ResourceRegister);
+
+ // Test REST methods configuration
+ $restMethods = $this->route->getRest();
+ $this->assertIsArray($restMethods);
+ $this->assertArrayHasKey('index', $restMethods);
+ $this->assertArrayHasKey('create', $restMethods);
+ $this->assertArrayHasKey('save', $restMethods);
+ $this->assertArrayHasKey('read', $restMethods);
+ $this->assertArrayHasKey('edit', $restMethods);
+ $this->assertArrayHasKey('update', $restMethods);
+ $this->assertArrayHasKey('delete', $restMethods);
+
+ // Test custom REST method modification
+ $this->route->rest('custom', ['get', '/custom', 'customAction']);
+ $customMethod = $this->route->getRest('custom');
+ $this->assertEquals(['get', '/custom', 'customAction'], $customMethod);
+ }
+
+ public function testUrlGeneration()
+ {
+ $this->route->get('user/', 'User/detail')->name('user.detail');
+ $this->route->post('user', 'User/save')->name('user.save');
+
+ $urlBuild = $this->route->buildUrl('user.detail', ['id' => 123]);
+ $this->assertInstanceOf(\think\route\Url::class, $urlBuild);
+
+ $urlBuild = $this->route->buildUrl('user.save');
+ $this->assertInstanceOf(\think\route\Url::class, $urlBuild);
+ }
+
+ public function testRouteParameterBinding()
+ {
+ $this->route->get('user/', function ($id) {
+ return "User ID: $id";
+ });
+
+ $request = $this->makeRequest('user/123', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('User ID: 123', $response->getContent());
+
+ // Test multiple parameters
+ $this->route->get('post//', function ($year, $month) {
+ return "Year: $year, Month: $month";
+ });
+
+ $request = $this->makeRequest('post/2024/12', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('Year: 2024, Month: 12', $response->getContent());
+ }
+
+ public function testRoutePatternValidation()
+ {
+ $this->route->get('user/', function ($id) {
+ return "User ID: $id";
+ })->pattern(['id' => '\d+']);
+
+ // Valid numeric ID
+ $request = $this->makeRequest('user/123', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('User ID: 123', $response->getContent());
+
+ // Test pattern with name validation
+ $this->route->get('profile/', function ($name) {
+ return "Profile: $name";
+ })->pattern(['name' => '[a-zA-Z]+']);
+
+ $request = $this->makeRequest('profile/john', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('Profile: john', $response->getContent());
+ }
+
+ public function testMissRoute()
+ {
+ $this->route->get('home', function () {
+ return 'home page';
+ });
+
+ $this->route->miss(function () {
+ return 'Page not found';
+ });
+
+ // Test existing route
+ $request = $this->makeRequest('home', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('home page', $response->getContent());
+
+ // Test miss route
+ $request = $this->makeRequest('nonexistent', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('Page not found', $response->getContent());
+ }
+
+ public function testRouteMiddleware()
+ {
+ $middleware = $this->createMiddleware();
+
+ $this->route->get('protected', function () {
+ return 'protected content';
+ })->middleware($middleware->mockery_getName());
+
+ $request = $this->makeRequest('protected', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('protected content', $response->getContent());
+ }
+
+ public function testRouteOptions()
+ {
+ $this->route->get('api//users', function ($version) {
+ return "API Version: $version";
+ })->option(['version' => '1.0']);
+
+ $request = $this->makeRequest('api/v2/users', 'get');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('API Version: v2', $response->getContent());
+ }
+
+ public function testRouteCache()
+ {
+ // Test route configuration
+ $config = $this->route->config();
+ $this->assertIsArray($config);
+
+ $caseConfig = $this->route->config('url_case_sensitive');
+ $this->assertIsBool($caseConfig);
+
+ // Test route name management
+ $this->route->get('test', function () {
+ return 'test';
+ })->name('test.route');
+
+ $names = $this->route->getName('test.route');
+ $this->assertIsArray($names);
+ }
+
+}
+
+class FooClass
+{
+ public $middleware = [];
+
+ public function bar()
+ {
+
+ }
+}
diff --git a/tests/SessionTest.php b/tests/SessionTest.php
new file mode 100644
index 0000000..f9b4827
--- /dev/null
+++ b/tests/SessionTest.php
@@ -0,0 +1,225 @@
+app = m::mock(App::class)->makePartial();
+ Container::setInstance($this->app);
+
+ $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
+ $this->config = m::mock(Config::class)->makePartial();
+
+ $this->app->shouldReceive('get')->with('config')->andReturn($this->config);
+ $handlerClass = "\\think\\session\\driver\\Test" . Str::random(10);
+ $this->config->shouldReceive("get")->with("session.type", "file")->andReturn($handlerClass);
+ $this->session = new Session($this->app);
+
+ $this->handler = m::mock('overload:' . $handlerClass, SessionHandlerInterface::class);
+ }
+
+ public function testLoadData()
+ {
+ $data = [
+ "bar" => 'foo',
+ ];
+
+ $id = md5(uniqid());
+
+ $this->handler->shouldReceive("read")->once()->with($id)->andReturn(serialize($data));
+
+ $this->session->setId($id);
+ $this->session->init();
+
+ $this->assertEquals('foo', $this->session->get('bar'));
+ $this->assertTrue($this->session->has('bar'));
+ $this->assertFalse($this->session->has('foo'));
+
+ $this->session->set('foo', 'bar');
+ $this->assertTrue($this->session->has('foo'));
+
+ $this->assertEquals('bar', $this->session->pull('foo'));
+ $this->assertFalse($this->session->has('foo'));
+ }
+
+ public function testSave()
+ {
+
+ $id = md5(uniqid());
+
+ $this->handler->shouldReceive('read')->once()->with($id)->andReturn("");
+
+ $this->handler->shouldReceive('write')->once()->with($id, serialize([
+ "bar" => 'foo',
+ ]))->andReturnTrue();
+
+ $this->session->setId($id);
+ $this->session->init();
+
+ $this->session->set('bar', 'foo');
+
+ $this->session->save();
+ }
+
+ public function testFlash()
+ {
+ $this->session->flash('foo', 'bar');
+ $this->session->flash('bar', 0);
+ $this->session->flash('baz', true);
+
+ $this->assertTrue($this->session->has('foo'));
+ $this->assertEquals('bar', $this->session->get('foo'));
+ $this->assertEquals(0, $this->session->get('bar'));
+ $this->assertTrue($this->session->get('baz'));
+
+ $this->session->clearFlashData();
+
+ $this->assertTrue($this->session->has('foo'));
+ $this->assertEquals('bar', $this->session->get('foo'));
+ $this->assertEquals(0, $this->session->get('bar'));
+
+ $this->session->clearFlashData();
+
+ $this->assertFalse($this->session->has('foo'));
+ $this->assertNull($this->session->get('foo'));
+
+ $this->session->flash('foo', 'bar');
+ $this->assertTrue($this->session->has('foo'));
+ $this->session->clearFlashData();
+ $this->session->reflash();
+ $this->session->clearFlashData();
+
+ $this->assertTrue($this->session->has('foo'));
+ }
+
+ public function testClear()
+ {
+ $this->session->set('bar', 'foo');
+ $this->assertEquals('foo', $this->session->get('bar'));
+ $this->session->clear();
+ $this->assertFalse($this->session->has('foo'));
+ }
+
+ public function testSetName()
+ {
+ $this->session->setName('foo');
+ $this->assertEquals('foo', $this->session->getName());
+ }
+
+ public function testDestroy()
+ {
+ $id = md5(uniqid());
+
+ $this->handler->shouldReceive('read')->once()->with($id)->andReturn("");
+ $this->handler->shouldReceive('delete')->once()->with($id)->andReturnTrue();
+
+ $this->session->setId($id);
+ $this->session->init();
+
+ $this->session->set('bar', 'foo');
+
+ $this->session->destroy();
+
+ $this->assertFalse($this->session->has('bar'));
+
+ $this->assertNotEquals($id, $this->session->getId());
+ }
+
+ public function testFileHandler()
+ {
+ $root = vfsStream::setup();
+
+ vfsStream::newFile('bar')
+ ->at($root)
+ ->lastModified(time());
+
+ vfsStream::newFile('bar')
+ ->at(vfsStream::newDirectory("foo")->at($root))
+ ->lastModified(100);
+
+ $this->assertTrue($root->hasChild("bar"));
+ $this->assertTrue($root->hasChild("foo/bar"));
+
+ $handler = new TestFileHandle($this->app, [
+ 'path' => $root->url(),
+ 'gc_probability' => 1,
+ 'gc_divisor' => 1,
+ ]);
+
+ $this->assertTrue($root->hasChild("bar"));
+ $this->assertFalse($root->hasChild("foo/bar"));
+
+ $id = md5(uniqid());
+ $handler->write($id, "bar");
+
+ $this->assertTrue($root->hasChild("sess_{$id}"));
+
+ $this->assertEquals("bar", $handler->read($id));
+
+ $handler->delete($id);
+
+ $this->assertFalse($root->hasChild("sess_{$id}"));
+ }
+
+ public function testCacheHandler()
+ {
+ $id = md5(uniqid());
+
+ $cache = m::mock(\think\Cache::class);
+
+ $store = m::mock(Driver::class);
+
+ $cache->shouldReceive('store')->once()->with('redis')->andReturn($store);
+
+ $handler = new Cache($cache, ['store' => 'redis']);
+
+ $store->shouldReceive("set")->with($id, "bar", 1440)->once()->andReturnTrue();
+ $handler->write($id, "bar");
+
+ $store->shouldReceive("get")->with($id)->once()->andReturn("bar");
+ $this->assertEquals("bar", $handler->read($id));
+
+ $store->shouldReceive("delete")->with($id)->once()->andReturnTrue();
+ $handler->delete($id);
+ }
+}
+
+class TestFileHandle extends File
+{
+ protected function writeFile($path, $content): bool
+ {
+ return (bool) file_put_contents($path, $content);
+ }
+}
diff --git a/tests/UrlRouteTest.php b/tests/UrlRouteTest.php
new file mode 100644
index 0000000..28c502f
--- /dev/null
+++ b/tests/UrlRouteTest.php
@@ -0,0 +1,60 @@
+prepareApp();
+
+ $this->route = new Route($this->app);
+ }
+
+ public function testUrlDispatch()
+ {
+ $controller = m::mock(FooClass::class);
+ $controller->shouldReceive('index')->andReturn('bar');
+
+ $this->app->shouldReceive('parseClass')->once()->with('controller', 'Foo')
+ ->andReturn($controller->mockery_getName());
+ $this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
+
+ $request = $this->makeRequest('foo');
+ $response = $this->route->dispatch($request);
+ $this->assertEquals('bar', $response->getContent());
+ }
+
+ /**
+ * @param $path
+ * @param string $method
+ * @param string $host
+ * @return m\Mock|Request
+ */
+ protected function makeRequest($path, $method = 'GET', $host = 'localhost')
+ {
+ $request = m::mock(Request::class)->makePartial();
+ $request->shouldReceive('host')->andReturn($host);
+ $request->shouldReceive('pathinfo')->andReturn($path);
+ $request->shouldReceive('url')->andReturn('/' . $path);
+ $request->shouldReceive('method')->andReturn(strtoupper($method));
+ return $request;
+ }
+
+}
diff --git a/tests/ViewTest.php b/tests/ViewTest.php
new file mode 100644
index 0000000..9bd6dc4
--- /dev/null
+++ b/tests/ViewTest.php
@@ -0,0 +1,122 @@
+app = m::mock(App::class)->makePartial();
+ Container::setInstance($this->app);
+
+ $this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
+ $this->config = m::mock(Config::class)->makePartial();
+ $this->app->shouldReceive('get')->with('config')->andReturn($this->config);
+
+ $this->view = new View($this->app);
+ }
+
+ public function testAssignData()
+ {
+ $this->view->assign('foo', 'bar');
+ $this->view->assign(['baz' => 'boom']);
+ $this->view->qux = "corge";
+
+ $this->assertEquals('bar', $this->view->foo);
+ $this->assertEquals('boom', $this->view->baz);
+ $this->assertEquals('corge', $this->view->qux);
+ $this->assertTrue(isset($this->view->qux));
+ }
+
+ public function testRender()
+ {
+ $this->config->shouldReceive("get")->with("view.type", 'php')->andReturn(TestTemplate::class);
+
+ $this->view->filter(function ($content) {
+ return $content;
+ });
+
+ $this->assertEquals("fetch", $this->view->fetch('foo'));
+ $this->assertEquals("display", $this->view->display('foo'));
+ }
+
+}
+
+class TestTemplate implements TemplateHandlerInterface
+{
+
+ /**
+ * 检测是否存在模板文件
+ * @param string $template 模板文件或者模板规则
+ * @return bool
+ */
+ public function exists(string $template): bool
+ {
+ return true;
+ }
+
+ /**
+ * 渲染模板文件
+ * @param string $template 模板文件
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function fetch(string $template, array $data = []): void
+ {
+ echo "fetch";
+ }
+
+ /**
+ * 渲染模板内容
+ * @param string $content 模板内容
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function display(string $content, array $data = []): void
+ {
+ echo "display";
+ }
+
+ /**
+ * 配置模板引擎
+ * @param array $config 参数
+ * @return void
+ */
+ public function config(array $config): void
+ {
+ // TODO: Implement config() method.
+ }
+
+ /**
+ * 获取模板引擎配置
+ * @param string $name 参数名
+ * @return mixed
+ */
+ public function getConfig(string $name)
+ {
+ // TODO: Implement getConfig() method.
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..3459061
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,3 @@
+