PHP 开发规范:实战篇

本规范基于 PSR 和实际项目经验整理而成,目前已在公司内部推行使用,特分享如下。

分为编码格式篇程序设计篇两大部分。

编码格式篇

基于 PSR-1、PSR-2、PSR-12 。

样例

<?php

/**
 * this is a example class
 */

declare(strict_types=1);

namespace Vendor\Package;

use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
use Vendor\Package\SomeNamespace\ClassD as D;

use function Vendor\Package\{functionA, functionB, functionC};

use const Vendor\Package\{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 中的问题请自行百度);
  • <?phpdeclarenamespaceuse必须按照顺序编写,并且后面必须跟一个空行;
  • 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 = []
	    	) {
	        	// 方法的内容
	   	 }
	}

修饰符的使用

  • abstractfinal 声明时,必须写在访问修饰符前;
  • 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()),直接将浏览器输入一股脑全部丢进去,你让后人如何维护?
posted @ 2020-04-26 14:39  林子er  阅读(506)  评论(0编辑  收藏  举报