PHP 框架 —— Just Do It
相关项目:
jdi-ops: JDI 框架的 OPS 运维后台
composer require muzk6/jdi
作为框架使用
index.php
require __DIR__ . '/vendor/autoload.php';
// 框架初始化
\JDI\App::init();
// 注册路由
route_get('/', function () {
return 'Just Do It!';
});
// 分发路由
svc_router()->dispatch();
开启服务:php -S 0.0.0.0:8080
没有用例!无需\JDI\App::init()
,可以直接作为库嵌入到现有项目使用
自定义配置:\JDI\App::init(['config.debug' => false]);
其它配置项参考下表:
配置项 | 默认值 | 描述 |
---|---|---|
config.debug | true | 调试开发模式,用于显示错误信息、关闭视图模板缓存、关闭 opcache |
config.path_data | <jdi 根目录>/data | 数据目录,保证有写权限 |
config.path_view | <jdi 根目录>/views | 视图模板目录 |
config.path_config_first | <空> | 第一优先级配置目录,找不到配置文件时,就在第二优先级配置目录里找,以此类推 |
config.path_config_second | <空> | 第二优先级配置目录 |
config.path_config_third | <jdi 根目录>/config | 第三优先级配置目录,一般默认即可,取框架的默认配置文件 |
config.timezone | PRC | 时区 |
config.session_start | true | 开启 session |
config.init_handler | null | 容器初始化回调,null 时默认调用 \JDI\App::initHandler |
- 简单路由
php -S 0.0.0.0:8080 router_simple.php
- 中间件与路由
php -S 0.0.0.0:8080 router_advanced.php
- 经典 MVC
php -S 0.0.0.0:8080 router_mvc.php
仅仅建议
- 变量名小驼峰,与数据库字段、数组键、URL中的参数统一
- 方法名小驼峰,与面向对象统一
- 函数名下划线,与面向过程、内置函数统一
route_get()
注册回调 GET 请求route_post()
注册回调 POST 请求route_any()
注册回调任何请求route_middleware()
注册路由中间件,顺序执行,组内优先route_group()
路由分组,隔离中间件
其中参数 url: /demo
全匹配;#/demo#
正则匹配(#
开头自动切换为正则模式);更多高级用法可使用 \JDI\Services\Router::addRoute
route_middleware(function () {
echo '中间件a1'; // 这里为了测试才用 echo,实际开发时只用于处理逻辑而不需要打印
});
route_middleware(function () {
echo '中间件a2';
});
route_group(function () {
route_middleware(function () {
echo '中间件b1';
});
route_get('/mid', function () {
return 'Just Do It!';
});
});
route_middleware(function () {
echo '中间件a3';
});
GET 请求 /mid
输出:
中间件b1
中间件a1
中间件a2
Just Do It!
中间件a3
// 不指定参数三,用默认方式
route_post('/xhr', function () {
panic();
});
// 指定参数三,自定义异常处理
route_post('/doc', function () {
panic('doc error');
}, function (Exception $exception) {
if ($exception instanceof AppException) {
// 只提示业务异常
alert($exception->getMessage());
} else {
// 非业务异常不提示
back();
}
});
POST 请求 /xhr
输出:{ "s": false, "c": 0, "m": "", "d": {} }
POST 请求 /doc
输出 alert()
弹层:doc error
\JDI\Services\Router::setStatus404Handler
设置响应 404 的回调函数\JDI\Services\Router::fireStatus404
触发 404 错误\JDI\Services\Router::getMatchedRoute
成功匹配的路由\JDI\Services\Router::getREMatches
URL 正则捕获项\JDI\Services\Router::getException
异常,通常用于在后置中间件做处理\JDI\Services\Router::setResponseContent
设置响应内容,通常用于在后置中间件改写响应内容\JDI\Services\Router::getResponseContent
获取响应内容
获取、过滤、表单验证、类型强转 请求参数
$_GET,$_POST
支持payload
- 以下的验证失败时会抛出异常 \JDI\Exceptions\AppException
$first_name = input('first_name');
$last_name = input('last_name');
var_dump($first_name, $last_name);
$request = request();
var_dump($request);
$first_name = input('first_name');
$last_name = validate('last_name')->required()->get('名字');
var_dump($first_name, $last_name);
validate('last_name')->required()->setTitle('名字');
$request = request();
var_dump($request);
遇到验证不通过时,立即终止后面的验证
validate('first_name')->required();
validate('last_name')->required()->setTitle('名字');
$request = request(); // 以串联短路方式验证
var_dump($request);
串联结果
{
"s": false,
"c": 10001000,
"m": "参数错误",
"d": {
"first_name": "不能为空"
}
}
即使前面的验证不通过,也会继续验证后面的字段
validate('first_name')->required();
validate('last_name')->required()->setTitle('名字');
$request = request(true); // 以并联方式验证
并联结果
{
"s": false,
"c": 10001000,
"m": "参数错误",
"d": {
"first_name": "不能为空",
"last_name": "名字不能为空"
}
}
'get.foo:i'
中的类型转换i
为整型,其它类型为:
Name | Type |
---|---|
i | int |
s | string |
b | bool |
a | array |
f | float |
d | double |
在路由回调里使用
return 'Just Do It!';
Just Do It!return [];
{ "s": true, "c": 0, "m": "", "d": {} }return ['foo' => 1];
{ "s": true, "c": 0, "m": "", "d": { "foo": 1 } }return api_msg('保存成功');
{ "s": true, "c": 0, "m": "保存成功", "d": {} }panic();
{ "s": false, "c": 0, "m": "", "d": {} }panic('保存失败');
{ "s": false, "c": 0, "m": "保存失败", "d": {} }panic('保存失败', ['foo' => 1]);
{ "s": false, "c": 0, "m": "保存失败", "d": { "foo": 1 } }panic(10001000);
{ "s": false, "c": 10001000, "m": "参数错误", "d": {} }; 参考翻译文件 lang_zh_CN.phppanic([10002001, 'name' => 'tom']);
{ "s": false, "c": 10002001, "m": "欢迎 tom", "d": {} }
以上方式都是 return api_json()
的衍生,更多需求可直接调用 api_json()
其中与 api_format()
的关系是:api_json()
即 json_encode(api_format())
function svc_foo()
{
return \JDI\App::singleton(__FUNCTION__, function () {
return new Foo();
});
}
如果上面的 svc_foo()
还未调用过,可以覆盖:
\JDI\App::set('svc_foo', function () {
return new Bar();
});
如果要强制修改,可以先删除:
\JDI\App::unset('svc_foo');
可以配置 MySQL, SQLite 等 PDO 支持的数据库
- 配置文件
config/mysql.php
- 用例参考
tests/phpunit/Services/DBTest.php
如果想同时使用 SQLite 等数据库, 复制 mysql.php
为新的数据库配置文件,按需配置 dsn,再注册容器即可(参考 services.php
svc_mysql()
)
config('app.lang')
- 例如第一、二优先级目录分别是
config/dev/
,config/common/
- 依次搜索下面文件,存在时返回第一个结果的文件内容:
config/dev/app.php
config/common/app.php
vendor/muzk6/jdi/config/app.php
config(['app.lang' => 'en'])
设置 run-time 的配置
trans(10001000)
- 假设当前语言是
zh_CN
, 默认语言是en
- 依次搜索
lang_zh_CN.php, lang_en.php
, 存在10001000
这个key
时返回第一个结果内容,都不存在时返回''
log_push('test', ['foo', 'bar'], 'login')
把内容写到data/log/login_20190328.log
各日志文件说明:
standard_xxx.log
PHP 标准错误处理程序写的日志,比较精简,但能记录 Fatal Error, Parse Errorerror_xxx.log
框架写的错误日志,比较详细app_xxx.log
用户写的默认日志,文件名可以修改,由log_push()
参数3控制
通用日志字段场景:
log_push('test', ['user_id' => 123, '这里写日志']);
log_push('test', ['user_id' => 123, '另一处又写日志']);
如果像以上例子都要记录 user_id
, 可以使用 \JDI\Services\Log::setExtraData
单独把 user_id
设置起来,后面调用 log_push()
时不需要再记录 user_id
:
svc_log()->setExtraData('user_id', 123);
log_push('test', ['这里写日志']);
log_push('test', ['另一处又写日志']);
log_push()
还有一个特性是延迟执行(脚本程序结束时才真正写日志),因此上面例子又可以写成:
log_push('test', ['这里写日志']);
log_push('test', ['另一处又写日志']);
svc_log()->setExtraData('user_id', 123);
手动刷写日志场景:
默认自动刷写,特殊场景(在 register_shutdown_function()
之前就调用了日志有关方法),手动刷写可参考 \JDI\Services\Router::dispatch
里的用法
svc_log()->autoFlush(false); // 关闭自动刷写
svc_log()->flush(); // 手动刷写
自定义日志引擎,参考 \JDI\Support\Svc::log
使用 \JDI\Services\Log::setFlushHandler
的例子
- 当前域名URL:
url('path/to')
- 其它域名URL:
url(['test', '/path/to'])
panic(10001000)
等于throw new AppException('10001000')
自动转为错误码对应的文本,参考翻译文件 lang_zh_CN.phppanic('foo')
等于throw new AppException('foo')
panic('foo', ['bar'])
等于throw (new AppException('foo'))->setData(['bar'])
AppException
异常属于业务逻辑,能够作为提示通过接口返回给用户看,而其它异常则不会(安全考虑)
request_flash()
把本次请求的参数缓存起来old(string $name = null, string $default = '')
上次请求的字段值
xsrf_field()
直接生成 HTMLxsrf_token()
生成 tokenxsrf_check()
效验,token 来源于$_SERVER['HTTP_X_XSRF_TOKEN'], $_POST['_token'], $_GET['_token'], $_REQUEST['_token']
请求时带上 Token
, 使用以下任意一种方法
POST
请求通过表单参数_token
, 后端将从$_POST['_token']
读取GET
请求通过?_token=
, 后端将从$_GET['_token']
读取- 通过指定请求头
X-XSRF-Token
, 后端将从$_SERVER['HTTP_X_XSRF_TOKEN']
读取
flash_set(string $key, $value)
闪存设置flash_has(string $key)
存在且为真flash_exists(string $key)
闪存是否存在,即使值为 nullflash_get(string $key)
闪存获取并删除flash_del(string $key)
闪存删除
assign('firstName', 'Hello')
定义模板变量return view('demo', ['title' => $title])
定义模板变量的同时返回渲染内容
back()
网页后退redirect('/demo')
跳转到/demo
alert()
JS alert() 并跳转回上一页
svc_auth()->login(1010); // 登录 ID 为 1010
svc_auth()->getUserId(); // 1010
svc_auth()->isLogin(); // true
svc_auth()->logout(); // 退出登录
- worker 遇到信号
SIGTERM
,SIGHUP
,SIGINT
,SIGQUIT
会平滑结束进程。如果要强行结束可使用信号SIGKILL
, 命令为kill -s KILL <PID>
- 当文件有变动时,队列有消息会触发 worker 退出,因此需要以守护进程方式启动 worker, 建议生产环境使用 supervisor 服务,临时测试可用
watch
命令启动 worker
composer require php-amqplib/php-amqplib:^2.9
config/rabbitmq.php
参考 tests/feature/message_queue.php
建议规则:
- 每个 worker 只消费一个队列;
- 队列名与 worker名 一致,便于定位队列名对应的 worker 文件;
- 队列名与 worker名 要有项目名前缀,防止在 Supervisor, RabbitMq 里与其它项目搞混
推荐使用本框架的运维后台 jdi-ops
跟踪调试日志
日志默认位于 data/xdebug_trace/
ext-xdebug
- 当前URL 主动开启:
/?_xt=name0
,name0
是当前日志的标识名 - Cookie 主动开启:
_xt=name0;
注意:URL
, Cookie
方式的前提必须先设置 config/whitelist.php
白名单 IP
或 白名单 Cookie
php demo.php --trace
在任何脚本命令后面加上参数 --trace
即可
日志默认位于 data/xhprof/
- 配置文件
config/xhprof.php
enable
设置为1
, 即可记录大于等于指定耗时的请求