{"id":36974729,"url":"https://github.com/muzk6/sparrow","last_synced_at":"2026-01-13T22:01:48.264Z","repository":{"id":53073328,"uuid":"164642305","full_name":"muzk6/sparrow","owner":"muzk6","description":"PHP框架 Sparrow","archived":false,"fork":false,"pushed_at":"2021-04-07T23:00:32.000Z","size":1886,"stargazers_count":5,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-11T04:09:42.794Z","etag":null,"topics":["framework","php"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/muzk6.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-01-08T12:17:02.000Z","updated_at":"2024-04-23T16:08:50.000Z","dependencies_parsed_at":"2022-08-23T13:50:53.224Z","dependency_job_id":null,"html_url":"https://github.com/muzk6/sparrow","commit_stats":null,"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"purl":"pkg:github/muzk6/sparrow","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/muzk6%2Fsparrow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/muzk6%2Fsparrow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/muzk6%2Fsparrow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/muzk6%2Fsparrow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/muzk6","download_url":"https://codeload.github.com/muzk6/sparrow/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/muzk6%2Fsparrow/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28401986,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","php"],"created_at":"2026-01-13T22:01:48.207Z","updated_at":"2026-01-13T22:01:48.259Z","avatar_url":"https://github.com/muzk6.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sparrow Framework\n\u003e [PHP框架 Sparrow](https://github.com/muzk6/sparrow)\n\n![](./public/app/img/logo.svg)\n\n## 安装\n\n### 创建项目\n\n`composer create-project --prefer-dist muzk6/sparrow your-projectname`\n\n*请确保项目目录 `data` 有**写**权限*\n\n### 使用 docker-compose 部署开发环境\n\n- `docker-compose up -d nginx php-fpm` 部署基础环境\n- 或者部署完整环境 `docker-compose up -d`，支持数据库、缓存、队列等服务\n\n*不建议生产环境使用 docker 部署*\n\n### 访问链接\n\n- http://localhost:37061/ 项目主页\n- http://localhost:37062/ 运维后台\n    - 登录密码位于 `app/Routes/OPS/index.php` 常量 `LOGIN_PASSWD`，可自行修改\n    - 详情查看后面章节 `OPS 运维与开发`\n- http://localhost:37063/ 业务后台\n\n*所有后台访问都有 IP, Cookie 白名单，配置位于 `config/.../whitelist.php`*\u003cbr\u003e\n\n![](https://raw.githubusercontent.com/muzk6/sparrow-res/master/img/home.png)\n\n## 目录结构\n\nDir | Desc\n--- | ---\napp | 业务逻辑\napp/Providers | 容器服务提供层\napp/Routes | 路由层(控制器层)\napp/Services | 业务服务层\ncli | 命令行脚本\nconfig | 配置文件，通用配置放在当前目录下\nconfig/dev | dev环境的配置\ncore | 框架文件\ndata | 缓存、日志数据目录，需要写权限\nlang | 国际化多语言\nprivate | 私有 Web 入口目录，配置的域名不应该被外网访问\nprivate/admin | Admin 后台入口\nprivate/rpc | RPC 入口目录\npublic | 公有 Web 入口目录\ntests | 单元测试\nvendor | Composer库\nviews | 视图文件\nworkers | worker 文件\n\n## 常用常量\n\nName | Desc\n--- | ---\nTIME | `$_SERVER['REQUEST_TIME']`, 脚本启动时间，不能在 worker 里使用，否则不会变化\nIS_POST | 是否为 POST 请求\nIS_GET | 是否为 GET 请求\nIS_DEV | 是否为开发环境\nAPP_LANG | 当前语言 `eg. zh_CN`\nAPP_ENV | 服务器环境 `eg. dev`\nTEST_ENV | 是否为单元测试的环境\nPATH_APP | 项目业务目录\nPATH_ROUTES | 路由目录\nPATH_PUBLIC | 网站入口路径\nPATH_DATA | 数据目录，需要有写权限\nPATH_LOG | 日志目录\n\n## 测试文件\n\n- `cli/test.php` 为 `php-cli` 测试脚本\n- `public/test.php` 为 `php-cgi` 测试入口\n\n## 路由\n\n### 注册路由\n\n- `route_get()` 注册回调 GET 请求\n    - `route_get_re()` 正则匹配\n- `route_post()` 注册回调 POST 请求\n    - `route_post_re()` 正则匹配 \n- `route_any()` 注册回调任何请求\n    - `route_any_re()` 正则匹配\n- `route_middleware()` 注册路由中间件，顺序执行，组内优先\n- `route_group()` 路由分组，隔离中间件\n\n### 用例\n\n```php\nroute_middleware(function () {\n    echo '中间件a1';\n});\n\nroute_middleware(function () {\n    echo '中间件a2';\n});\n\nroute_group(function () {\n    route_middleware(function () {\n        echo '中间件b1';\n    });\n\n    route_get('/', function () {\n        return 'Just Do It!';\n    });\n});\n\nroute_middleware(function () {\n    echo '中间件a3';\n});\n```\n\n输出如下：\n```\n中间件b1\n中间件a1\n中间件a2\nJust Do It!\n中间件a3\n```\n\n- 基本用法参考 `app/Routes/index.php`\n- 高级用法参考测试用例: `tests/feature/router_advanced.php`\n- 如果要实现 MVC 的 Controller::action 模式，可参考 `tests/feature/router_mvc.php`\n\n### 自定义404\n\n在调用 `app(Router::class)-\u003edispatch()` 之前像下面例子设置 404 回调：\n\n```php\napp(Router::class)-\u003esetStatus404Handler(function () {\n    return '自定义404页面'; // return view('...')\n});\n```\n\n## 请求参数\n\u003e 获取、过滤、表单验证、类型强转 请求参数 `$_GET,$_POST` 支持 `payload`\n\n- 以下的验证失败时会抛出异常 \\Core\\AppException\n\n### 不验证，一个一个获取\n\n```php\n$firstName = input('post.first_name');\n$lastName = input('last_name');\nvar_dump($firstName, $lastName);exit;\n```\n\n### 不验证，统一获取\n\n```php\ninput('post.first_name');\ninput('last_name');\n$request = request();\nvar_dump($request);exit;\n```\n\n### 部分验证，一个一个获取\n\n```php\n$firstName = input('post.first_name');\n$lastName = validate('last_name')-\u003erequired()-\u003eget('名字');\nvar_dump($firstName, $lastName);exit;\n```\n\n### 部分验证，统一获取\n\n```php\ninput('post.first_name');\nvalidate('last_name')-\u003erequired()-\u003esetTitle('名字');\n$request = request();\nvar_dump($request);exit;\n```\n\n### 串联短路方式验证（默认）\n\n遇到验证不通过时，立即终止后面的验证\n\n```php\nvalidate('post.first_name')-\u003erequired();\nvalidate('last_name')-\u003erequired()-\u003esetTitle('名字');\n$request = request(); // 以串联短路方式验证\n```\n\n*串联结果*\n```json\n{\n    \"s\": false,\n    \"c\": 10001000,\n    \"m\": \"参数错误\",\n    \"d\": {\n        \"first_name\": \"不能为空\"\n    }\n}\n```\n\n### 并联验证\n\n即使前面的验证不通过，也会继续验证后面的字段\n\n```php\nvalidate('post.first_name')-\u003erequired();\nvalidate('last_name')-\u003erequired()-\u003esetTitle('名字');\n$request = request(true); // 以并联方式验证\n```\n\n*并联结果*\n```json\n{\n    \"s\": false,\n    \"c\": 10001000,\n    \"m\": \"参数错误\",\n    \"d\": {\n        \"first_name\": \"不能为空\",\n        \"last_name\": \"名字不能为空\"\n    }\n}\n```\n\n### `input()` 参数说明\n\n`'get.foo:i'` 中的类型转换`i`为整型，其它类型为：\n\nName | Type\n--- | ---\ni | int\ns | string\nb | bool\na | array\nf | float\nd | double\n\n## PDO 数据库\n\n可以配置 MySQL, SQLite 等 PDO 支持的数据库\n\n- 配置文件 `config/.../mysql.php`\n- 用例参考 `tests/feature/db.php`\n\n如果想同时使用 SQLite 等数据库, 参考复制 `mysql.php` 为新的数据库配置文件，按需配置 dsn，再注册容器即可(参考 `\\Core\\ServiceProvider` 的 `PDOEngine`)\n\n## `helpers` 其它辅助函数用例\n\n#### `app()` 容器\n\n- `app(\\App\\Services\\DemoService::class)` 取 DemoService 单例对象，自动定义容器元素和依赖注入。如果需要手动定义，可以在 `app/Providers` 里定义\n- `app(\\App\\Services\\DemoService::class, $value)` 设置(或重置)容器里的元素，常用于单元测试 mock 对象\n\n#### `config()` 配置文件\n\n- `config('app.lang')`\n- 假设当前环境是`dev`\n- 依次搜索`config/dev/app.php, config/app.php`, 存在时返回第一个结果文件的内容，都不存在时返回`''`\n- `config(['app.lang' =\u003e 'en'])`设置 run-time 的配置\n\n添加新环境配置：\n复制目录 `config/dev` 及其配置文件，在 `config/env.php` 中添加多一个新环境分支\n\n#### `trans()` 多语言文本\n\n- `trans(10001000)`\n- 假设当前语言是`zh_CN`, 默认语言是`en`\n- 依次搜索`lang/zh_CN.php, lang/en.php`, 存在`10001000`这个`key`时返回第一个结果内容，都不存在时返回`?`\n\n#### `logfile()` 文件日志\n\n`logfile('test', ['foo', 'bar'], 'login')` 把内容写到`data/log/login_20190328.log`\n\n各日志文件说明：\n\n- `standard_xxx.log` PHP 标准错误处理程序写的日志，比较精简，但只能它才能记录 Fatal Error, Parse Error\n- `error_xxx.log` 框架写的错误日志，比较详细\n- `access_xxx.log` 框架的写访问日志\n- `app_xx.log` 用户写的默认日志，文件名可以修改，由 `logfile()` 参数3控制 \n\n#### `url()` 带协议和域名的完整URL\n\n- 当前域名URL：`url('path/to')`\n- 其它域名URL：`url(['test', '/path/to'])`\n\n#### `panic()` 直接抛出业务异常对象\n\n- `panic(10001000)` 等于 `throw new AppException('10001000')` 自动转为错误码对应的文本，参考翻译文件 lang/zh_CN.php\n- `panic('foo')` 等于 `throw new AppException('foo')`\n- `panic('foo', ['bar'])` 等于 `throw (new AppException('foo'))-\u003esetData(['bar'])`\n\n`AppException` 异常属于业务逻辑，能够作为提示通过接口返回给用户看，而其它异常则不会(安全考虑)\n\n#### `inject()` 支持自动依赖注入的函数调用\n\n通过回调函数的形参里声明类型，就能会自动注入\n\n```php\ninject(function (\\Core\\Queue $queue) {\n    //todo...\n});\n```\n\n#### `request_flash()`, `old()` 记住并使用上次的请求参数\n\n- `request_flash()` 把本次请求的参数缓存起来\n- `old(string $name = null, string $default = '')` 上次请求的字段值\n\n#### `csrf_*()` CSRF, XSRF\n\n- `csrf_field()`直接生成 HTML\n- `csrf_token()`生成 token\n- `csrf_check()`效验，token 来源于 `$_SERVER['HTTP_X_CSRF_TOKEN'], $_POST['_token'], $_GET['_token'], $_REQUEST['_token']`\n\n请求时带上 `Token`, 使用以下任意一种方法\n\n- `POST` 请求通过表单参数 `_token`, 后端将从 `$_POST['_token']` 读取\n- `GET` 请求通过 `?_token=`, 后端将从 `$_GET['_token']` 读取\n- 通过指定请求头 `X-CSRF-Token`, 后端将从 `$_SERVER['HTTP_X_CSRF_TOKEN']` 读取\n\n#### `flash_*()` 闪存，一性次缓存\n\n- `flash_set(string $key, $value)` 闪存设置\n- `flash_has(string $key)` 存在且为真\n- `flash_exists(string $key)` 闪存是否存在，即使值为 null\n- `flash_get(string $key)` 闪存获取并删除\n- `flash_del(string $key)` 闪存删除\n\n#### `api_format()`, `api_json()` 格式化为接口输出的内容结构\n\n- `api_format(true, ['foo' =\u003e 1])` 格式化为成功的内容结构 array\n- `api_format($exception)` 格式化异常对象为失败的内容结构 array\n- `api_json()`, `api_format()` 用法一样，区别是前者返回 string-json\n- `api_success()`, `api_error()` 是 `api_json()` 的简写\n\n#### 成功提示\n\n```json\n{\n    \"s\": true,\n    \"c\": 0,\n    \"m\": \"\",\n    \"d\": {\n        \"foo\": 1\n    }\n}\n```\n\n路由里等价写法如下：\n\n```php\nreturn ['foo' =\u003e 1]; // 只能返回消息体 d\nreturn api_success('', 0, ['foo' =\u003e 1]); // 一般用于方便返回纯 m, 例如 api_success('我是成功消息');\nreturn api_json(true, ['foo' =\u003e 1]);\n```\n\n#### 错误提示\n\n```json\n{\n    \"s\": false,\n    \"c\": 0,\n    \"m\": \"我是失败消息\",\n    \"d\": {\n        \"foo\": 1\n    }\n}\n```\n\n路由里等价写法如下：\n\n```php\npanic('我是失败消息', ['foo' =\u003e 1]); // 直接抛出异常，不用 return; 另一种便捷的用法是 panic(10001000);\nreturn api_error('我是失败消息', 0, ['foo' =\u003e 1]); // 可自由指定错误码\nreturn api_json(false, ['foo' =\u003e 1]);\n```\n\n#### `assign()`, `view()` 模板与变量\n\n- `assign('firstName', 'Hello')` 定义模板变量\n- `return view('demo', ['title' =\u003e $title])` 定义模板变量的同时返回渲染内容\n\n#### `back()`, `redirect()` 网页跳转\n\n- `return back()` 跳转回上一步\n- `return redirect('/demo')` 跳转到 `/demo`\n\n## 缓存 redis\n\n### 依赖\n\n`pecl install redis`\n\n### 用例\n\n`app(\\Core\\AppRedis::class)-\u003esetex('key', 3600, 'value')` 与原生一致\n\n## 登录\n\n```php\napp(\\Core\\Auth::class)-\u003elogin(1010); // 登录 ID 为 1010\napp(\\Core\\Auth::class)-\u003egetUserId(); // 1010\napp(\\Core\\Auth::class)-\u003eisLogin(); // true\napp(\\Core\\Auth::class)-\u003elogout(); // 退出登录\n```\n\n## RPC 远程过程调用\n\n- 服务端入口 `private/rpc/index.php`, 注意要使用内部域名，不能让外网访问\n- 客户端调用参考 `tests/feature/curl.php`\n\n## 消息队列\n\nworker 遇到信号 `SIGTERM`, `SIGHUP`, `SIGINT`, `SIGQUIT` 会平滑结束进程。\n如果要强行结束可使用信号 `SIGKILL`, 命令为 `kill -s KILL \u003cPID\u003e`\n\n### 依赖\n\n`composer require php-amqplib/php-amqplib`\n\n### 配置\n\n`config/.../rabbitmq.php`\n\n### 用例\n\n- `queue_publish('SPARROW_QUEUE_DEMO', ['foo' =\u003e 1, 'bar' =\u003e 2]);` 发布消息\n- 消费的 worker, 参考 `workers/SPARROW_QUEUE_DEMO.php`\n- docker 容器 php-fpm 里面已经有 supervisor, 使 worker 变为长驻进程\n    - 示例配置文件为 `docker/php-fpm/supervisor_conf.d/SPARROW_QUEUE_DEMO.conf`\n    - 日志可通过\"运维与开发\"后台查看，或者在 supervisorctl 里面使用 tail 命令查看\n    - 必须先启动 rabbitmq 服务，再启动 worker, 否则会报错。如果遇到 rabbitmq 容器比 php-fpm 容器先启动的情况，执行这个命令即可: `docker-compose exec php-fpm bash -c \"supervisorctl start all\"`\n\n建议规则：\n- 每个 worker 只消费一个队列；\n- 队列名与 worker名 一致，便于定位队列名对应的 worker 文件；\n- 队列名与 worker名 要有项目名前缀，防止在 Supervisor, RabbitMq 里与其它项目搞混\n\n## 邮件 email\n\n### 依赖\n\n`composer require swiftmailer/swiftmailer`\n\n### 配置\n\n`config/.../email.php`\n\n### 用例\n\n参考类文档 `\\Core\\Mail`\n\n## OPS 运维与开发\n\u003e 用于运维监控与开发调试，包括 日志、调试、性能分析\n\n![](https://raw.githubusercontent.com/muzk6/sparrow-res/master/img/ops.png)\n\n- 默认地址为 http://localhost:37062/\n- 可自行修改 nginx 配置：`docker/nginx/conf.d/ops.sparrow.conf`\n- 注意安全性，端口和域名不要对外开放\n\n### XDebug\n\u003e 断点调试\n\n配置文件位置 `docker/php-fpm/php_ini/xdebug.ini`\n\n### XDebug Trace\n\u003e 跟踪调试日志\n\n以下任意方式可开启跟踪，\n日志可在运维后台 `XDebug - 跟踪文件` 查看， \n或者直接在项目目录 `data/trace/` 里查看\n\n*注意：请确保对 `data/` 目录有写权限*\n\n#### 跟踪 fpm\n\n- 预先配置监听，两种方法设置:\n    - 在运维后台 `XDebug - 监听设置` \n    - 命令行 `php cli/trace.php`，参数 `--help` 查看帮助\n- 当前URL 主动开启: `/?_xt=name0`，`name0`是当前日志的标识名\n- Cookie 主动开启: `_xt=name0;`\n\n*注意：`URL`, `Cookie` 方式的前提必须先设置 `config/.../whitelist.php` 白名单 `IP` 或 白名单 `Cookie`*\n\n#### 跟踪 cli\n\n`php demo.php --trace` 在任何脚本命令后面加上参数 `--trace` 即可\n\n### XHProf\n\n#### 依赖\n\n- [扩展 tideways_xhprof](https://github.com/tideways/php-xhprof-extension/releases)\n- GUI - View Full Callgraph 功能，需要安装 `graphviz`\n    - Ubuntu: `sudo apt install graphviz`\n    - CentOS: `yum install graphviz`\n\n#### 使用\n\n- 配置文件 `config/.../xhprof.php`\n- `enable` 设置为 `true`, 即可记录大于指定耗时的请求\n\n*注意：请确保对 `data/` 目录有写权限*\n\n## 维护模式\n\u003e 开启维护模式，关闭网站访问入口\n\n- `php cli/maintain.php`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmuzk6%2Fsparrow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmuzk6%2Fsparrow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmuzk6%2Fsparrow/lists"}