Manual:代码编写约定/PHP
此页面记载MediaWiki的开发指引,由开发人员随着时间推移达成共识,精心制作而来(或有些时候由开发人员的领导公告而来) |
本頁說明在MediaWiki程式碼庫中以PHP編寫的檔案所採用的編碼規範。 另請參閱適用於包括PHP在內的所有程式語言的通用規範。 若您需要一份簡短的檢查清單來協助檢視您的提交內容,不妨嘗試使用 Pre-commit checklist。
大多數程式碼的樣式規則可透過PHP_CodeSniffer(亦稱PHPCS)自動修正、或至少能被偵測到,此機制採用MediaWiki 的自訂規則集。 更多信息请参见持续集成/PHP代码筛选。
代码结构
空格
MediaWiki傾向使用間距較大的樣式以實現最佳可讀性。
縮排請用tab、不要用空格。 限制每行字數為120個字元(假設tab寬度為4個字元)。
請在每個二元運算符的左右兩側放置空格,例如:
// 錯的:
$a=$b+$c;
// 對的:
$a = $b + $c;
除非括號內是空的,否則請將空格放進在括號內的兩旁。函式名稱後面請勿放置空格。
$a = getFoo( $b );
$c = getBar();
請在函式回傳型別提示中的:後面加上一個空格,但別加在前面:
function square( int $x ): int {
return $x * $x;
}
宣告陣列時,除非陣列是空的,否則請在方括號中放置一些空格。存取陣列的元素時,請勿在方括號中放置空格。
// 對的
$a = [ 'foo', 'bar' ];
$x = [];
// 錯的
$a = ['foo', 'bar'];
$x = [ ];
Spaces in brackets when accessing array elements are optional. Choose based on what you find readable or consistent, at the author's discretion. (T249872)
// OK
$a = $list[0];
$b = $list[$i];
$c = $list[$this->index];
// OK
$a = $list[ 0 ];
$b = $list[ $this->index + 1 ];
$c = $list[ $this->next( $this->index ) ];
諸如if、while、for、foreach、switch等控制結構、以及關鍵字catch之後應跟隨一個空格:
// 對的
if ( isFoo() ) {
$a = 'foo';
}
// 錯的
if( isFoo() ) {
$a = 'foo';
}
型別轉換時,請勿在轉換運算符之內或之後使用空格:
// 對的
(int)$foo;
// 錯的
(int) $bar;
( int )$bar;
( int ) $bar;
在註解中,在#或//字符與註釋之間應有一個空格。
// 對的:正確的內嵌註解
// 錯的:少了空格
/***** 切勿以這種方式發表評論 ***/
三元運算符
如果表達式很短且一目了然,則三元運算符使用起來可以有利:
$title = $page ? $page->getTitle() : Title::newMainPage();
但若您正考慮在多行的表達式中使用三元運算子,請考慮改用 if () 區塊來替代。
切記,磁碟的空間不值錢,但程式碼的可讀性至關重要,「if」是英文,而「?:」不是。
若使用多行的三元運算式,問號與冒號應置於第二行與第三行的開頭,而非第一行與第二行的結尾(此與MediaWiki的JavaScript的慣例相反)。
由於MediaWiki需要PHP 8.2.0版或更高版本,因此使用在PHP 5.3版中引入的短版三元運算子(?:),亦稱為貓王運算子是被允許的。
自PHP 7.0版起,空值合併運算子亦已可用,並可在某些使用情境中取代三元運算子。
例如,與其寫
$wiki = isset( $this->mParams['wiki'] ) ? $this->mParams['wiki'] : false;
您可以改寫為:
$wiki = $this->mParams['wiki'] ?? false;
字符串字面值(string literals)
在所有單引號與雙引號具有相同效果的情況下,則應優先使用單引號。
使用單引號的程式碼較不易出錯且更易於檢閱,因為它不會不小心包含了跳脫序列(Escape Sequences)或變數。
例如,正則表達式 "/\\n+/" 需要額外添加反斜線,這使得它比 '/\n+/' 稍顯混淆且更容易出錯。此外,對於使用美式/英式QWERTY鍵盤的人們而言,他們打字更容易,因為單引號無需按Shift鍵。
然而,不需害怕使用PHP的雙引號字串安插(string interpolation)功能:
$elementId = "myextension-$index";
此方法的效能表現略優於使用串接(點)運算子的等效寫法,且視覺上也加更為美觀。
Heredoc風格的字串有時相當實用:
$s = <<<EOT
<div class="mw-some-class">
$boxContents
</div>
EOT;
某些作者喜歡使用END作為結束权标,而這同時也是某個PHP函數的名稱。
函數和參數
請避免將大量參數傳遞給函數或构造函数:
// Block.php從1.17版到1.26版的构造函数。 千萬別這樣做!
function __construct( $address = '', $user = 0, $by = 0, $reason = '',
$timestamp = 0, $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0,
$hideName = 0, $blockEmail = 0, $allowUsertalk = 0
) {
...
}
參數的順序很快就會被你忘得一乾二淨,最終你勢必得硬编码所有給呼叫端的預設值,只為能在清單的末尾自訂一個參數。 若您正想以這種方式編寫函式,不妨改使用具名參數的關聯陣列來傳遞參數。
一般而言,不建議在函式中使用布林參數。
在$object->getSomething( $input, true, true, false )中,若不查閱MyClass::getSomething()的文檔,便無從得知這些參數的具體含義。
較好的做法是使用類別常數、然後製作一個泛用型旗標參數:
$myResult = MyClass::getSomething( $input, MyClass::FROM_DB | MyClass::PUBLIC_ONLY );
或是讓你的函式能接受一個具名參數的陣列:
$myResult = MyClass::getSomething( $input, [ 'fromDB', 'publicOnly' ] );
請盡量避免在函式執行過程中重新賦予變數新用途,並避免修改傳遞給函式的參數(除非參數是以傳遞參考的方式,而這很顯然正是該函數的全部意義之所在)。
賦值表達式
使用賦值作為表達式出乎讀者的意料之外、且看起來像是個錯誤。 請不要寫像這樣的代碼:
if ( $a = foo() ) {
bar();
}
空格不用錢、而且你打字快,所以請改用:
$a = foo();
if ( $a ) {
bar();
}
在 while() 子句中使用賦值曾是合理的,用於迭代:
$res = $dbr->query( 'SELECT foo, bar FROM some_table' );
while ( $row = $dbr->fetchObject( $res ) ) {
showRow( $row );
}
在新的程式碼中已無需如此;請改用:
$res = $dbr->query( 'SELECT foo, bar FROM some_table' );
foreach ( $res as $row ) {
showRow( $row );
}
向C語言取經
PHP語言是由一群熱愛C語言的人所設計,他們渴望將C語言的特色元素融入PHP之中。 但PHP存在一些與C語言重大的差異。
在C語言中,常數是預處理器以巨集的形式實現,因此執行速度快。 在PHP語言中,常數是透過在執行時對常數名稱進行哈希表的查找來實現的,其效能比直接使用字串常值來得慢。 在大多數你會在C語言中使用枚举型別或類似枚举型別的巨集的地方,您都可以在PHP中使用字串常值。
PHP擁有三個特殊的字串常值,在語言層面中(自PHP 5.1.3起)是不區分大小寫,但依循我們的慣例,這些字串常值始終採用小寫形式:true、false、及null。
請使用elseif而不是else if。
它們略有不同的含義:
// 這個:
if ( $foo === 'bar' ) {
echo 'Hello world';
} else if ( $foo === 'Bar' ) {
echo 'Hello world';
} else if ( $baz === $foo ) {
echo 'Hello baz';
} else {
echo 'Eh?';
}
// 實際上等效於這個:
if ( $foo === 'bar' ) {
echo 'Hello world';
} else {
if ( $foo == 'Bar' ) {
echo 'Hello world';
} else {
if ( $baz == $foo ) {
echo 'Hello baz';
} else {
echo 'Eh?';
}
}
}
而後者性能較差。
控制結構的替代語法
PHP提供了一種控制結構的替代語法,其使用冒號和一些關鍵字(例如endif、endwhile等):
if ( $foo == $bar ):
echo "<div>Hello world</div>";
endif;
應避免使用此語法,因為它會導致許多文字編輯器無法自動匹配和收合大括號。 應改用大括號:
if ( $foo == $bar ) {
echo "<div>Hello world</div>";
}
括號的置放
請參閱代码编写约定#缩进与对齐
針對匿名函式的應用時機,當匿名函式僅包含一行時,請優先採用箭頭函式。 箭頭函式比一般的匿名函式更簡潔易讀,並巧妙地避開了單行匿名函式可能引發的格式問題。[1]
在函式的參數中的型別宣告
請在可適用的情況下使用原生的型別宣告和回傳型別宣告。 (但請參閱下文#切勿為「龐大」的既有類別增加型別宣告。)
自 MediaWiki 1.35 起,在切換至PHP 7.2後,允許使用純量型別提示。 (T231710).
使用PHP 7.1語法來處理可空參數:請選擇
public function foo ( ?MyClass $mc ) {}
而不要選擇
public function foo ( MyClass $mc = null ) {}
前者精確地傳達了參數的可空性、同時避免了任何與非必填參數之間的歧義的風險。 整合開發環境(IDE)與靜態分析工具亦會識別此特性,當有個非可空參數緊接在某個可空參數之後時,將不會發出警告。
請勿添加僅重複原生型別的 PHPDoc 註解。
若需使用 PHPDoc 註解來描述無法透過原生型別表達的型別(例如string[],其原生型別為array),或當註解能提供超越型別與參數/函式名稱所揭示的實用資訊時,則應添加PHPDoc註解。[2]
命名
現有程式碼庫中,駝峰式大小寫(CamelCase)對於首字母縮略字(如HTML或XML)與對於縮寫(如DB代表「資料庫」、ID代表「識別碼」)的處理方式不一致。
建議新程式碼以單一詞彙對待首字母縮略字與縮寫以提升可讀性,例如:getHtmlId, HtmlCorsHeader(而非 getHTMLID, HTMLCORSHeader)。
| 文件 | UpperCamelCase | PHP 檔案名稱應採用其所包含類別的命名規則,即大駝峰式大小寫(UpperCamelCase)。 例如,WebRequest.php包含有WebRequest類別。 参见手册:代码编写约定#檔案命名。
|
|---|---|---|
| 命名空间 | UpperCamelCase | |
| 类 | UpperCamelCase | 命名類別時請使用大駝峰式大小寫。 例如: class ImportantClass
|
| 常量 | Uppercase with underscores | 使用大寫字母搭配下划线作為全域變數與類別常數:DB_PRIMARY, IDBAccessObject::READ_LATEST。
|
| 函式 | lowerCamelCase | 命名函式時請使用小駝峰式大小寫。 例如:
private function doSomething( $userPrefs, $editSummary )
|
| 函式的變數 | lowerCamelCase | 命名函式的變數時請使用小駝峰式大小寫。 請避免在變數名稱中使用下划线。 |
字首
另有一些字首可用在不同地方:
在函式的名稱之中
wf(wiki函式) – 是頂層的函式,例如function wfFuncname() { ... }
ef(擴充功能函式) = 擴充功能中的全域函式,儘管「在大多數情況下,現代風格會將勾點函數作為靜態方法置於類別之中,因此幾乎沒有或完全沒有原始的頂層函數會被如此命名。」(——brion在Manual_talk:Coding_conventions#ef_prefix_9510中如此說)
以動詞片語為首選:使用「getReturnText()」而非「returnText()」。
當公開這些函式供測試使用時,請依照穩定介面政策的規範將它們標記為 @internal。
濫用或非官方地依賴這些方法,比多數內部方法更為棘手,因此當它們在測試環境之外執行時,我們傾向讓它們拋出異常。
/**
* 重設範例的資料快取。
*
* @internal 僅供測試用
*/
public static function clearCacheForTest(): void {
if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
throw new RuntimeException( 'Not allowed outside tests' );
}
self::$exampleDataCache = [];
}
在變數的名稱之中
$wg– 是全域變數,例如$wgTitle。 請始終以此方式建立新的全域變數,以便可以輕易看到遺漏的「global $wgFoo」宣告。 在擴充功能中,擴充功能的名稱應作為命名空間的分隔符使用。 例如:$wgAbuseFilterConditionLimit,而不是$wgConditionLimit。- 全域宣告應置於函式開頭,如此便無需看過整個函式即可確定其依赖關係。
通常會使用 Database 類別的實例進行操作;我們為這些實例制定了命名規範,其有助於追蹤我們所連接伺服器的性質。
這在複製出來的環境中尤為重要,例如維基媒體及其他大型維基;而在開發的環境中,這兩種類型通常沒有區別,這可能掩蓋了細微的錯誤。
$dbw– 一個用於寫入(主要連線)的Database物件$dbr– 一個用於非並行敏感的讀取的Database物件(此可能為唯讀副本,狀態略落後於主節點,因此切勿嘗試透過此副本寫入資料庫,亦勿以此獲取權限與區塊狀態等重要查詢的「權威性」答案)
以下情況可能出現在舊版程式碼中,但不建議在新版程式碼中使用:
$ws– 對談變數,例如$_SESSION['wsSessionName']$wc– Cookie變數,例如$_COOKIE['wcCookieName']$wp– Post變數(透過資料欄提交),例如$wgRequest->getText( 'wpLoginName' )$m– 物件成員變數:$this->mPage。 此做法在新程式碼中不建議使用,但請盡量在同一個類別內保持一致性。
陷阱
empty()
empty() 函數應僅在您要抑制錯誤時使用。
其他時候只需使用!(布林轉換)。
empty( $var )essentially does!isset( $var ) || !$var.
常見使用情境:預設值為false的非必填布林配置鍵。$this->enableFoo = !empty( $options['foo'] );- 小心布林轉換陷阱。
- 它抑制了未定義的屬性與變數所引發的錯誤。 若只想要檢測未定義變數,請使用
!isset()。 若只想要檢測「空」值(例如:false、0、[]、等),請使用!。
isset()
切勿使用isset()來測試null。
在此情況下使用isset可能因掩蓋拼錯的變數名稱而引入錯誤。請改以$var === null替代。
測試某個不能為空但無預設值的型別化的屬性是否已初始化,是isset的一個有效用法,但會混淆PHP靜態分析工具Phan (T336665)。
您通常可透過使用?? / ??=來避免此情況。
布林轉換
if ( !$var ) {
…
}
- 請勿使用
!或empty來測試某個字串或陣列是否為空,因為PHP會將'0'視為假值(falsy)——但在MediaWiki中'0'是有效的標題和使用者名稱。 请使用=== ''或=== []代替。 - 請詳閱轉換為布林值的規則。在將字串轉換為布林值時請務必謹慎操作。
其他
- Array plus 不會重新編號那些以數值為索引的陣列的鍵,因此
[ 'a' ] + [ 'b' ] === [ 'a' ]. 若你要的是重新編號那些鍵,請使用 array_merge():array_merge( [ 'a' ], [ 'b' ] ) === [ 'a', 'b' ] - 請確保您已將 error_reporting() 設定為
-1。 這將通知您未定義的變數及其他細微陷阱,這些情況在標準 PHP 中會被忽略。 另請參閱 Manual:如何调试。 - 在純PHP檔案(例如非HTML模板)中工作時,請省略任何尾隨的
?>標籤。 這些標籤常會引發尾隨空白問題及「header已傳送」錯誤訊息(參見 bugzilla:17642 和 http://news.php.net/php.general/280796 )。 版本控制中,檔案在末尾加入換行符(編輯器可能會自動添加)是常見做法,此舉將觸發此錯誤。 - 請勿使用 5.3 版引入的 goto() 語法。 PHP或許引進了這項功能,但這並不意味著我們應該使用它。
- 除非「必須」如此,否則在使用
foreach遍歷陣列時,切勿以傳遞參考來傳遞參數。即使在必要時,也請注意其可能造成的後果。 (請參見 https://web.archive.org/web/20220924191559/https://www.intracto.com/en-be/blog/php-quirks-passing-an-array-by-reference/ 及 PHP手冊) - PHP允許您在類別的非靜態方法中宣告靜態變數。 這在某些情況下會導致隱晦的bug,因為變數在實例之間是共享的。 若您不會使用某個
private static屬性,同樣地,也請勿使用靜態變數。
等號運算子
請謹慎使用雙等號比較運算子。
三等號(===)通常更直觀,除非有理由使用雙等號(==),否則應優先採用三等號。
'000' == '0'是true(!)'000' === '0'是false- 若要檢查兩個應為數值的純量是否相等,請使用
==,例如5 == "5"為true。 - 若要檢查兩個變數是否皆為'string'類型且字元序列相同,請使用
===,例如"1.e6" === "1.0e6"為false。
請留意使用弱比較的內部函數與結構;例如,提供第三個參數給in_array、以及避免在switch結構中混用純量型別。
請勿使用尤達式條件式。
JSON數字型別的精度
JSON使用了JavaScript的型別系統,因此所有數字皆以64位元IEEE浮點數表示。 這意味著數字在變大的過程中會逐漸失去精確度,最終導致某些整數變得無法區分:超過2^52的數字其精確度將低於 ±0.5,因此一個龐大的整數最終可能轉變成為另一個不同的整數。 為避免此問題,請在JSON中將潛在性龐大的整數以字串形式表示。
君子有所為有所不為
請勿使用內建的序列化功能
PHP 的內建序列化機制(即 serialize() 和 unserialize() 函式)不該被用於儲存(或讀取)當前程序外部的資料。
請改採用基於JSON的序列化(然而,請注意潛在陷阱)。
此政策由RFCT161647所制定。
原因有二:(1)使用此機制序列化的資料無法以同一個類別的後續版本進行可靠地反序列化。 (2)經過精心謀劃後的序列化資料可被用於執行惡意程式碼,進而引發嚴重的安全風險。
有時,您的程式碼不會去控制序列化機制,而是會使用某些內部採用這個機制的函式庫或驅動程式。 在這種情況下,應採取措施減輕風險。 上述第一個問題可透過在序列化前將任何資料轉換為陣列或純粹的匿名物件來減輕。 第二個問題或許能透過PHP 7為反序列化所引入的白名單功能來緩解。
雖然對於簡單的類別而言,PHP的JsonSerializable介面可能已足夠,但在進行JSON序列化時,更複雜的實例很可能會發現wikimedia/json-codec套件相當實用。
它包含用於整合服務與依赖注入的機制,同時也能整合那些不原生支援序列化的外部類別。
核心中的JsonCodec服務擴展了wikimedia/json-codec提供的編解碼器。
切勿為「龐大」的既有類別增加型別宣告
MediaWiki包含了一些遲早會被拆分或被替換的龐大類別。
此件事會在過渡期間以維持程式碼相容性的方式進行,但可能導致預期在參數型別、回傳型別、屬性型別或類似情境中使用舊版類別的擴充功能程式碼失效。
例如,勾點處理函式的 $title 參數可能會傳遞某種 MockTitleCompat 類別,而非實際的 Title。
因此,如此龐大的既有類別不應在型別提示中使用,僅只在PHPDoc中使用。[3][4] 這些類別包括:
TitleArticleWikiPageUserMediaWikiOutputPageWebRequestEditPage
切勿為DOM類別增加型別宣告
PHP 8.4引入了一個全新的\Dom\Document類別,其與PHP<=8.3中所使用的舊版\DOMDocument類別並不完全相容。
位於wikimedia/parsoid中的Wikimedia\Parsoid\Utils\DOMCompat類別,其功能在於彌合兩種實作之間的差距,並為任一實作中所缺失的功能提供符合標準的實作方案。
建議採取以下任方式二擇一:將(允許在執行時傳遞 \Dom\Document 或 \DOMDocument 類別的)DOM類別的顯式類型聲明省略、或使用在類型提示中Parsoid所提供的Wikimedia\Parsoid\DOM\Document別名——該別名在PHP 8.4之前會解析為\DOMDocument、在PHP 8.4之後則解析為\Dom\Document。
註解與文檔
您的程式碼具備有完善的文檔記錄至關重要,如此其他開發人員與隐错修復者才能輕鬆地理解程式碼的邏輯架構。 新的類別、方法、及成員變數應包含有註解,提供其功能的簡要說明(除非功能是顯而易見),即使是私有成員變數亦然。 此外,所有新的類別方法都應記錄它的參數與返回值。
我們採用Doxygen文檔格式(其與我們所使用的子集PHPDoc非常類似),可自動從程式碼的註解生成文檔(參見 手册:mwdocgen.php)。
請以/**開頭來建立Doxygen註解區塊,而非採用Qt風格的/*!格式。
Doxygen的結構化指令是以@標籤名稱開頭。
(請使用@而非\作為跳脫字元——兩種格式皆可在Doxygen中運作,但為確保向後與向前相容性,MediaWiki已選擇採用@param格式。)
它們(使用@ingroup)將生成的文檔組織起來,且(使用@author標籤)識別作者。
它們描述一個函式或類別的方法、其(使用@param)接收的參數、以及函式(使用@return)的回傳值。參數的格式為:
@param 型別 $參數名稱 参数的描述
若參數可具有多種型別,請使用垂直線『|』字元將它們分隔,例如:
@param string|Language|bool $lang Language for the ToC title, defaults to user language
將註釋中的句子續寫於下一行,並額外多缩进一個空格。
對於每個您新增或變更的公開介面(方法、類別、變數、不管什麼),請提供一個 @since VERSION 標籤,讓那些透過此介面擴展程式碼的人員知悉其行為將破壞與舊版程式碼的相容性。
class Foo {
/**
* @var array 請在此處描述
* @example [ 'foo' => Bar, 'quux' => Bar, .. ]
*/
protected $bar;
/**
* 請在此處描述,隨後為參數的文檔說明。
*
* 一些例子:
* @code
* ...
* @endcode
*
* @since 1.24
* @param FooContext $context 解碼Foos的上下文
* @param array|string $options 可選擇性地傳遞額外的選項。
* 一個字串、或一個字串陣列。
* @return Foo|null Foo的新實例,若quuxification失敗則返回null。
*/
public function makeQuuxificatedFoo( FooContext $context = null, $options = [] ) {
/* .. */
}
}
FIXME通常表示某處存在問題或故障。TODO表示需要改進之處;但並不必然意味著加入此註解的人會去做。HACK則表示針對當前問題採取了快速但不夠優雅、笨拙、或次優的解決方案,最終應對該段程式碼進行更徹底的重寫。
原始碼檔案標頭
為符合多數授權條款的要求,您應在每個原始碼檔案的標頭加入類似以下內容(適用GPLv2授權的應用程式):
<?php
/**
* @license GPL-2.0-or-later
* @file
*/
Doxygen標籤
我們使用下列Doxygen可識別的註解。為保持一致性,請按此階層使用:
檔案層級:
- @file
類別、類別成員、或全域成員的層級:
- @todo
- @var
- @stable, @newable, @deprecated, @internal, @unstable, @private
- @see
- @since
- @ingroup
- @param
- @return
- @throws
- @author
- @copyright
測試註解
在測試時,我們使用以下的註解等等。這些註解不僅是文檔說明,它們對 PHPUnit 具有特定含義,並會影響測試的執行。
- @depends
- @group
- @covers
- @dataProvider
整合
MediaWiki的程式碼庫中有幾段程式碼,其設計初衷是作為獨立模組、且可輕鬆移植至其他應用程式。
雖然其中某些現已作為獨立函式庫存在,但其餘部份仍保留於MediaWiki原始碼樹中(例如位於/includes/libs目錄內的檔案)。
除去這些不看,程式碼應該要整合至MediaWiki環境的其他部份,並應允許程式碼庫的其他區域與其相互整合。
可見性
除非有必要提高程式碼的可見性,否則將其標記為private。
不要將所有東西都設為protected(即對子類別公開)或public。
全域对象
請勿直接存取PHP的超全域變數$_GET、$_POST 等;請改用 $request->get*( 'param' )來存取;有多種函數可根據你所需值的類型來使用。您可從最近的 RequestContext 取得 WebRequest、或在絕對必要時使用 RequestContext::getMain()。
同樣地,請勿直接存取 $_SERVER;若需取得當前使用者的IP位址,請改用 $request->getIP()。
靜態方法
使用靜態方法的程式碼應設計為:所有類別內的方法的呼叫皆應採用晚期靜態綁定機制,此機制的核心意義在於,對可覆寫的靜態方法的呼叫將以與可覆寫的實例方法的呼叫相同的方式進行解析。具體而言:
- 在呼叫可能被子類別從類別內部覆寫的靜態方法時,請使用
static::func()。 這將呼叫在子類別(若存在的話)中定義的覆寫方法,就像是實例方法的$this->func()作用方式。 - 在呼叫可能未覆寫的靜態方法(特別是private方法)時,請使用
self::func()。 這僅會呼叫該類別所在的類別及其父類別的方法。 - 當從某個靜態方法的覆寫版本呼叫父類別的方法時,請使用
parent::func()。 - 若你感覺你還是需要呼叫靜態方法的祖類別版本、或子類別版本,請務必三思;若實在想不出更好的辦法,那就使用
forward_static_call()吧。
切勿將類別名稱寫成上述情況中 ClassName::func() 的形式,否則該方法內的所有方法呼叫都會忽略子類別對該類別成員的覆寫。[5] 這個問題僅對於靜態方法,它會在實例方法中如預期般地運作,但為避免對呼叫行為產生混淆,也請在實例方法中避開使用該語法。[6]
這些複雜情況實在令人討厭。最好避免使用靜態方法,你就不用煩惱這些了。
呼叫方法
為明確起見,方法呼叫的語法應符合方法的類型:
- 呼叫靜態方法時應始終使用
::,即使PHP偶爾可以讓你使用->。[7] - 呼叫實例方法時應始終使用
->,即使PHP偶爾可以讓你使用::。[8] (self::和parent::可在需要時使用。)
類別
請將程式碼封裝在物件導向的類別之中、或是新增功能到現有類別之中;切勿新增全域函式或全域變數。
請留意「後端」類別與「前端」類別的區別:前者代表資料庫中的實體(例如 User、Block、RevisionRecord 等等),後者則代表對使用者可見的頁面或介面(例如 SpecialPage、Article、ChangesList 等等)。
即使您的程式碼很明顯並非採用物件導向的設計,仍可將其置於靜態類別中(例如 IP 或 Html)。
由於一個PHP 4缺乏私有類別的成員與方法所遺留下來的歷史包袱,舊版程式碼會透過註解一個(例如/** @private */)標記來表明意圖;請將此視為由解譯器強制執行的規範予以尊重。
以適當的可見性修飾符標記新程式碼,必要時可包含public,但切勿在未經初步的檢查、測試、及必要重構的情況下,擅自為現有程式碼添加可見性修飾符。
除非您對函數所做的變更本就會破壞舊有的使用方式,否則通常應避免更改其可見性。
错误处理
一般而言,您不應該抑制PHP錯誤。正確的處理錯誤方法是「實質上處理錯誤」。
例如,若您正考慮使用某個錯誤抑制運算子來抑制無效的陣列索引警告,您應改為在嘗試存取陣列索引之前,先執行 isset 檢查。
在可能的情況下,「務必」捕捉PHP的錯誤、或是與生俱來地避免PHP的錯誤。
僅當您預期會出現無法避免的PHP警告時,才可使用PHP的@運算子。此情況適用於以下情形:
- # 即將發生的錯誤是不可能預料得到的;
- 且,您正計劃著在錯誤發生後以適當方式處理這些錯誤。
- You are planning on handling the error in an appropriate manner after it occurs.
我們使用PHPCS來警告避免使用at運算子。若您確實需要使用它,還需指示PHPCS進行例外處理,例如這樣:
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$content = @file_get_contents( $path );
一個典型使用範例是使用 fopen() 開啟檔案。您可嘗試呼叫 $2 和 $3 來預測錯誤,但與 $4 不同,此類檔案操作會增加顯著開銷並導致程式碼不穩定。例如,在檔案的檢查與實際 $5 呼叫之間,可能就已被刪除或修改(參見 TOC/TOU)。
You can try to predict the error by calling file_exists() and is_readable(), but unlike isset(), such file operations add significant overhead and make for unstable code.
For example, the file may be deleted or changed between the check and the actual fopen() call (see TOC/TOU).
在此情況下,請先編寫執行主要操作的程式碼。接著處理檔案開啟失敗的情境:使用 $1 運算子避免PHP輸出冗餘訊息,然後檢查隨後的結果。對於 $2 和 $3,需檢查是否返回布林值false,再執行備援方案或拋出一個例外。
Then handle the case of the file failing to open, by using the @ operator to prevent PHP from being noisy, and then check the result afterwards.
For fopen() and filemtime(), that means checking for a boolean false return, and then performing a fallback, or throw an exception.
AtEase
對於 PHP 5 及更早版本,MediaWiki 開發人員不建議使用@運算子,因其會導致未記錄且無法解釋的致命錯誤 (r39789)。
取而代之的是,我們採用了來自 at-ease 函式庫的自訂 AtEase::suppressWarnings() 和 AtEase::restoreWarnings() 方法。
原因在於at運算子會導致PHP在發生致命錯誤時不會提供錯誤的訊息或堆疊的追蹤。
雖然at運算子主要用於處理非致命錯誤(非例外或致命錯誤),但若是致命錯誤真的發生了,它將會造成極差的開發者體驗。
use Wikimedia\AtEase\AtEase;
AtEase::suppressWarnings();
$content = file_get_contents( $path );
AtEase::restoreWarnings();
在 PHP 7 中,例外處理程序已修正(範例),無論是否抑制錯誤,皆會始終提供包含堆疊追蹤的錯誤訊息。2020 年起,AtEase的使用開始逐步淘汰,恢復使用at運算子。(T253461)
例外處理
例外可分為已檢查型(意指呼叫方預期會去捕捉它們)或未檢查型(意指呼叫方不會去捕捉它們)。
未檢查的例外通常是用在程式設計的錯誤,例如傳遞無效的參數給某個函式。
這些例外情況通常應(直接或透過繼承給子類別)使用PHP標準函式庫的例外類別,且不得在文檔中使用@throws註解進行註記。然而,當這些例外是屬於類別方法的協定的一部份時(例如字串參數不得為空、或整數參數必須為非負值),其觸發的條件應以白話文的形式記錄於文檔註解中。
另一方面,已檢查的例外情況必須始終以@throws註解記錄於文檔中。
當呼叫某個能拋出已檢查的例外的方法時,該例外若不是被捕捉住、不然就是被記錄在呼叫者的文檔註解中。
已檢查的例外通常應使用專用的例外類別,該類別繼承自Exception。
建議不要將標準函式庫的例外作為已檢查例外的基底類別,如此便能透過靜態程式碼分析器強制執行例外類別的正確使用方式。
絕不可直接拋出基底類別Exception :請改用更具體的例外類別。
若你的意圖是要捕捉所有可能的例外的話,則它是可以用在catch語組中,但在此目的通常是使用Throwable較為正確。
MWException在老式的程式碼中,拋出這個類別或繼承這個類別,相對上是很常見的。
此類別在新建程式碼中應避免使用,因為它並未提供任何優勢、反而可能造成混淆 (T86704)。
在建立一個新的例外類別時,若例外訊息中包含有變數的部分,請考慮實作INormalizedException這個類別;若例外訊息是要顯示給使用者看的,則請實作ILocalizedException這個類別。
若你不確定該使用哪種例外類別,可丟出一個LogicException例外類別來指出代碼中的錯誤(例如錯誤的函式參數、或碰觸到某個不可碰觸的分支)、其餘情況(例如某個外部伺服器當機)則丟出一個RuntimeException例外類別。
另見
注释
- ↑ T154789 閉包(Closure)的格式難看死了
- ↑ [wikitech-l] 徵求意見回饋:PHPCS(PHP_CodeSniffer)在靜態型別世界中的應用
- ↑ phab:T240307#6191788
- ↑ phab:T354697
- ↑
class X { // 列印出呼叫該方法的類別名稱 protected static function f() { echo static::class; } // 測試當呼叫該方法時會發生什麼事 public static function test() { A::f(); B::f(); C::f(); } } class A extends X { // 未覆寫f()函式,此處將輸出「A」 } class B extends X { // 透過使用 `parent` 呼叫父類方法的覆寫方法,此處將輸出「B」 protected static function f() { parent::f(); } } class C extends X { // 透過使用 `X` 呼叫父類別方法的覆寫方法,此處將輸出「X」而非「C」 protected static function f() { X::f(); } } X::test();
- ↑
class X { // 列印出呼叫該方法的類別名稱 protected function f() { echo static::class; } // 測試當呼叫該方法時會發生什麼事 public static function test() { ( new A )->f(); ( new B )->f(); ( new C )->f(); } } class A extends X { // 未覆寫f()函式,此處將輸出「A」 } class B extends X { // 透過使用 `parent` 呼叫父類方法的覆寫方法,此處將輸出「B」 protected function f() { parent::f(); } } class C extends X { // 透過使用 `X` 呼叫父類別方法的覆寫方法,此處將輸出「C」 // (當該方法非靜態時)。 protected function f() { X::f(); // 不鼓勵 } } X::test();
- ↑
class A { static function f() { echo 'f'; } } $a = new A; A::f(); $a::f(); // 可行,但不鼓勵 $a->f(); // 可行,但不鼓勵
- ↑
class A { function f() { echo 'f'; } function __construct() { $this->f(); $this::f(); // 可行,但不鼓勵 static::f(); // 可行,但不鼓勵 A::f(); // 可行,但不鼓勵 } } $a = new A; A::f(); // 可在PHP 7中運作正常,但無法在PHP 8中運作