本规范基于 PSR 和实际项目经验整理而成,目前已在公司内部推行使用,特分享如下。
分为编码格式篇和程序设计篇两大部分。
目录
编码格式篇
基于 PSR-1、PSR-2、PSR-12 。
样例
<?php
/**
* this is a example class
*/
declare(strict_types=1);
namespace VendorPackage;
use VendorPackage{ClassA as A, ClassB, ClassC as C};
use VendorPackageSomeNamespaceClassD as D;
use function VendorPackage{functionA, functionB, functionC};
use const VendorPackage{ConstantA, ConstantB, ConstantC};
class Foo extends Bar implements FooInterface
{
public function sampleFunction(int $a, int $b = null): array
{
if ($a === $b) {
bar();
} elseif ($a > $b) {
$foo->bar($arg1);
} else {
BazClass::bar($arg2, $arg3);
}
}
final public static function bar()
{
// method body
}
}
文件
- PHP 代码必须使用
<?php ?>
标签,如果是纯 PHP 代码,则不带结束标签?>
; - 编码:PHP 代码文件必须以不带 BOM 的 UTF-8 编码(关于 BOM 以及在 PHP 中的问题请自行百度);
<?php
、declare
、namespace
、use
块必须按照顺序编写,并且后面必须跟一个空行;use
块:类、函数(use function
)、常量(use const
)的use
需按照此顺序书写,且每个小块之间必须有一空行;
行
- 每行不该多于80个字符,大于80字符的行 应该 折成多行;
- 非空行后一定不可有多余的空格符;
- 每行一定不可存在多于一条语句
缩进
- 代码必须使用4个空格符的缩进(请将 IDE 设置成 Tab 转 4 空格);
关键字
- PHP 关键字必须小写,且使用缩写形式(如使用 bool 而不是 boolean);
命名
- 类的命名必须符合首字母大写的驼峰规则;
- 方法和函数的命名必须符合首字母小写的驼峰规则;
- 常量命名必须全部大写,以下划线分割字母;
- 方法和属性不可用前导下划线表示其可访问性,而应当使用相应的访问修饰符;
- 类、方法、属性的名称应当能反映其意义,禁止使用诸如
$a
、$ddd
这样毫无意义的命名; - 应当优先使用业务概念命名,尽量避免使用纯技术命名,如 sendCoupon 表示发券,属于业务用语,而 createUserCoupon 属于纯粹的技术用语;
- 在概念明晰的前提下,命名应当尽可能简洁,避免不必要的词语。如:相比 $orderList、ajaxGetOrderList,更好的命名是 $orders,getOrders;再如:UserCoupon::send() 优于 UserCoupon::sendCoupon(),前者恰好表达了其含义,而后者不必要地重复了词语 Coupon;
- 不应使用通用的变量名,而应该使用具体的名称以增强可读性。如相对于使用
$list
,$users
更符合上下文,更易于理解和维护; - 不应使用非通用的缩写,造成理解上的困难;
- 避免使用纯技术要素的前后缀,如 ajaxGetOrders(作为一个接口,没必要也不应当限制消费者必须使用ajax);
- 应当使用名词复数表示集合,如应使用 $orders 表示订单列表而不是 $orderList;
命名空间和类
- 命名空间和类的命名必须符合 PSR-4;
- 每个文件只定义一个类;
- 类命名:大写驼峰规则;
- 不要将类放到顶级命名空间中,至少需使用一层命名空间(一些特殊框架或历史项目可不遵守);
- 创建类:
$cls = new MyClass();
无论有无参数,都要加括号; traits
:use traits
:必须放在类左大括号下一行,每个trait
单独一行,有自己的use
。use traits block 后面要有一个空行;
例:
class ClassName extends ParentClass implements ArrayAccess, Countable
{
// constants, properties, methods
}
class ClassName extends ParentClass implements
ArrayAccess,
Countable,
Serializable
{
// constants, properties, methods
}
class ClassName
{
use FirstTrait;
use SecondTrait;
use ThirdTrait;
}
class Talker
{
use A, B, C {
B::smallTalk insteadof A;
A::bigTalk insteadof C;
C::mediumTalk as FooBar;
}
}
类的常量、属性和方法
- 常量:全部字母大写,用下划线分割,如
ORDER_TYPE
; - 属性:
- 小写驼峰命名,如
$order
; - 必须使用访问修饰符,不可使用
var
修饰属性; - 不可使用下划线开头来区分可访问性;
- 小写驼峰命名,如
- 方法:
- 小写驼峰,如
submitOrder
; - 必须使用访问修饰符;
- 不可使用下划线开头来区分可访问性;
- 方法名称后一定不可有空格符;
- 参数列表中,每个逗号后面必须要有一个空格,而逗号前面一定不可有空格;
- 参数列表可以分列成多行,若这样,则包括第一个参数在内的每个参数都必须单独成行,并且结束括号以及方法开始花括号必须写在同一行,中间用一个空格分隔;
- 小写驼峰,如
例:
class ClassName
{
private $name ='lisi';
public function aVeryLongMethodName(
ClassTypeHint $arg1,
&$arg2,
array $arg3 = []
) {
// 方法的内容
}
}
修饰符的使用
abstract
或final
声明时,必须写在访问修饰符前;static
必须写在其后;
例:
abstract class ClassName
{
protected static $foo;
abstract protected function zim();
final public static function bar()
{
// method body
}
}
方法和函数的调用
- 方法及函数调用时,方法名或函数名与参数左括号之间一定不可有空格,参数右括号前也一定不可有空格。每个参数前一定不可有空格,但其后 必须 有一个空格。
- 参数可以分列成多行,此时包括第一个参数在内的每个参数都必须单独成行;
例:
bar();
$foo->bar($arg1);
Foo::bar($arg2, $arg3);
$foo->bar(
$longArgument,
$longerArgument,
$muchLongerArgument
);
控制结构
- 控制结构关键词后必须有一个空格;
- 左括号 ( 后一定不可有空格;
- 右括号 ) 前也一定不可有空格;
- 右括号 ) 与开始花括号 { 间必须有一个空格;
- 结构体主体必须要有一次缩进;
- 结束花括号 }必须在结构体主体后单独成行;
- 每个结构体的主体都必须被包含在成对的花括号之中,哪怕只有一条语句;
- 使用关键词
elseif
代替else if
; - if 断行:if 中条件过多,可每个条件一行,第一个条件需单独成行,boolean操作符要么全部放开头,要么全部结尾,不可混用;
switch
:case
语句 必须 相对switch
进行一次缩进,而break
语句以及case
内的其它语句都 必须 相对case
进行一次缩进;
例:
if ($expr1) {
// if body
} elseif ($expr2) {
// elseif body
} else {
// else body;
}
if (
$expr1
&& $expr2
) {
// if body
} elseif (
$expr3
&& $expr4
) {
// elseif body
}
switch ($expr) {
case 0:
echo 'First case, with a break';
break;
case 1:
echo 'Second case, which falls through';
// no break
case 2:
case 3:
case 4:
echo 'Third case, return instead of break';
return;
default:
echo 'Default case';
break;
}
while ($expr) {
// structure body
}
for ($i = 0; $i < 10; $i++) {
// for body
}
foreach ($iterable as $key => $value) {
// foreach body
}
try {
// try body
} catch (FirstExceptionType $e) {
// catch body
} catch (OtherExceptionType $e) {
// catch body
}
花括号的使用
- 类和方法:起始和结束花括号必须单独一行,且起始花括号前后不能有空行;
- 流程控制语句:起始花括号不单独成行,结束花括号单独成行;
- 任何右打括号
}
后面不可跟注释或其它语句;
例:
class Foo extends Bar implements FooInterface
{
public function sampleFunction($a, $b = null)
{
if ($a === $b) {
bar();
} elseif ($a > $b) {
$foo->bar($arg1);
} else {
BazClass::bar($arg2, $arg3);
}
}
}
运算符
- 所有的二元和三元运算符的前后必须各有一个空格;
- 一元运算符
!
后面不可有空格;
例:
if ($a === $b) {
$foo = $bar ?? $a ?? $b;
} elseif ($a > $b) {
$variable = $foo ? 'foo' : 'bar';
}
闭包
- 闭包声明时,关键词
function
后以及关键词use
的前后都必须要有一个空格; - 开始花括号必须写在声明的同一行,结束花括号必须紧跟主体结束的下一行;
- 参数列表和变量列表的左括号后以及右括号前,一定不可有空格;
- 参数和变量列表中,逗号前一定不可有空格,而逗号后必须要有空格;
- 参数列表以及变量列表 可以 分成多行,这样,包括第一个在内的每个参数或变量都 必须 单独成行,而列表的右括号与闭包的开始花括号 必须 放在同一行;
例:
$closureWithArgs = function ($arg1, $arg2) {
// body
};
$closureWithArgsAndVars = function ($arg1, $arg2) use ($var1, $var2) {
// body
};
$noArgs_longVars = function () use (
$longVar1,
$longerVar2,
$muchLongerVar3
) {
// body
};
$longArgs_longVars = function (
$longArgument,
$longerArgument,
$muchLongerArgument
) use (
$longVar1,
$longerVar2,
$muchLongerVar3
) {
// body
};
$foo->bar(
$arg1,
function ($arg2) use ($var1) {
// body
},
$arg3
);
代码注释
- 类、方法、函数必须写注释;
- 类、方法必须使用块级注释,代码段视情况使用块级或行内注释;
- 注释应当包括功能说明、参数列表、返回类型、异常抛出情况;
- 注释文本和 // 之间有且只有一个空格;
- 比较复杂的代码段应当编写合适的注释;
- 不要写不必要的注释,比如下面的注释就是多余的:
// 如果用户存在
if ($user) {
// do something...
}
程序设计篇
注:本规范没有考虑历史项目现状,历史项目可能在某些地方并不符合,可根据实际情况决定是否遵守。
异常
- 异常的定义:凡是导致流程无法正常进行下去的,或者没有获取到预期结果的,都属于异常,例如除数的值是 0,获取用户信息接口没有查到用户;
- 代码中的异常应当抛出,而不应当以错误码的形式返回(除了最外层如控制器层,这层需要将异常转换成合适的格式输出给用户或日志。抛出异常而不是返回错误码遵循的原则是:业务逻辑和错误处理(非业务逻辑)分离,处理业务逻辑的代码只需将异常抛出(告诉上层),上层可以处理该异常,也可以不处理(直接再给上层));
- 异常应当包含明确的错误码和异常描述,其中错误码应当以常量的形式在项目中统一定义,而不应当以直接数字的形式写死(可读性、可维护性);
- 控制器层必须捕获并以合适的方式处理异常,不能继续向上抛出。处理方式包括但不限于返回合适的错误码、记录日志、发告警通知等;
状态码/错误码
- 不应当在程序中直接写数字状态码,而应当在项目中统一的地方定义状态码常量(或类常量);
- 状态码常量应当符合命名一节的规范描述;
- 不应当在非控制器层返回状态码,而应当以相应的异常代替(相应地,状态码体现在异常实例的 Code 上);
- 不应当使用通用状态码,每种错误应当定义自己的、唯一的状态码;
- 状态码应该在项目级别进行规划,不同的项目允许状态码重复,项目内部不允许不同的状态描述使用同一个状态码,反之,也不允许同一个状态描述使用不同的状态码;
日志
- 原则上应当只在应用层(如应用层服务、控制器等)记录日志,尽量避免在领域层(业务逻辑层)记录日志,但该原则不做绝对要求;
- 日志内容包括但不限于:请求编号、请求详细内容、响应内容、错误发生的平台、错误描述、调用栈;
- 原则上所有的异常都应当有日志可追踪;
- 建议对所有的外部请求以及本系统对外的 API 调用都做日志记录,用于出现异常情况时排查问题;
- 日志的实现应当遵循 PSR-3 日志接口规范;
缓存
- 应当为 js、html、css、image 等静态资源设置使用前端浏览器缓存(配置 nginx 或其他 Web 服务器);
- 应当对 js、html、css 资源开启压缩功能(配置 nginx 或其他 Web 服务器);
- 应当对经常访问但较少修改的数据使用内存缓存如 Redis、Memcache;
- 缓存的数据更新后应当及时更新/失效缓存;
- 应当只缓存热数据,且设置合适的缓存期限。后端缓存建议过期时间不超过7天;
- 不应当缓存大体量但并非全部热数据的数据;
- 后端缓存的实现应当遵循 PSR-16 缓存接口规范;
数据库
- 数据表字段原则上必须添加注释,除非像 id、is_deleted 等大众皆知的字段;
- 表字段不可多义(一个字段表达多个业务含义,例如“用户登录表”用 user_id 是否为空表示用户是否登录,这里 user_id 表达了两层含义:用户标识和登录态。但需要区分的是,“多义”和“多值”是不同的,如用 status 字段通过多值与运算来存储多个状态,这里 status 的含义仍然是明确的);
- 数据表的设计应该是“直白”的,不应当在字段上强加隐含的业务逻辑。例如上面的通过 user_id 是否为空来表示用户是否登录,就存在隐含业务逻辑,导致表结构的不稳定性(因为此时底层的存储结构依赖于上层的业务逻辑,而上层一般总是比底层不稳定);
- 使用字符串存储 json 时必须仔细考虑其中字段是否可能会被检索,如果需要检索,则这种设计会带来麻烦;
- 必须根据业务情况为表创建合适的索引,即使当前数据量不大(必须用动态眼光看待当前的情况,当前量不大不代表以后不大);
- 原则上禁止在一次请求中对同一条数据先写后读,防止读写分离下数据不一致。如果必须这样做,建议在写入后 sleep 1-2 秒再读;
- 不应使用
*
查询数据库字段,应当明确字段; - 连表查询:四个表以上的关联需要慎重,且需要经过所在团队 2 个以上成员的审核;
- 禁止直接操作非本系统/项目的数据库,必须调用相关接口,例如禁止在微信端直接操作券系统的数据库;
- 表字段:类似于
last_update_time
这样的字段必须设置on update current_timestamp
保证更新性; - 禁止在数据库事务中进行远程调用,这样会导致长事物,高并发下可能会导致数据库崩溃。解决方案:要么去掉事务,要么把远程调用拿到事务外面;
控制器
- 禁止在 Controller 中使用静态变量、静态方法。(完全没有必要,且在 easySwoole 等框架中容易出问题);
- 禁止在基类
Controller
中写 Action,即基类Controller
不能对外提供 API(否则任何子类都拥有该 API,后面无法知道外界实际上到底访问了哪些控制器的该 API); - 基类控制器只能提供一些便捷属性和内部便捷方法,以及一些前后置处理逻辑,这些属性和方法都应当是 protected 级别的;
- 禁止在控制器中写大量业务逻辑,应将其放入逻辑层,保持控制器层的简单;
Session
- Session 应当仅仅存放“会话”信息,即会话上下文中必须使用的(公共)信息,其他信息应当用缓存存储。例如:商户平台登录者基本信息、所拥有的权限集、当前所在的层(集团、油站组、油站)等更登录会话密切相关的、公共的信息;
- 不应当在领域层(业务逻辑)中直接使用 $_SESSION,而应当通过传参提供方法需要的东西。换句话说,只应当在应用层(如控制器)中使用 Session,防止 Session 污染;
- Session 的添加、修改应当在统一的地方进行,一般如登录成功后、退出登录、切换商户层级等,禁止在业务代码中随意设置 Session;
API 接口
- 对外的 API 接口必须有同步的、详细的文档,目前接口文档统一写在 showdoc 上面;
- API 接口的更新必须保证向前兼容性(除非能够确定调用方且能够相互协商修改);
- 写型API(添加、更新、删除)必须保证多次调用的幂等性(如多次调用不会导致重复添加多条数据),方便失败重试和手工补偿;
- API 返回的数据结构必须保证一致性,包括字段、结构一致性和数据类型一致性。如不可在某种情况下缺少某个字段,不同情况下某个字段类型不一致等;
- 所有的列表请求都必须支持分页,除非理论上不可能超过 50 条数据;
其它
- 不应在业务逻辑层写非本业务领域代码,而应当将其抽离成基础设施、本地服务或第三方接口(远程服务)。如虽然发送短信验证码属于用户注册流程的一环节,但发送短信验证码本身的逻辑不属于用户注册的业务领域,应将其抽离;
- 禁止大段代码拷贝,应重构成方法或类;
- 一个方法或函数不应超过 120 行,一个类不应超过 800 行;
- 谨慎使用静态方法,因为从单元测试的角度一般认为静态方法不具有可测试性;
- 查询型方法不应产生副作用(修改系统状态、数据库记录、插入数据等),只能返回相关数据(即保证查询方法的只读性);
- 业务模型不应直接依赖于 GET、POST 等传入的参数,即不应将外界传入的参数直接丢给业务模型(甚至是直接插入数据库),业务模型应当显式定义自己需要的参数;
- 函数、方法参数的设计:
- 方法的参数应当拥有自解释的能力,即每个参数拥有明确的含义;
- 优先采用具有明确含义的多参数传递策略。如果参数数量过多,可采用传对象(DTO)的方式。尽量不要直接传递数组,因为数组元素不具有自解释性和约束性,不可维护,是下下策。
- 例:用户登录校验传参:
- 推荐:
$login->verify($username, $password);
多参数传参,具有自解释性; - 如果参数过多(如超过 7 个),采用传对象方式:
$login->verify(LoginDTO $loginDTO);
因为对象具有明确的定义,也具有解释性; - 下下策:
$login->verify($params);
谁都不知道这个 $params 里面到底有什么; - 最下下策:
$login->verify($request->params())
,直接将浏览器输入一股脑全部丢进去,你让后人如何维护?
- 推荐: