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()),直接將瀏覽器輸入一股腦全部丟進去,你讓後人如何維護?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章