总结下webman最新版本的开发规范以及代码建议。欢迎大家提交自己更好的开发规范以及代码建议。
如果是一个全新项目,并没有PHP版本依赖,目前应该尽量选择PHP8.2+
作为你的PHP版本,可以拥有更好的性能,webman1.6.0
最新版本的最低版本要求是PHP8.0
。
1.6.0
升级指南:https://www.workerman.net/doc/webman/upgrade/1-6.html
有些PHP扩展可能不支持PHP的高版本,这个时候你要做出选择,使用低版本还是寻求更好的扩展解决方案。
webman遵循PSR-2
命名规范以及PSR-4
自动加载规范,并注意如下规范:
如果你没有遵循某些规范,可能会导致部分功能的异常。
Trait
)文件名和类名保持一致,并且使用首字母大写的驼峰命名;.php
后缀;generate_order_no
;getUserName
;tableName
、instance
;__
打头的函数或方法作为魔术方法,例如__call
和__callStatic
;webman是常驻内存的框架,一般来说,php文件载入后便以opcode
的方式常驻内存,不会再次从磁盘读取(模版文件除外)。 所以正式环境业务代码或配置变更后需要执行php start.php reload
才能生效。如果是更改进程相关配置或者安装了新的composer包需要重启php start.php restart
。
为了方便开发,webman自带一个monitor自定义进程用于监控业务文件更新,当有业务文件更新时会自动执行reload。
此功能只在workerman以调试模式运行(启动时不加-d
)才启用。windows用户需要执行windows.bat
或者php windows.php
才能启用。
在传统php-fpm
项目里,使用echo
、var_dump
等函数输出数据会直接显示在页面里,而在webman开发过程中(调试模式启动时),这些输出往往显示在终端上,并不会显示在页面中(模版文件中的输出除外)。
exit
、die
、sleep
语句执行die
或者exit
会使得进程退出并重启,导致当前请求无法被正确响应。业务执行执行die
或者exit
语句会导致进程退出,并显示WORKER EXIT UNEXPECTED
错误。当然,进程退出了会立刻重启一个新的进程继续服务。如果需要返回,可以调用return
。
sleep
语句会让进程睡眠,睡眠过程中不会执行任何业务,框架也会停止运行,会导致该进程的所有客户端请求都无法处理。
pcntl_fork
函数pcntl_fork
用户创建一个进程,这在webman中是不允许的。
业务代码里不要有死循环,否则会导致控制权无法交还给workerman框架,导致无法接收处理其它客户端消息。
项目团队应当尽量使用统一的IDE作为开发工具,并规范一致的代码规范配置项,如果使用的第三方代码规范及自动完成插件。如果团队成员较多而无法完全统一,最低限度,项目代码风格必须遵循PSR-1
和PSR-2
规范。
助手函数的初衷是为了简化代码和更方便记忆,但如果不是很清楚助手函数的内部实现原理,很容易导致滥用
由于现代的IDE提示和自动完成功能之强大,助手函数的作用非常有限,而且只会用助手函数对于框架的原理认识较浅,因此建议是掌握助手函数的内部实现原理后再来决定在项目规范中是否需要使用助手函数,以及如何使用。毕竟有些场景下,助手函数是非常简单实用的,例如:
public function get_user_info($id)
{
$user = User::getOrEmpty($id);
return json($user);
}
产品交付给客户的时候,有些时候助手函数能够让客户自定义模板的时候更方便。如果你需要额外定义或者覆盖原有的助手函数,可以直接在应用的functions.php
公共文件中定义。
日志记录建议直接使用PSR-3
规范提供的接口方法记录,例如:
Log::log('info','开源技术小栈测试日志');
Log::log('error','开源技术小栈测试错误日志');
应当改为
Log::info('开源技术小栈测试日志');
Log::error('开源技术小栈测试错误日志');
支持的方法包括debug
, info
, notice
, warning
, error
, critical
, alert
, emergency
以及用于SQL日志记录的sql
方法。
确保设置日志的最大数量限制$maxFiles
,避免日志空间过大导致存储空间占满。
7 // $maxFiles
超过设置的数量后,最早的日志将会被自动清理。
webman默认路由规则是http://127.0.0.1:8787/{控制器}/{动作}
。
默认控制器为app\controller\IndexController
,默认动作为index
。
如果某个应用需要关闭路由功能,可以在应用的route.php
配置文件中定义
Route::disableDefaultRoute();
尽量明确定义路由的请求类型,提高路由解析的效率。
推荐使用:
Route::get('hello/{id}', 'index/hello');
替代:
Route::add(['GET'],'hello/{id}', 'index/hello');
/
开头错误的用法
Route::any('test', function (Request $request) {
return response('test');
});
正确的用法
Route::any('/test', function (Request $request) {
return response('test');
});
可能的情况下,尽可能多使用路由分组。可以充分利用分组的匹配机制提高路由解析性能。
推荐使用
Route::group('/blog', function() {
Route::get(':id$', 'blog/read');
Route::get(':id/edit$', 'blog/edit');
});
不建议使用
Route::get('/blog/:id$', 'blog/read');
Route::get('/blog/:id/edit$', 'blog/edit');
如果路由中存在参数,通过{key}
来匹配,匹配结果将传递到对应的控制器方法参数中(从第二个参数开始依次传递),例如:
// 匹配 /user/123 /user/abc Route::any('/user/{id}', \[app\\controller\\UserController::class, 'get'\]);
namespace app\controller;
use support\Request;
class UserController
{
public function get(Request $request, $id)
{
return response('接收到参数'.$id);
}
}
请务必把你的WEB
根目录指向public
目录而不是应用根目录,并且不要随意更改入口文件的位置。public
目录下面不要放除了入口文件和资源文件以外的其它应用文件。
在开发过程中,应该尽量保持你的测试环境和正式部署环境的一致性,包括运行环境和版本,无论在本地测试环境还是部署环境,都应当统一使用域名方式访问,本地可以使用测试域名,例如你的正式部署域名为webman.cn
,那么本地测试环境可以使用webman
或者webman.test
作为测试域名,避免使用localhost
或者127.0.0.1
这种测试地址。对于有多个域名的部署应用,本地也要尽量模拟多个域名。
在部署到生产环境的时候,确保你已经关闭了调试模式,可以通过修改环境变量的方式关闭调试模式。
'debug' => true
无论是本地开发还是生产环境部署,都建议保持统一的配置文件,然后通过修改环境变量的方式(本地开发可以通过定义
.env
文件)设置区别部分。
关闭调试模式后,系统的健康状态和运行监控主要依靠日志或者你使用的监控服务。所以,要养成定时检查日志和运行状态的习惯。
项目根目录下面有一个.gitignore
文件,用于定义提交版本库的时候哪些文件或者目录需要忽略,设置忽略的文件不会被同步到远程服务器,只是用于本地开发。
该文件默认内容如下,你可以根据项目的目录和规范进行调整。
/runtime
/.idea
/.vscode
/vendor
*.log
.env
/tests/tmp
/config/thinkorm.php
项目使用的核心框架以及composer
安装的扩展,不应当被同步到版本库中,只需要同步composer.json
以及composer.lock
文件。然后在服务器端进行composer
更新。
每个项目都应该在根目录添加readme.md
文件,并遵循Markdown
规范写作,对项目做简要的说明(尤其是目录和代码规范),如果项目比较复杂,可以附上一个项目详细说明或者规范的文档地址,如果你的项目是前后端完全分离开发的话,应该事先规划好后台的API接口,然后创建一个API文档,便于指导前端开发人员进行接口调用,以及方便在线调试。
为了避免命名冲突,配置文件中统一开启控制器类库后缀。
'controller_suffix' => 'Controller',
支持在config/app.php
设置控制器后缀,如果config/app.php
里controller_suffix
设置为空''
,则控制器类似如下app\controller\Foo.php
。
<?php
namespace app\controller;
use support\Request;
class Foo
{
public function index(Request $request)
{
return response('hello index');
}
public function hello(Request $request)
{
return response('hello webman');
}
}
强烈建议将控制器后缀设置为Controller
,这样能能避免控制器与模型类名冲突,同时增加安全性。
控制器建议继承一个公共的控制器类,便于统一调整和增加通用逻辑。默认安装后,你可以编写了一个BaseController
实例基础控制器类,你可以根据自己项目的需求进行调整,包括改变命名空间。
对于控制器操作方法的拦截以及统一处理应当使用中间件独立操作,原来的初始化方法已经废弃。控制器中间件不需要继承任何的基础控制器类即可使用,仅仅需要你定义middleware
属性即可。
控制器的代码应当尽量少,以确保逻辑清晰和可读性。并始终保持controller
层作为访问控制器层的名称。
请求数据的验证操作统一使用验证器进行验证。操作方法中的对象使用依赖注入,其它的必要参数使用参数自动绑定。不要在操作方法中输出除了调试信息之外的任何内容,而是通过return
返回需要输出的内容。
操作方法中始终明确响应输出的类型,默认的return
方式使用的是HTML
输出类型。
数据表和字段采用小写加下划线方式命名,例如resty_user
表和user_name
字段,禁止使用驼峰、中文或者拼音作为数据表及字段命名。
id
;resource_id
形式(例如user_id
);create_time
和update_time
),并使用datetime
类型;delete_time
,类型和系统时间字段保持一致;ThinkORM
模型类应当通过定义autoWriteTimestamp
属性明确时间字段类型;不要在数据库配置文件以外的地方配置或者动态设置数据库连接信息,包括模型内部。尽量不使用原生SQL查询,而应当使用查询构造器。
不要使用任何数据库工具创建、修改数据表和填充数据,应当使用数据迁移并同步版本库给所有成员。
每次数据查询都用Db
类或者模型类的静态方法。避免在模型方法中直接写复杂的查询条件,而应当使用查询范围或者搜索器统一定义后调用。用查询表达式方式替代传统的数组查询。
查询数据的处理统一使用获取器定义,而不要直接处理数据。对写入数据需要额外处理的话统一使用修改器。
对于使用了SQL函数的用法,使用fieldRaw
、orderRaw
和whereRaw
/whereExp
替代field
、order
和where
用法。
仅在使用字符串查询条件,以及调用whereExp
和whereRaw
方法的时候需要使用手动参数绑定,其余情况下都会自动进行参数绑定,禁止手动调用bind
方法。
不要在WEB访问的时候进行大量数据操作,容易超时的数据处理应当在命令行下通过创建指令完成。
注:以下以使用
ThinkORM
关联模型说明
Null
的数据查询值为Null
的数据应当使用whereNull
或者whereNotNull
方法
// 查询email为空,并且name不为空的用户数据
User::whereNull('email')
->whereNotNull('name')
->select();
对于一些常用的查询,尽量使用系统封装的快捷查询方法,例如:
User::whereIn('id', [1,2,3])
->whereLike('name', 'think%')
->select();
相当于下面的查询
User::where('id', 'in', [1,2,3])
->where('name', 'like', 'think%')
->select();
更多的方法可以参考官方手册或者使用IDE的自动提示。
对于一些简单的数据获取,你完全不需要查询整个表的数据,例如查询某个字段(满足条件的)值或者列数据。
// 获取id为10的用户名称
User::where('id', 10)
->value('name');
// 获取状态为1的用户名称列表
User::where('status', 1)
->column('name');
// 获取分数大于80的用户分数列表,以用户ID为索引
User::where('score', '>', 80)
->column('score', 'id');
如果你的min
/max
查询的是一个字符串类型字段,记得加上第二个参数并传入false
。
// 获取name字段的最大值
User::max('name', false);
时间查询主要用于时间字段的区间查询,whereTime
方法的优势是支持自动识别时间字段类型并进行转换处理。
// 大于某个时间
User::whereTime('birthday', '>=', '2008-10-1')
->select();
// 小于某个时间
User::whereTime('birthday', '<', '2000-10-1')
->select();
// 时间区间查询
User::whereBetweenTime('birthday', '1990-10-1', '2000-10-1')
->select();
// 不在某个时间区间
User::whereNotBetweenTime('birthday', '1970-10-1', '2000-10-1')
->select();
对于年/月/日/周的时间查询,推荐使用whereYear
/whereMonth
/whereDay
/whereWeek
方法查询,例如:
// 查询本月注册的用户
Db::name('user')
->whereMonth('create_time')
->select();
// 查询上月注册用户
Db::name('user')
->whereMonth('create_time','last month')
->select();
// 查询2018年6月注册的用户
Db::name('user')
->whereMonth('create_time', '2018-06')
->select();
// 查询当天注册的用户
Db::name('user')
->whereDay('create_time')
->select();
// 查询昨天注册的用户
Db::name('user')
->whereDay('create_time', 'yesterday')
->select();
// 查询2018年6月1日注册的用户
Db::name('user')
->whereDay('create_time', '2018-06-01')
->select();
高级的时间表达式查询可以使用PHP的相对时间格式,例如:
// 查询两天以内的博客
Blog::whereTime('create_time','-2 days')
->select();
// 查询昨天中午后发的博客
Blog::whereTime('create_time','yesterday noon')
->select();
更多的时间表达式查询你可以自由发挥。
你可以查询当前时间是否在两个时间字段区间范围内,通常用于一些活动以及优惠券的有效期查询等等。
// 查询有效期内的活动
Event::whereBetweenTimeField('start_time','end_time')
->select();
// 查询没有开始或者已经过期的活动
Event::whereNotBetweenTimeField('start_time','end_time')
->select();
可以直接比较两个字段的大小进行查询
User::whereColumn('update_time', '>', 'create_time')
->select();
User::whereColumn('score1', '>', 'score2')
->select();
如果需要比较两个字段相同,可以使用
User::whereColumn('score1', 'score2')
->select();
应当使用条件查询替代在组装查询条件的时候写大量的if
和else
。
User::when($condition, function ($query) {
// 满足条件后执行
$query->where('score', '>', 80)->limit(10);
})->select();
并且支持不满足条件的分支查询,并且支持多次调用when
方法。
User::when($condition, function ($query) {
// 满足条件后执行
$query->where('score', '>', 80)->limit(10);
}, function ($query) {
// 不满足条件执行
$query->where('score', '>', 60);
})->select();
JSON
查询如果你的字段类型使用的是JSON类型,那么可以直接使用框架提供的JSON查询支持。
User::where('info->nickname', 'webman')
->find();
注意,需要在模型里面定义JSON
字段属性。
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
// 设置json类型字段
protected $json = ['info'];
}
如果使用Db查询的话,可以改为
$user = Db::name('user')
->json(['info'])
->where('info->nickname','webman')
->find();
如果需要对某个字段使用SQL函数表达式查询,可以使用
User::whereExp('nickname', "= CONCAT(name, '-', id)")
->whereRaw('LEFT(nickname, 5) = ?', ['think'])
->select();
注意whereExp
和whereRaw
方法的区别,前者是对某个字段使用SQL函数表达式,后者是整个查询就是一个SQL函数表达式。
可以使用:
// 博客的阅读数递增1 评论数递减2
Blog::where('id', 10)
->inc('read_count')
->dec('comment_count', 2)
->save();
如果你需要按照指定字段的值的顺序来排序,可以使用
User::where('status', 1)
->orderField('id', [1,2,3])
->select();
网站的上传功能也是一个非常容易被攻击的入口,所以对上传功能的安全检查是尤其必要的。
系统的验证类提供了文件上传的安全支持,包括对文件后缀、文件类型、文件大小以及上传图片文件的合法性检查,确保你已经在上传操作中启用了这些合法性检查。
对于大量的表单需要验证的情况,建议使用验证器功能统一进行数据的合规验证。验证器的验证操作应该在控制器或者路由阶段使用validate
方法进行处理,模型的数据验证功能新版已经取消不再建议使用,模型和数据库操作的时候应该传入经过安全处理过的数据。
跨站脚本攻击(cross-site scripting,简称XSS
),XSS是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中。
在渲染输出的页面中,要对一些数据进行安全处理,防止被恶意利用造成XSS攻击,如果是5.1版本的话,所有的输出都已经经过了htmlentities
转义输出,确保安全。如果是5.0版本的话,你可以自定义一个xss过滤函数,在模板文件中对一些关键内容变量进行函数处理。
CSRF 跨站请求伪造是 Web 应用中最常见的安全威胁之一,攻击者伪造目标用户的HTTP请求,然后此请求发送到有CSRF漏洞的网站,网站执行此请求后,引发跨站请求伪造攻击。攻击者利用隐蔽的HTTP连接,让目标用户在不注意的情况下单击这个链接,由于是用户自己点击的,而他又是合法用户拥有合法权限,所以目标用户能够在网站内执行特定的HTTP链接,从而达到攻击者的目的。
开启表单令牌验证,尽量开启强制路由并严格规范每个URL请求,定义单独的MISS路由规则。遵循请求类型的使用规范并做好权限验证,删除操作必须使用DELETE
请求,数据更改操作必须使用POST
、PUT
或者PATCH
请求方法,GET
请求不应该更改任何数据。