本規範基於 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 中的問題請自行百度);
<?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())
,直接將瀏覽器輸入一股腦全部丟進去,你讓後人如何維護?
- 推薦: