十個 PHP 開(kāi)發者最容易犯的錯誤

2021-07-13 查看(4599)

PHP 語言讓 WEB 端程序設計變得簡單,這也是它能流行起來的原因。但也是因爲它的簡單,PHP 也慢(màn)慢(màn)發展成一(yī)個相對複雜(zá)的語言,層出不窮的框架,各種語言特性和版本差異都時常讓搞的我(wǒ)們頭大(dà),不得不浪費(fèi)大(dà)量時間去(qù)調試。這篇文章列出了十個最容易出錯的地方,值得我(wǒ)們去(qù)注意。

易犯錯誤 #1: 在 foreach循環後留下(xià)數組的引用

還不清楚 PHP 中(zhōng) foreach 遍曆的工(gōng)作原理?如果你在想遍曆數組時操作數組中(zhōng)每個元素,在 foreach 循環中(zhōng)使用引用會十分(fēn)方便,例如

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
        $value = $value * 2;
}
// $arr 現在是 array(2, 4, 6, 8)複制代碼

   

問題是,如果你不注意的話(huà)這會導緻一(yī)些意想不到的負面作用。在上述例子,在代碼執行完以後,$value 仍保留在作用域内,并保留着對數組最後一(yī)個元素的引用。之後與 $value 相關的操作會無意中(zhōng)修改數組中(zhōng)最後一(yī)個元素的值。

你要記住 foreach 并不會産生(shēng)一(yī)個塊級作用域。因此,在上面例子中(zhōng) $value 是一(yī)個全局引用變量。在 foreach 遍曆中(zhōng),每一(yī)次叠代都會形成一(yī)個對 $arr 下(xià)一(yī)個元素的引用。當遍曆結束後, $value 會引用 $arr 的最後一(yī)個元素,并保留在作用域中(zhōng)

這種行爲會導緻一(yī)些不易發現的,令人困惑的bug,以下(xià)是一(yī)個例子

$array = [1, 2, 3];
echo implode(',', $array), "\n";

foreach ($array as &$value) {}    // 通過引用遍曆
echo implode(',', $array), "\n";

foreach ($array as $value) {}     // 通過賦值遍曆
echo implode(',', $array), "\n";複制代碼

   

以上代碼會輸出

1,2,3
1,2,3
1,2,2複制代碼

   

你沒有看錯,最後一(yī)行的最後一(yī)個值是 2 ,而不是 3 ,爲什麽?

在完成第一(yī)個 foreach 遍曆後, $array 并沒有改變,但是像上述解釋的那樣, $value 留下(xià)了一(yī)個對 $array 最後一(yī)個元素的危險的引用(因爲 foreach 通過引用獲得 $value

這導緻當運行到第二個 foreach ,這個"奇怪的東西"發生(shēng)了。當 $value 通過賦值獲得, foreach 按順序複制每個 $array 的元素到 $value 時,第二個 foreach 裏面的細節是這樣的

  • 第一(yī)步:複制 $array[0] (也就是 1 )到 $value$value 其實是 $array最後一(yī)個元素的引用,即 $array[2]),所以 $array[2] 現在等于 1。所以 $array 現在包含 [1, 2, 1]

  • 第二步:複制 $array[1](也就是 2 )到 $value$array[2] 的引用),所以 $array[2] 現在等于 2。所以 $array 現在包含 [1, 2, 2]

  • 第三步:複制 $array[2](現在等于 2 ) 到 $value$array[2] 的引用),所以 $array[2] 現在等于 2 。所以 $array 現在包含 [1, 2, 2]

爲了在 foreach 中(zhōng)方便的使用引用而免遭這種麻煩,請在 foreach 執行完畢後 unset() 掉這個保留着引用的變量。例如

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
}
unset($value);   // $value 不再引用 $arr[3]複制代碼

   

常見錯誤 #2: 誤解 isset() 的行爲

盡管名字叫 isset,但是 isset() 不僅會在變量不存在的時候返回 false,在變量值爲 null 的時候也會返回 false

這種行爲比最初出現的問題更爲棘手,同時也是一(yī)種常見的錯誤源。

看看下(xià)面的代碼:

$data = fetchRecordFromStorage($storage, $identifier);
if (!isset($data['keyShouldBeSet']) {
    // do something here if 'keyShouldBeSet' is not set
}複制代碼

   

開(kāi)發者想必是想确認 keyShouldBeSet 是否存在于 $data 中(zhōng)。然而,正如上面說的,如果 $data['keyShouldBeSet'] 存在并且值爲 null 的時候, isset($data['keyShouldBeSet']) 也會返回 false。所以上面的邏輯是不嚴謹的。

我(wǒ)們來看另外(wài)一(yī)個例子:

if ($_POST['active']) {
    $postData = extractSomething($_POST);
}

// ...

if (!isset($postData)) {
    echo 'post not active';
}複制代碼

   

上述代碼,通常認爲,假如 $_POST['active'] 返回 true,那麽 postData 必将存在,因此 isset($postData) 也将返回 true。反之, isset($postData) 返回 false 的唯一(yī)可能是 $_POST['active'] 也返回 false

然而事實并非如此!

如我(wǒ)所言,如果$postData 存在且被設置爲 nullisset($postData) 也會返回 false 。 也就是說,即使 $_POST['active'] 返回 true, isset($postData) 也可能會返回 false 。        再一(yī)次說明上面的邏輯不嚴謹。

順便一(yī)提,如果上面代碼的意圖真的是再次确認 $_POST['active'] 是否返回 true,依賴 isset() 來做,不管對于哪種場景來說都是一(yī)種糟糕的決定。更好的做法是再次檢查 $_POST['active'],即:

if ($_POST['active']) {
    $postData = extractSomething($_POST);
}

// ...

if ($_POST['active']) {
    echo 'post not active';
}複制代碼

   

對于這種情況,雖然檢查一(yī)個變量是否真的存在很重要(即:區分(fēn)一(yī)個變量是未被設置還是被設置爲 null);但是使用 array_key_exists() 這個函數卻是個更健壯的解決途徑。

比如,我(wǒ)們可以像下(xià)面這樣重寫上面第一(yī)個例子:

$data = fetchRecordFromStorage($storage, $identifier);
if (! array_key_exists('keyShouldBeSet', $data)) {
    // do this if 'keyShouldBeSet' isn't set
}複制代碼

   

另外(wài),通過結合 array_key_exists() 和 get_defined_vars(), 我(wǒ)們能更加可靠的判斷一(yī)個變量在當前作用域中(zhōng)是否存在:

if (array_key_exists('varShouldBeSet', get_defined_vars())) {
    // variable $varShouldBeSet exists in current scope
}複制代碼

   

常見錯誤 #3:關于通過引用返回與通過值返回的困惑

考慮下(xià)面的代碼片段:

class Config
{
    private $values = [];

    public function getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];複制代碼

   

如果你運行上面的代碼,将得到下(xià)面的輸出:

PHP Notice:  Undefined index: test in /path/to/my/script.php on line 21複制代碼

   

出了什麽問題?

上面代碼的問題在于沒有搞清楚通過引用與通過值返回數組的區别。除非你明确告訴 PHP 通過引用返回一(yī)個數組(例如,使用 &),否則 PHP 默認将會「通過值」返回這個數組。這意味着這個數組的一(yī)份拷貝将會被返回,因此被調函數與調用者所訪問的數組并不是同樣的數組實例。

所以上面對 getValues() 的調用将會返回 $values 數組的一(yī)份拷貝,而不是對它的引用。考慮到這一(yī)點,讓我(wǒ)們重新回顧一(yī)下(xià)以上例子中(zhōng)的兩個關鍵行:

// getValues() 返回了一(yī)個 $values 數組的拷貝
// 所以`test`元素被添加到了這個拷貝中(zhōng),而不是 $values 數組本身。
$config->getValues()['test'] = 'test';


// getValues() 又(yòu)返回了另一(yī)份 $values 數組的拷貝
// 且這份拷貝中(zhōng)并不包含一(yī)個`test`元素(這就是爲什麽我(wǒ)們會得到 「未定義索引」 消息)。
echo $config->getValues()['test'];複制代碼

   

一(yī)個可能的修改方法是存儲第一(yī)次通過 getValues() 返回的 $values 數組拷貝,然後後續操作都在那份拷貝上進行;例如:

$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];複制代碼

   

這段代碼将會正常工(gōng)作(例如,它将會輸出test而不會産生(shēng)任何「未定義索引」消息),但是這個方法可能并不能滿足你的需求。特别是上面的代碼并不會修改原始的$values數組。如果你想要修改原始的數組(例如添加一(yī)個test元素),就需要修改getValues()函數,讓它返回一(yī)個$values數組自身的引用。通過在函數名前面添加一(yī)個&來說明這個函數将返回一(yī)個引用;例如:

class Config
{
    private $values = [];

    // 返回一(yī)個 $values 數組的引用
    public function &getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];複制代碼

   

這會輸出期待的test

但是現在讓事情更困惑一(yī)些,請考慮下(xià)面的代碼片段:

class Config
{
    private $values;

    // 使用數組對象而不是數組
    public function __construct() {
        $this->values = new ArrayObject();
    }

    public function getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];複制代碼

   

如果你認爲這段代碼會導緻與之前的數組例子一(yī)樣的「未定義索引」錯誤,那就錯了。實際上,這段代碼将會正常運行。原因是,與數組不同,PHP 永遠會将對象按引用傳遞。(ArrayObject 是一(yī)個 SPL 對象,它完全模仿數組的用法,但是卻是以對象來工(gōng)作。)

像以上例子說明的,你應該以引用還是拷貝來處理通常不是很明顯就能看出來。因此,理解這些默認的行爲(例如,變量和數組以值傳遞;對象以引用傳遞)并且仔細查看你将要調用的函數 API 文檔,看看它是返回一(yī)個值,數組的拷貝,數組的引用或是對象的引用是必要的。

盡管如此,我(wǒ)們要認識到應該盡量避免返回一(yī)個數組或 ArrayObject,因爲這會讓調用者能夠修改實例對象的私有數據。這就破壞了對象的封裝性。所以最好的方式是使用傳統的「getters」和「setters」,例如:

class Config
{
    private $values = [];

    public function setValue($key, $value) {
        $this->values[$key] = $value;
    }

    public function getValue($key) {
        return $this->values[$key];
    }
}

$config = new Config();

$config->setValue('testKey', 'testValue');
echo $config->getValue('testKey');    // 輸出『testValue』複制代碼

   

這個方法讓調用者可以在不對私有的$values數組本身進行公開(kāi)訪問的情況下(xià)設置或者獲取數組中(zhōng)的任意值。

常見的錯誤 #4:在循環中(zhōng)執行查詢

如果像這樣的話(huà),一(yī)定不難見到你的 PHP 無法正常工(gōng)作。

$models = [];

foreach ($inputValues as $inputValue) {
    $models[] = $valueRepository->findByValue($inputValue);
}複制代碼

   

這裏也許沒有真正的錯誤, 但是如果你跟随着代碼的邏輯走下(xià)去(qù), 你也許會發現這個看似無害的調用$valueRepository->findByValue() 最終執行了這樣一(yī)種查詢,例如:

$result = $connection->query(" `x`,`y` FROM `values` WHERE `value`=" . $inputValue);複制代碼

   

結果每輪循環都會産生(shēng)一(yī)次對數據庫的查詢。 因此,假如你爲這個循環提供了一(yī)個包含 1000 個值的數組,它會對資(zī)源産生(shēng) 1000 單獨的請求!如果這樣的腳本在多個線程中(zhōng)被調用,他會有導緻系統崩潰的潛在危險。

因此,至關重要的是,當你的代碼要進行查詢時,應該盡可能的收集需要用到的值,然後在一(yī)個查詢中(zhōng)獲取所有結果。

一(yī)個我(wǒ)們平時常常能見到查詢效率低下(xià)的地方 (例如:在循環中(zhōng))是使用一(yī)個數組中(zhōng)的值 (比如說很多的 ID )向表發起請求。檢索每一(yī)個 ID 的所有的數據,代碼将會叠代這個數組,每個 ID 進行一(yī)次SQL查詢請求,它看起來常常是這樣:

$data = [];
foreach ($ids as $id) {
    $result = $connection->query(" `x`, `y` FROM `values` WHERE `id` = " . $id);
    $data[] = $result->fetch_row();
}複制代碼

   

但是 隻用一(yī)條 SQL 查詢語句就可以更高效的完成相同的工(gōng)作,比如像下(xià)面這樣:

$data = [];
if (count($ids)) {
    $result = $connection->query(" `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids));
    while ($row = $result->fetch_row()) {
        $data[] = $row;
    }
}複制代碼

   

因此在你的代碼直接或間接進行查詢請求時,一(yī)定要認出這種查詢。盡可能的通過一(yī)次查詢得到想要的結果。然而,依然要小(xiǎo)心謹慎,不然就可能會出現下(xià)面我(wǒ)們要講的另一(yī)個易犯的錯誤...

常見問題 #5: 内存使用欺騙與低效

一(yī)次取多條記錄肯定是比一(yī)條條的取高效,但是當我(wǒ)們使用 PHP 的 mysql 擴展的時候,這也可能成爲一(yī)個導緻 libmysqlclient 出現『内存不足』(out of memory)的條件。

我(wǒ)們在一(yī)個測試盒裏演示一(yī)下(xià),該測試盒的環境是:有限的内存(512MB RAM),MySQL,和 php-cli

我(wǒ)們将像下(xià)面這樣引導一(yī)個數據表:

// 連接 mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');

// 創建 400 個字段
$query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT';
for ($col = 0; $col < 400; $col++) {
    $query .= ", `col$col` CHAR(10) NOT NULL";
}
$query .= ');';
$connection->query($query);

// 寫入 2 百萬行數據
for ($row = 0; $row < 2000000; $row++) {
    $query = "  `test` VALUES ($row";
    for ($col = 0; $col < 400; $col++) {
        $query .= ', ' . mt_rand(1000000000, 9999999999);
    }
    $query .= ')';
    $connection->query($query);
}複制代碼

   

OK,現在讓我(wǒ)們一(yī)起來看一(yī)下(xià)内存使用情況:

// 連接 mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');
echo "Before: " . memory_get_peak_usage() . "\n";

$res = $connection->query(' `x`,`y` FROM `test` LIMIT 1');
echo "Limit 1: " . memory_get_peak_usage() . "\n";

$res = $connection->query(' `x`,`y` FROM `test` LIMIT 10000');
echo "Limit 10000: " . memory_get_peak_usage() . "\n";複制代碼

   

輸出結果是:

Before: 224704
Limit 1: 224704
Limit 10000: 224704複制代碼

   

Cool。 看來就内存使用而言,内部安全地管理了這個查詢的内存。

爲了更加明确這一(yī)點,我(wǒ)們把限制提高一(yī)倍,使其達到 100,000。 額~如果真這麽幹了,我(wǒ)們将會得到如下(xià)結果:

PHP Warning:  mysqli::query(): (HY000/2013):
              Lost connection to MySQL server during query in /root/test.php on line 11複制代碼

   

究竟發生(shēng)了啥?

這就涉及到 PHP 的 mysql 模塊的工(gōng)作方式的問題了。它其實隻是個 libmysqlclient 的代理,專門負責幹髒活累活。每查出一(yī)部分(fēn)數據後,它就立即把數據放(fàng)入内存中(zhōng)。由于這塊内存還沒被 PHP 管理,所以,當我(wǒ)們在查詢裏增加限制的數量的時候,  memory_get_peak_usage() 不會顯示任何增加的資(zī)源使用情況 。我(wǒ)們被『内存管理沒問題』這種自滿的思想所欺騙了,所以才會導緻上面的演示出現那種問題。        老實說,我(wǒ)們的内存管理确實是有缺陷的,并且我(wǒ)們也會遇到如上所示的問題。

如果使用 mysqlnd 模塊的話(huà),你至少可以避免上面那種欺騙(盡管它自身并不會提升你的内存利用率)。 mysqlnd 被編譯成原生(shēng)的 PHP 擴展,并且确實 使用 PHP 的内存管理器。

因此,如果使用 mysqlnd 而不是 mysql,我(wǒ)們将會得到更真實的内存利用率的信息:

Before: 232048
Limit 1: 324952
Limit 10000: 32572912複制代碼

   

順便一(yī)提,這比剛才更糟糕。根據 PHP 的文檔所說,mysql 使用 mysqlnd 兩倍的内存來存儲數據, 所以,原來使用 mysql 那個腳本真正使用的内存比這裏顯示的更多(大(dà)約是兩倍)。

爲了避免出現這種問題,考慮限制一(yī)下(xià)你查詢的數量,使用一(yī)個較小(xiǎo)的數字來循環,像這樣:

$totalNumberToFetch = 10000;
$portionSize = 100;

for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) {
    $limitFrom = $portionSize * $i;
    $res = $connection->query(
                         " `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize");
}複制代碼

   

當我(wǒ)們把這個常見錯誤和上面的 常見錯誤 #4 結合起來考慮的時候, 就會意識到我(wǒ)們的代碼理想需要在兩者間實現一(yī)個平衡。是讓查詢粒度化和重複化,還是讓單個查詢巨大(dà)化。生(shēng)活亦是如此,平衡不可或缺;哪一(yī)個極端都不好,都可能會導緻 PHP 無法正常運行。

常見錯誤 #6: 忽略 Unicode/UTF-8 的問題

從某種意義上說,這實際上是PHP本身的一(yī)個問題,而不是你在調試 PHP 時遇到的問題,但是它從未得到妥善的解決。 PHP 6 的核心就是要做到支持 Unicode。但是随着 PHP 6 在 2010 年的暫停而擱置了。

這并不意味着開(kāi)發者能夠避免 正确處理 UTF-8 并避免做出所有字符串必須是『古老的 ASCII』的假設。 沒有正确處理非 ASCII 字符串的代碼會因爲引入粗糙的 海森(sēn)堡bug(heisenbugs)  而變得臭名昭著。當一(yī)個名字包含        『Schrödinger』的人注冊到你的系統時,即使簡單的 strlen($_POST['name']) 調用也會出現問題。

下(xià)面是一(yī)些可以避免出現這種問題的清單:

  • 如果你對 UTF-8 還不了解,那麽你至少應該了解下(xià)基礎的東西。 這兒 有個很好的引子。

  • 确保使用 mb_* 函數代替老舊(jiù)的字符串處理函數(需要先保證你的 PHP 構建版本開(kāi)啓了『多字節』(multibyte)擴展)。

  • 确保你的數據庫和表設置了 Unicode 編碼(許多 MySQL 的構建版本仍然默認使用 latin1  )。

  • 記住 json_encode() 會轉換非 ASCII 标識(比如: 『Schrödinger』會被轉換成 『Schru00f6dinger』),但是 serialize() 不會 轉換。

  • 确保 PHP 文件也是 UTF-8 編碼,以避免在連接硬編碼字符串或者配置字符串常量的時候産生(shēng)沖突。

Francisco Claria  在本博客上發表的 UTF-8 Primer for PHP and MySQL  是份寶貴的資(zī)源。

常見錯誤 #7: 認爲 $_POST 總是包含你 POST 的數據

不管它的名稱,$_POST 數組不是總是包含你 POST 的數據,他也有可能會是空的。 爲了理解這一(yī)點,讓我(wǒ)們來看一(yī)下(xià)下(xià)面這個例子。假設我(wǒ)們使用 jQuery.ajax() 模拟一(yī)個服務請求,如下(xià):

// js
$.ajax({
    url: 'http://my.site/some/path',
    method: 'post',
    data: JSON.stringify({a: 'a', b: 'b'}),
    contentType: 'application/json'
});複制代碼

   

(順帶一(yī)提,注意這裏的 contentType: 'application/json' 。我(wǒ)們用 JSON 類型發送數據,這在接口中(zhōng)非常流行。這在 AngularJS $http service 裏是默認的發送數據的類型。)

在我(wǒ)們舉例子的服務端,我(wǒ)們簡單的打印一(yī)下(xià) $_POST 數組:

// php
var_dump($_POST);複制代碼

   

奇怪的是,結果如下(xià):

array(0) { }複制代碼

   

爲什麽?我(wǒ)們的 JSON 串 {a: 'a', b: 'b'} 究竟發生(shēng)了什麽?

原因在于 當内容類型爲 application/x-www-form-urlencoded 或者 multipart/form-data 的時候 PHP 隻會自動解析一(yī)個 POST 的有效内容。這裏面有曆史的原因 --- 這兩種内容類型是在 PHP 的 $_POST 實現前就已經在使用了的兩個重要的類型。所以不管使用其他任何内容類型 (即使是那些現在很流行的,像 application/json),        PHP 也不會自動加載到 POST 的有效内容。

既然 $_POST 是一(yī)個超級全局變量,如果我(wǒ)們重寫 一(yī)次 (在我(wǒ)們的腳本裏盡可能早的),被修改的值(包括 POST 的有效内容)将可以在我(wǒ)們的代碼裏被引用。這很重要因爲 $_POST 已經被 PHP 框架和幾乎所有的自定義的腳本普遍使用來獲取和傳遞請求數據。

所以,舉個例子,當處理一(yī)個内容類型爲 application/json 的 POST 有效内容的時候 ,我(wǒ)們需要手動解析請求内容(decode 出 JSON 數據)并且覆蓋 $_POST 變量,如下(xià):

// php
$_POST = json_decode(file_get_contents('php://input'), true);複制代碼

   

然後當我(wǒ)們打印 $_POST 數組的時候,我(wǒ)們可以看到他正确的包含了 POST 的有效内容;如下(xià):

array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }複制代碼

   

常見錯誤 #8: 認爲 PHP 支持單字符數據類型

閱讀下(xià)面的代碼并思考會輸出什麽:

for ($c = 'a'; $c <= 'z'; $c++) {
    echo $c . "\n";
}複制代碼

   

如果你的答案是 az,那麽你可能會對這是一(yī)個錯誤答案感到吃驚。

沒錯,它确實會輸出 az,但是,它還會繼續輸出 aayz。我(wǒ)們一(yī)起來看一(yī)下(xià)這是爲什麽。

PHP 中(zhōng)沒有 char 數據類型; 隻能用 string 類型。記住一(yī)點,在 PHP 中(zhōng)增加 string 類型的 z 得到的是 aa

php> $c = 'z'; echo ++$c . "\n";
aa複制代碼

   

沒那麽令人混淆的是,aa 的字典順序是 小(xiǎo)于  z 的:

php> var_export((boolean)('aa' < 'z')) . "\n";
true複制代碼

   

這也是爲什麽上面那段簡單的代碼會輸出 a 到 z, 然後 繼續 輸出 aa到 yz。 它停在了 za,那是它遇到的第一(yī)個比 z 大(dà) 的:

php> var_export((boolean)('za' < 'z')) . "\n";
false複制代碼

   

事實上,在 PHP 裏 有合适的 方式在循環中(zhōng)輸出 az 的值:

for ($i = ord('a'); $i <= ord('z'); $i++) {
    echo chr($i) . "\n";
}複制代碼

   

或者是這樣:

$letters = range('a', 'z');

for ($i = 0; $i < count($letters); $i++) {
    echo $letters[$i] . "\n";
}複制代碼

   

常見 錯誤 #9: 忽視代碼規範

盡管忽視代碼标準并不直接導緻需要去(qù)調試 PHP 代碼,但這可能是所有需要談論的事情裏最重要的一(yī)項。

在一(yī)個項目中(zhōng)忽視代碼規範能夠導緻大(dà)量的問題。最樂觀的預計,前後代碼不一(yī)緻(在此之前每個開(kāi)發者都在“做自己的事情”)。但最差的結果,PHP 代碼不能運行或者很難(有時是不可能的)去(qù)順利通過,這對于 調試代碼、提升性能、維護項目來說也是困難重重。并且這意味着降低你們團隊的生(shēng)産力,增加大(dà)量的額外(wài)(或者至少是本不必要的)精力消耗。

幸運的是對于 PHP 開(kāi)發者來說,存在 PHP 編碼标準建議(PSR),它由下(xià)面的五個标準組成:

  • PSR-0: 自動加載标準

  • PSR-1: 基礎編碼标準

  • PSR-2: 編碼風格指導

  • PSR-3: 日志(zhì)接口

  • PSR-4: 自動加載增強版

PSR 起初是由市場上最大(dà)的組織平台維護者創造的。 Zend, Drupal, Symfony, Joomla 和 其他 爲這些标準做出了貢獻,并一(yī)直遵守它們。甚至,多年前試圖成爲一(yī)個标準的 PEAR ,現在也加入到 PSR 中(zhōng)來。

某種意義上,你的代碼标準是什麽幾乎是不重要的,隻要你遵循一(yī)個标準并堅持下(xià)去(qù),但一(yī)般來講,跟随 PSR 是一(yī)個很不錯的主意,除非你的項目上有其他讓人難以抗拒的理由。越來越多的團隊和項目正在遵從 PSR 。在這一(yī)點上,大(dà)部分(fēn)的 PHP 開(kāi)發者達成了共識,因此使用 PSR 代碼标準,有利于使新加入團隊的開(kāi)發者對你的代碼标準感到更加的熟悉與舒适。

常見錯誤 #10:  濫用 empty()

一(yī)些 PHP 開(kāi)發者喜歡對幾乎所有的事情使用 empty() 做布爾值檢驗。不過,在一(yī)些情況下(xià),這會導緻混亂。

首先,讓我(wǒ)們回到數組和 ArrayObject 實例(和數組類似)。考慮到他們的相似性,很容易假設它們的行爲是相同的。然而,事實證明這是一(yī)個危險的假設。舉例,在 PHP 5.0 中(zhōng):

// PHP 5.0 或後續版本:
$array = [];
var_dump(empty($array));        // 輸出 bool(true)
$array = new ArrayObject();
var_dump(empty($array));        // 輸出 bool(false)
// 爲什麽這兩種方法不産生(shēng)相同的輸出呢?複制代碼

   

更糟糕的是,PHP 5.0之前的結果可能是不同的:

// PHP 5.0 之前:
$array = [];
var_dump(empty($array));        // 輸出 bool(false)
$array = new ArrayObject();
var_dump(empty($array));        // 輸出 bool(false)複制代碼

   

這種方法上的不幸是十分(fēn)普遍的。比如,在 Zend Framework 2 下(xià)的 Zend\Db\TableGateway 的 TableGateway::() 結果中(zhōng)調用 current() 時返回數據的方式,正如文檔所表明的那樣。開(kāi)發者很容易就會變成此類數據錯誤的受害者。

爲了避免這些問題的産生(shēng),更好的方法是使用 count() 去(qù)檢驗空數組結構:

// 注意這會在 PHP 的所有版本中(zhōng)發揮作用 (5.0 前後都是):
$array = [];
var_dump(count($array));        // 輸出 int(0)
$array = new ArrayObject();
var_dump(count($array));        // 輸出 int(0)複制代碼

   

順便說一(yī)句, 由于 PHP 将 0 轉換爲 false , count() 能夠被使用在 if() 條件内部去(qù)檢驗空數組。同樣值得注意的是,在 PHP 中(zhōng), count() 在數組中(zhōng)是常量複雜(zá)度 (O(1) 操作) ,這更清晰的表明它是正确的選擇。

另一(yī)個使用 empty() 産生(shēng)危險的例子是當它和魔術方法 _get() 一(yī)起使用。我(wǒ)們來定義兩個類并使其都有一(yī)個 test 屬性。

首先我(wǒ)們定義包含 test 公共屬性的 Regular 類。

class Regular
{
    public $test = 'value';
}複制代碼

   

然後我(wǒ)們定義 Magic 類,這裏使用魔術方法 __get() 來操作去(qù)訪問它的 test 屬性:

class Magic
{
    private $values = ['test' => 'value'];

    public function __get($key)
    {
        if (isset($this->values[$key])) {
            return $this->values[$key];
        }
    }
}複制代碼

   

好了,現在我(wǒ)們嘗試去(qù)訪問每個類中(zhōng)的 test 屬性看看會發生(shēng)什麽:

$regular = new Regular();
var_dump($regular->test);    // 輸出 string(4) "value"
$magic = new Magic();
var_dump($magic->test);      // 輸出 string(4) "value"複制代碼

   

到目前爲止還好。

但是現在當我(wǒ)們對其中(zhōng)的每一(yī)個都調用 empty() ,讓我(wǒ)們看看會發生(shēng)什麽:

var_dump(empty($regular->test));    // 輸出 bool(false)
var_dump(empty($magic->test));      // 輸出 bool(true)複制代碼

   

咳。所以如果我(wǒ)們依賴 empty() ,我(wǒ)們很可能誤認爲 $magic 的屬性 test 是空的,而實際上它被設置爲 'value'

不幸的是,如果類使用魔術方法 __get() 來獲取屬性值,那麽就沒有萬無一(yī)失的方法來檢查該屬性值是否爲空。
在類的作用域之外(wài),你僅僅隻能檢查是否将返回一(yī)個 null 值,這并不意味着沒有設置相應的鍵,因爲它實際上還可能被設置爲 null

相反,如果我(wǒ)們試圖去(qù)引用 Regular 類實例中(zhōng)不存在的屬性,我(wǒ)們将得到一(yī)個類似于以下(xià)内容的通知(zhī):

Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10

Call Stack:
    0.0012     234704   1. {main}() /path/to/test.php:0複制代碼

   

所以這裏的主要觀點是 empty() 方法應該被謹慎地使用,因爲如果不小(xiǎo)心的話(huà)它可能導緻混亂 -- 甚至潛在的誤導 -- 結果。

總結

PHP 的易用性讓開(kāi)發者陷入一(yī)種虛假的舒适感,語言本身的一(yī)些細微差别和特質,可能花費(fèi)掉你大(dà)量的時間去(qù)調試。這些可能會導緻 PHP 程序無法正常工(gōng)作,并導緻諸如此處所述的問題。

PHP 在其20年的曆史中(zhōng),已經發生(shēng)了顯著的變化。花時間去(qù)熟悉語言本身的微妙之處是值得的,因爲它有助于确保你編寫的軟件更具可擴展性,健壯和可維護性。


作者:金正皓
鏈接:https://juejin.cn/post/6844903586745286664
來源:掘金
著作權歸作者所有。商(shāng)業轉載請聯系作者獲得授權,非商(shāng)業轉載請注明出處。


掃二維碼與項目經理溝通

我(wǒ)們在微信上24小(xiǎo)時期待你的聲音

解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯網交流

鄭重聲明:郑州禾木网络技术有限公司網絡科技有限公司以外(wài)的任何單位或個人,不得使用該案例作爲工(gōng)作成功展示!