目录:
以下是对 MySQL Internals Manual :: 26.1.1 Coding Style
的强调与补充。
-
禁止设置字段允许
NULL
,使用默认值代替。 -
能用 Unique 索引限制唯一,则不要用 Index 索引。
-
表、字段 Charset 统一
utf8
,Collation 统一utf8_general_ci
,存储引擎统一InnoDB
。 -
类似
is_delete
的字段,统一使用TINYINT(1)
类型,且务必建 Index 索引。 -
除非情况特殊,严禁使用
TEXT
/LONGTEXT
/BLOB
/LONGBLOB
等类型。 -
图标、超过255字节的文本,尽量不要存进数据库,会引起 行存储溢出。
-
对于能够使用
INNER JOIN
的场景,尽可能少用LEFT JOIN
,且关联的字段务必建索引。 -
数据库升级脚本内,对于
CREATE TABLE
等语句,务必加入IF NOT EXISTS
,尽可能避免引起异常。 -
对于存储 URL 的字段,必须采用
VARCHAR
类型,建议长度:2048
-8192
,参见:https://stackoverflow.com/questions/2659952/maximum-length-of-http-get-request -
JOIN ON
后面只带关联条件,将固定条件移动至WHERE
后。
以下是对 PSR-1
、PSR-2
的强调与补充;有关 PSR 公共规范,请参见:https://github.com/summerblue/psr.phphub.org/tree/master/psrs。
-
局部变量统一使用小驼峰,例如:
$goodsList
-
foreach 修改数组元素的值,可以使用引用赋值的方式,例如:
foreach($list as $k => $v){ $list[$k]['foo'] = 'bar'; }
可以优化为:
foreach($list as $k => &$v){ $v['foo'] = 'bar'; } unset($v); // 建议随手 unset,否则修改 $v 会改变数组末尾元素的值
-
对于外部不需要访问的属性或方法,尽可能写成
protected
/private
,以增强健壮性。 -
对于运行时可能出现的问题(例如类型错误等),尽可能采取「零容忍」态度,且报错越早越好;避免
return false
,使用throw new Exception
。 -
尽可能减少代码冗余,将重复的部分提取凝练到一起,例如:
public function couponInfo() { $coupon = Coupon::find()->where(['id'=>$this->id])->asArray()->one(); if($coupon['appoint_type'] == 1){ $info = Cat::find()->select('id,name')->where(['id'=>json_decode($coupon['cat_id_list'])])->andWhere(['is_delete'=>0,'store_id'=>$this->store_id])->asArray()->all(); }else if($coupon['appoint_type'] == 2){ $info = Goods::find()->select('id,name')->where(['id'=>json_decode($coupon['goods_id_list'])])->andWhere(['is_delete'=>0,'store_id'=>$this->store_id])->asArray()->all(); }else{ $info = []; } return [ 'code' => 0, 'data' => [ 'coupon'=>$coupon, 'info'=>$info, ], ]; }
可以优化为:
public function couponInfo() { $coupon = Coupon::find()->where(['id'=>$this->id])->asArray()->one(); switch($coupon['appoint_type']) { case 1: $info = Cat::find()->where(['id' => json_decode($coupon['cat_id_list'])]); case 2: $info = Goods::find()->where(['id' => json_decode($coupon['goods_id_list'])]); default: $info = null; } if($info){ $info = $info->select('id,name')->andWhere(['is_delete' => 0, 'store_id' => $this->store_id])->asArray()->all(); } return [ 'code' => 0, 'data' => [ 'coupon'=>$coupon, 'info'=>$info, ], ]; }
增强代码可读性,降低维护难度。
-
数据库查询出来的原始数据,将格式转换封装进 模型获取器 进行处理。
-
switch
语句return
后无需使用break
。 -
switch
语句必须带default
子句,如遇不可能值,则抛出异常。 -
对于废弃的函数、语句、变量、类,严禁注释、抛出异常或
return
,必须删除代码。 -
禁止使用
rand
函数,请用mt_rand
代替。 -
禁止使用
md5
函数,请用sha1
代替。 -
提取二维数组内某元素的值作为一个单独的数组,建议使用
array_map
。
-
控制器基础的方法名称统一,特殊方法可自定义。
-
控制器中不写逻辑代码,所逻辑代码处理放在
ModelForm
中。 -
Admin 模块
- actionIndex (列表显示页面)
- actionCreate (添加数据页面)
- actionStore (添加数据)
- actionEdit (编辑数据页面)
- actionUpdate (更新数据)
- actionDestroy (删除数据)
- 其它 (自定义名称)
例:
public function actionIdnex() { return 'index' } ... public function actionOther() { return 'other' }
-
Api 模块
- actionIndex (列表数据)
- actionStore (添加数据)
- actionEdit (单条数据)
- actionUpdate (更新数据)
- actionDestroy (删除数据)
- 其它 (自定义名称)
例:
public function actionIdnex() { return 'index'; } ... public function actionOther() { return 'other'; }
-
每一个控制器对应一个
ModelForm
目录,目录名称和控制器名称相同。User + |- GetUserForm.php |- GetUserTypeForm.php ...
-
单一原则,一个方法中只做一件事。
错误:
public function getUsers() { $users = User::find()->all(); $data = []; foreach($users as $item) { $data[] = $item['name']; ... } return $data; }
正确:
public function getUsers() { $users = User::find()->all(); return $data; } public function getResetUsers($users) { $data = []; foreach($users as $item) { $data[] = $item['name'] ... } return $data; }
-
需要获取关联数据时,尽可能采用
hasOne
/hasMany
定义模型关系,替代原生的leftJoin
查询;参见:https://www.yiiframework.com/doc/guide/2.0/zh-cn/db-active-record#relational-data。错误:
User:find()->alias('u')->leftJoin(['g' => Goods::tableName()], 'o.goods_id=u.id')->asArray()->all();
正确:
class User extends ActiveRecord { public function getGoods() { return $this->hasMany(Good::className(), ['user_id' => 'id']); } } class UserFormModel extends Model { public function getUsers() { $users = User::find()->with('goods')->all(); return $users; } }
-
在
FormModel
中进行数据查询时,尽量不要使用asArray()
方法。否则导致模型特性消失,无法访问关联模型的数据。错误:
User::find()->asArray()->all();
正确:
User::find()->all();
-
在条件查询时,不要出现
type=1
、is_delete=0
,status=2
等情况。错误:
User::find()->where(['type' => 1, 'is_delete' = 0, 'status' => 2])->all();
正确:
class User extends ActiveRecord { /** * 用户类型:管理员 */ const USER_TYPE_ADMIN = 1 /** * 用户类型:会员 */ const USER_TYPE_MEMBER = 2 /** * 用户状态:启用 */ const USER_STATUS_TRUE = 1 ... } class UserFormModel extends Model { public function getUsers() { $users = User::find()->andWhere(['type' => User::USER_TYPE_ADMIN, 'status' => User::USER_STATUS_TRUE])->all(); return $users; } }
- 所以关联关系写在对应的模型中(例子在
ModelForm
小节)。 - 模型中一个字段有着多种情况,应在模型中定义成常量并标识该字段含义(例子在
ModelForm
小节)。
- 公共逻辑目录:�
core/models/common
。 - 公共逻辑目录分为
Admin(后台管理)
和Api(小程序接口)
。 - 在编写代码的过程中,如果有部分逻辑代码是通用则可以写在
Common
中。例如:创建订单、处理订单、支付等。
响应格式:
{
"code": <int>,
"msg": <string>,
"data": <array> | <object>
}
-
在 Controller 中,可直接返回数组,以输出 JSON 数据:
return ['code' => $code, ...];
-
在 Filter 中,可直接使用如下方式设置响应数据,并截断执行:
\Yii::$app->response->data = $some_response_data; return false; // 参见:<https://www.yiiframework.com/doc/guide/2.0/zh-cn/structure-filters#creating-filters>
-
在
api
module 的 Controller 中,应使用如下方式输出结构化响应数据:return new \app\opening\ApiResponse($code, $msg, $data);
或:
return new \app\opening\BaseApiResponse($array);
切勿在输出响应时使用废弃的
json_encode
和renderJson
方法,将会引发app\opening\exceptions\InvaildResponseException
。
- 若模型继承
app\models\Model
,则可以直接使用errorResponse
属性:
if(!$this->validate()){
return $this->errorResponse;
}
- 其它情况,可使用如下形式输出模型验证错误:
return new \app\opening\ValidationErrorResponse($model->errors);
切勿在输出响应时使用废弃的 Model::getModelError
方法。
- IDE 生成的注释记得改名字,例如:
/**
* Created by PhpStorm.
* User: <YOUR NAME>
* Date: 2017/8/14
* Time: 17:46
*/
- 行内注释尽量首部带空格,例如:
// This is a function.
function foo(){
// do nothing.
}
-
对于注释无用代码,尽可能不要将
//
打在行首,会引起代码格式化时编辑器误判,例如:- 错误:
function foo(){ $bar = 1000; // $bar = -1000; return $bar; }
- 正确:
function foo(){ $bar = 1000; // $bar = -1000; return $bar; }
我们采用 git 的 tag 功能来实现对版本的精准控制。Gitee 自带了发行版(release
)控制功能(基于 git tag),所以我们直接在 gitee 上使用图形化操作的方式创建即可。
- 版本命名格式:
v*.*.*
,详细参见:https://semver.org/lang/zh-CN/。 - 标题命名格式:
v*.*.* - 20**年**周
。
- 图片目录:
/web/statics/wxapp/images/
- 代码添加:
/modules/api/models/StoreForm.php
- 前端应用:小程序图片数组已存入缓存,Key 值:
wxapp_img
;页面调用变量:__wxapp_img
多数情况,异常(exception
)对我们可能比较陌生,多数都是框架或系统抛出异常,我们接收并处理。但实际上,如果能够在我们编写的实际业务代码中恰当地抛出异常,将会有意想不到的效果。下面我用几个符合我们实际场景的简明实例来说明。
优化前:
function actionIndex(){
$result = '';
$foo = Yii::$app->request->post('foo');
if(empty($foo)) {
$result = 'foo error';
}
else {
$bar = Yii::$app->request->post('bar');
if(empty($bar)) {
$result = 'bar error';
}
$result = 'ok';
}
return $result;
}
如上可以发现,代码嵌套层数非常多,当然你可以优化成直接 return
的方式,但下面这个呢:
function actionIndex(){
$result = '';
$foo = getValueByName('foo');
if($foo === null) {
return 'foo error';
}
$bar = getValueByName('bar');
if($bar === null) {
return 'bar error';
}
return 'ok';
}
function getValueByName($name){
$value = Yii::$app->request->post($name);
if($value === null) {
return null;
}
else if(empty($value)){
return null;
}
return $value;
}
如上代码,存在的问题:
- 错误丢失:在
actionIndex
内我们只能得到的值是null
,无法判断getValue
内部具体错误是什么;虽然可以给getValue
方法多增加一个$error
参数返回具体错误,但在外层也需要多做判断,无疑大大增加代码复杂度,这不是我们想要的。 - 依赖外层判断:在
getValue
方法内如果出现错误,例如某个值不存在,而这个值可能导致后续系统运行出现 BUG;如果我们直接返回null
,那就无法保证这个错误会在外部被处理,可能在外层代码压根不会有人判断,由此引发更大的隐患。
因此我们将代码进行优化,优化后:
function actionIndex(){
$result = 'ok';
try{
$foo = getValueByName('foo');
$bar = getValueByName('bar');
}
catch(\Exception $e){
$result = $e->message; // 读取异常的 message 属性
}
return $result;
}
function getValueByName($name){
$value = Yii::$app->request->post($name);
if($value === null) {
throw new \Exception('value is null'); // 参数为 message 属性
}
else if(empty($value)) {
throw new \Exception('value is empty'); // 参数为 message 属性
}
return $value;
}
如上,代码清爽了不少。而且既能在不改变原有函数结构的基础上,将错误信息完整地传递到外层;又可以保证异常一定会被处理,否则就会报错(例如显示 Yii 框架的错误页面)。
其实,异常应该按照类型进行区分,根据不同类型的异常做不同的处理,比较规范的做法应该如下:
function actionIndex(){
$result = 'ok';
try{
$foo = getValueByName('foo');
$bar = getValueByName('bar');
}
catch(\yii\base\UnknownPropertyException $e){
$result = $e->message . 'is null';
}
catch(\yii\base\InvalidValueException $e){
$result = $e->message . 'is empty';
}
return $result;
}
function getValueByName($name){
$value = Yii::$app->request->post($name);
if($value === null) {
throw new \yii\base\UnknownPropertyException($name);
}
else if(empty($value)) {
throw new \yii\base\InvalidValueException($name);
}
return $value;
}
我们也可以自定义异常类,不过在大多数中小型系统的实际业务场景下,略显麻烦。
更多关于 PHP 异常处理的详细介绍,参见:http://www.w3school.com.cn/php/php_exception.asp
目前本项目已完全集成 Sentry SDK,配置文件位于 core/config/web.php
。在非 YII_DEBUG 模式下,只要产生未被处理的异常,就会被 Sentry 捕捉并记录到服务器,同时产生一条事件记录,对应此记录的 Event ID
将会被显示在页面上:
注:此页面的 view 位于
@app/views/error/
目录下。
通过修改 Sentry 配置(位于 config/web.php
),你可以实现通过类型或消息排除指定异常。
'sentry' => [
'class' => 'app\opening\Sentry',
'options' => [
'excluded_exceptions' => [
// 通过异常类型排除指定异常
'yii\web\HttpException',
// 通过异常类型 + 消息排除指定异常
'yii\db\Exception' => '/Connection refused/i', // 注意此处为 PREG 表达式
],
],
],
同时,你可以主动提交异常(exception
)或消息(message
)到 Sentry:
// 提交异常
\Yii::$app->sentry->captureException($ex);
// 高级用法,附带其他数据
\Yii::$app->sentry->captureException($ex, [
'extra' => [ // extra 必须为数组
'foo' => 'bar',
'...' => '...'
],
]);
// 提交消息
\Yii::$app->sentry->captureMessage('my log message');
// 高级用法,格式化消息并附带其他数据
// 注意,格式化消息是只有 captureMessage 方法才具备的特性
\Yii::$app->sentry->captureMessage('my %s message', ['log'], [
'extra' => [ // extra 必须为数组
'foo' => 'bar',
'...' => '...'
],
'level' => 'warning' // 默认 error
]);
另外,你还可以在某些关键点记录「日常运行产生的数据(breadcrumbs
)」。这些数据将会在发生异常或消息时,作为附属数据一并被提交到 Sentry;而在运行正常时,是不会提交到 Sentry 的。
\Yii::$app->sentry->breadcrumbs->record([
'foo' => 'bar',
'...' => '...',
]);
为了区别生产和调试环境,不同环境采用不同的日志级别、异常处理方式。目前,index.php
默认被定义为生产环境入口,原有 debug 特性迁移至 debug.php
以及 .env
文件。
简单来说,需要开启 debug 特性,有两种方法:
- 在远程调试客户错误时,将 URL 中
index.php
修改为debug.php
即可。 - 开发过程中,本地在
core
文件夹下新建名为.env
的文件,并配置环境变量即可(见下方说明)。
环境配置(env
)文件位于 core/.env
;其文件模板位于 core/.env.example
,根据情况取消注释即可。
业务代码中,可使用 env($name, $defaultValue)
函数读取环境变量的值。
关于新增 dev 分支后,有两种方式将本地改动提交到远程 dev 分支。
-
(1)
git fetch origin dev ## fetch dev分支到本地 git checkout dev ## 创建并切换到 dev 分支,且关联到远程的 dev 分支
-
(2)
git fetch origin dev ## fetch dev分支到本地 git branch --set-upstream-to=origin/dev master ## 将本地 master 分支关联到远程 dev 分支
请查看:core/helpers.php
。
将图片地址修改如下即可:
https://<DOMAIN>/<PATH_TO_CORE>/web/thumb.php?src=<IMG_PATH>&size=<WIDTH>x<HEIGHT>&zoom=1
- DOMAIN:域名
- PATH_TO_CORE:指向
core
目录的 URL 地址 - IMG_PATH:服务器上的图片绝对路径(务必确保是路径而非 URL)
- WIDTH:预期宽度
- HEIGHT:预期高度
更多详细说明,请参见:https://github.com/wi1dcard/thumb-php
目前我已经将数据序列化写成 Yii Component,请使用:
\Yii::$app->serializer->encode($data);
\Yii::$app->serializer->decode($data);
替代原有:
serialize();
unserialize();
运行 core/web/tester.php
即可,将会检查所需扩展是否安装、配置是否正常。
例如(点击链接查看示例图片):
用法参见:https://github.com/wi1dcard/yii2-opening-storage。
在本项目内,可使用如下方式获取 StorageComponent
实例;其余操作与扩展包文档一致。
$storage = \Yii::$app->storage; // 获取 StorageComponent
// 或
$storage = \Yii::$app->storageTemp; // 获取用于存储临时文件的 StorageComponent
用法参见:https://github.com/wi1dcard/yii2-opening-express。
用法参见:https://github.com/wi1dcard/yii2-opening-sms。
用法参见:https://github.com/wi1dcard/yii2-opening-event。
在本项目内,可使用如下方式获取 EventDispatcher
实例;其余操作与扩展包文档一致。
$ed = \Yii::$app->eventDispatcher;
在 Controller 内,将某 Action 改为内部调用其它 Action,此种行为被称作 动作重定向
。
在本项目内,可使用如下方式实现:
class Controller
{
public function actions()
{
return [
'foo' => new \app\utils\RedirectAction('bar'), // 访问 `module/controller/foo` 将会被重定向至 `actionBar`
// ...
];
}
public function actionBar()
{
return 'bar';
}
}
参见:https://wi1dcard.github.io/tutorials/yii2-redirect-controller-in-module/
本项目已集成至 app\modules\mch\Module
,在此类的实例内,可使用如下方式实现:
$this->redirectController('bar', 'foo'); // 将 foo(虚拟控制器)重定向至 bar(实际控制器)
// 或者批量方式...
$map = [
'foo' => 'bar',
'foo/bar' => 'bar/foo'
];
array_walk($map, [$this, 'redirectController']);
参见:https://wi1dcard.github.io/snippets/yii2-response-send-file/
如无特别说明,以下列出均为 Linux 命令;Windows 下可以使用 Git Bash 运行。
git ls-tree -r --name-only HEAD | wc -l
参见:https://wi1dcard.github.io/snippets/mysql-replace-text-in-all-fields/
执行检查:
composer check-cs <DIR_OR_FILE>
尝试自动修复:
composer fix-cs <DIR_OR_FILE>
-
WDCP 面板 Nginx + Apache 无法检测 HTTPS
修改
/www/wdlinux/nginx/conf/naproxy.conf
,新增一行proxy_set_header X-Forwarded-Proto $scheme;
即可。 -
宝塔面板上传文件
sha1_file
失败sha1_file(): open_basedir restriction in effect. File(/www/wwwroot/tmp/***) is not within the allowed path(s): (***)
宝塔面板创建站点时默认添加
.user.ini
,文件内包含open_basedir
环境变量,参见https://blog.csdn.net/fdipzone/article/details/54562656。解决方案有二:- 删除
.user.ini
文件。 - 在
open_basedir=***
后追加:
+ 报错信息内文件所在目录,例如/www/wwwroot/tmp/***
。
- 删除
-
AMH 面板重复加载 MySQL 扩展
PHP Warning: Module 'mysql' already loaded in Unknown on line 0
解决方案:
打开对应环境的
php.ini
配置文件(通常位于/home/wwwroot/<ENV>/etc/amh-php.ini
),使用#
或;
注释extension=mysql.so
此行即可。