十個 PHP 開發者最容易犯的錯誤

clipboard.png

PHP 語言讓 WEB 端程序設計變得簡單,這也是它能流行起來的緣由。但也是由於它的簡單,PHP 也慢慢發展成一個相對複雜的語言,層出不窮的框架,各類語言特性和版本差別都時常讓搞的咱們頭大,不得不浪費大量時間去調試。這篇文章列出了十個最容易出錯的地方,值得咱們去注意。php

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

還不清楚 PHP 中 foreach 遍歷的工做原理?若是你在想遍歷數組時操做數組中每一個元素,在 foreach 循環中使用引用會十分方便,例如html

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
        $value = $value * 2;
}
// $arr 如今是 array(2, 4, 6, 8)

問題是,若是你不注意的話這會致使一些意想不到的負面做用。在上述例子,在代碼執行完之後,$value 仍保留在做用域內,並保留着對數組最後一個元素的引用。以後與 $value 相關的操做會無心中修改數組中最後一個元素的值。mysql

你要記住 foreach 並不會產生一個塊級做用域。所以,在上面例子中 $value 是一個全局引用變量。在 foreach 遍歷中,每一次迭代都會造成一個對 $arr 下一個元素的引用。當遍歷結束後, $value 會引用 $arr 的最後一個元素,並保留在做用域中laravel

這種行爲會致使一些不易發現的,使人困惑的bug,如下是一個例子c++

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

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

foreach ($array as $value) {}     // 經過賦值遍歷
echo implode(',', $array), "\n";

以上代碼會輸出angularjs

1,2,3
1,2,3
1,2,2

你沒有看錯,最後一行的最後一個值是 2 ,而不是 3 ,爲何?ajax

在完成第一個 foreach 遍歷後, $array 並無改變,可是像上述解釋的那樣, $value 留下了一個對 $array 最後一個元素的危險的引用(由於 foreach 經過引用得到 $valuesql

這致使當運行到第二個 foreach ,這個"奇怪的東西"發生了。當 $value 經過賦值得到, foreach 按順序複製每一個 $array 的元素到 $value 時,第二個 foreach 裏面的細節是這樣的數據庫

  • 第一步:複製 $array[0] (也就是 1 )到 $value$value 實際上是 $array最後一個元素的引用,即 $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 中方便的使用引用而免遭這種麻煩,請在 foreach 執行完畢後 unset() 掉這個保留着引用的變量。例如json

$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

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

看看下面的代碼:

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

開發者想必是想確認 keyShouldBeSet 是否存在於 $data 中。然而,正如上面說的,若是 $data['keyShouldBeSet'] 存在而且值爲 null 的時候, isset($data['keyShouldBeSet']) 也會返回 false。因此上面的邏輯是不嚴謹的。

咱們來看另一個例子:

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

// ...

if (!isset($postData)) {
    echo 'post not active';
}

上述代碼,一般認爲,假如 $_POST['active'] 返回 true,那麼 postData 必將存在,所以 isset($postData) 也將返回 true。反之, isset($postData) 返回 false 的惟一多是 $_POST['active'] 也返回 false

然而事實並不是如此!

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

順便一提,若是上面代碼的意圖真的是再次確認 $_POST['active'] 是否返回 true,依賴 isset() 來作,無論對於哪一種場景來講都是一種糟糕的決定。更好的作法是再次檢查 $_POST['active'],即:

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

// ...

if ($_POST['active']) {
    echo 'post not active';
}

對於這種狀況,雖然檢查一個變量是否真的存在很重要(即:區分一個變量是未被設置仍是被設置爲 null);可是使用 array_key_exists() 這個函數倒是個更健壯的解決途徑。

好比,咱們能夠像下面這樣重寫上面第一個例子:

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

另外,經過結合 array_key_exists() 和 get_defined_vars(), 咱們能更加可靠的判斷一個變量在當前做用域中是否存在:

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

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

考慮下面的代碼片斷:

class Config
{
    private $values = [];

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

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

若是你運行上面的代碼,將獲得下面的輸出:

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

出了什麼問題?

上面代碼的問題在於沒有搞清楚經過引用與經過值返回數組的區別。除非你明確告訴 PHP 經過引用返回一個數組(例如,使用 &),不然 PHP 默認將會「經過值」返回這個數組。這意味着這個數組的一份拷貝將會被返回,所以被調函數與調用者所訪問的數組並非一樣的數組實例。

因此上面對 getValues() 的調用將會返回 $values 數組的一份拷貝,而不是對它的引用。考慮到這一點,讓咱們從新回顧一下以上例子中的兩個關鍵行:

// getValues() 返回了一個 $values 數組的拷貝
// 因此`test`元素被添加到了這個拷貝中,而不是 $values 數組自己。
$config->getValues()['test'] = 'test';


// getValues() 又返回了另外一份 $values 數組的拷貝
// 且這份拷貝中並不包含一個`test`元素(這就是爲何咱們會獲得 「未定義索引」 消息)。
echo $config->getValues()['test'];

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

$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];

這段代碼將會正常工做(例如,它將會輸出test而不會產生任何「未定義索引」消息),可是這個方法可能並不能知足你的需求。特別是上面的代碼並不會修改原始的$values數組。若是你想要修改原始的數組(例如添加一個test元素),就須要修改getValues()函數,讓它返回一個$values數組自身的引用。經過在函數名前面添加一個&來講明這個函數將返回一個引用;例如:

class Config
{
    private $values = [];

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

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

這會輸出期待的test

可是如今讓事情更困惑一些,請考慮下面的代碼片斷:

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'];

若是你認爲這段代碼會致使與以前的數組例子同樣的「未定義索引」錯誤,那就錯了。實際上,這段代碼將會正常運行。緣由是,與數組不一樣,PHP 永遠會將對象按引用傳遞。(ArrayObject 是一個 SPL 對象,它徹底模仿數組的用法,可是倒是以對象來工做。)

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

儘管如此,咱們要認識到應該儘可能避免返回一個數組或 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數組自己進行公開訪問的狀況下設置或者獲取數組中的任意值。

常見的錯誤 #4:在循環中執行查詢

若是像這樣的話,必定不難見到你的 PHP 沒法正常工做。

$models = [];

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

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

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

結果每輪循環都會產生一次對數據庫的查詢。 所以,假如你爲這個循環提供了一個包含 1000 個值的數組,它會對資源產生 1000 單獨的請求!若是這樣的腳本在多個線程中被調用,他會有致使系統崩潰的潛在危險。

所以,相當重要的是,當你的代碼要進行查詢時,應該儘量的收集須要用到的值,而後在一個查詢中獲取全部結果。

一個咱們平時經常能見到查詢效率低下的地方 (例如:在循環中)是使用一個數組中的值 (好比說不少的 ID )向表發起請求。檢索每個 ID 的全部的數據,代碼將會迭代這個數組,每一個 ID 進行一次SQL查詢請求,它看起來經常是這樣:

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

可是 只用一條 SQL 查詢語句就能夠更高效的完成相同的工做,好比像下面這樣:

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

所以在你的代碼直接或間接進行查詢請求時,必定要認出這種查詢。儘量的經過一次查詢獲得想要的結果。然而,依然要當心謹慎,否則就可能會出現下面咱們要講的另外一個易犯的錯誤...

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

一次取多條記錄確定是比一條條的取高效,可是當咱們使用 PHP 的 mysql 擴展的時候,這也可能成爲一個致使 libmysqlclient 出現『內存不足』(out of memory)的條件。

咱們在一個測試盒裏演示一下,該測試盒的環境是:有限的內存(512MB RAM),MySQL,和 php-cli

咱們將像下面這樣引導一個數據表:

// 鏈接 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 = "INSERT INTO `test` VALUES ($row";
    for ($col = 0; $col < 400; $col++) {
        $query .= ', ' . mt_rand(1000000000, 9999999999);
    }
    $query .= ')';
    $connection->query($query);
}

OK,如今讓咱們一塊兒來看一下內存使用狀況:

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

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

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

輸出結果是:

Before: 224704
Limit 1: 224704
Limit 10000: 224704

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

爲了更加明確這一點,咱們把限制提升一倍,使其達到 100,000。 額~若是真這麼幹了,咱們將會獲得以下結果:

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

究竟發生了啥?

這就涉及到 PHP 的 mysql 模塊的工做方式的問題了。它其實只是個 libmysqlclient 的代理,專門負責幹髒活累活。每查出一部分數據後,它就當即把數據放入內存中。因爲這塊內存還沒被 PHP 管理,因此,當咱們在查詢裏增長限制的數量的時候, memory_get_peak_usage() 不會顯示任何增長的資源使用狀況 。咱們被『內存管理沒問題』這種自滿的思想所欺騙了,因此纔會致使上面的演示出現那種問題。 老實說,咱們的內存管理確實是有缺陷的,而且咱們也會遇到如上所示的問題。

若是使用 mysqlnd 模塊的話,你至少能夠避免上面那種欺騙(儘管它自身並不會提高你的內存利用率)。 mysqlnd 被編譯成原生的 PHP 擴展,而且確實 使用 PHP 的內存管理器。

所以,若是使用 mysqlnd 而不是 mysql,咱們將會獲得更真實的內存利用率的信息:

Before: 232048
Limit 1: 324952
Limit 10000: 32572912

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

爲了不出現這種問題,考慮限制一下你查詢的數量,使用一個較小的數字來循環,像這樣:

$totalNumberToFetch = 10000;
$portionSize = 100;

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

當咱們把這個常見錯誤和上面的 常見錯誤 #4 結合起來考慮的時候, 就會意識到咱們的代碼理想須要在二者間實現一個平衡。是讓查詢粒度化和重複化,仍是讓單個查詢巨大化。生活亦是如此,平衡不可或缺;哪個極端都很差,均可能會致使 PHP 沒法正常運行。

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

從某種意義上說,這其實是PHP自己的一個問題,而不是你在調試 PHP 時遇到的問題,可是它從未獲得妥善的解決。 PHP 6 的核心就是要作到支持 Unicode。可是隨着 PHP 6 在 2010 年的暫停而擱置了。

這並不意味着開發者可以避免 正確處理 UTF-8 並避免作出全部字符串必須是『古老的 ASCII』的假設。 沒有正確處理非 ASCII 字符串的代碼會由於引入粗糙的 海森堡bug(heisenbugs)  而變得臭名昭著。當一個名字包含 『Schrödinger』的人註冊到你的系統時,即便簡單的 strlen($_POST['name']) 調用也會出現問題。

下面是一些能夠避免出現這種問題的清單:

  • 若是你對 UTF-8 還不瞭解,那麼你至少應該瞭解下基礎的東西。 這兒 有個很好的引子。
  • 確保使用 mb_* 函數代替老舊的字符串處理函數(須要先保證你的 PHP 構建版本開啓了『多字節』(multibyte)擴展)。
  • 確保你的數據庫和表設置了 Unicode 編碼(許多 MySQL 的構建版本仍然默認使用 latin1  )。
  • 記住 json_encode() 會轉換非 ASCII 標識(好比: 『Schrödinger』會被轉換成 『Schru00f6dinger』),可是 serialize() 不會 轉換。
  • 確保 PHP 文件也是 UTF-8 編碼,以免在鏈接硬編碼字符串或者配置字符串常量的時候產生衝突。

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

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

無論它的名稱,$_POST 數組不是老是包含你 POST 的數據,他也有可能會是空的。 爲了理解這一點,讓咱們來看一下下面這個例子。假設咱們使用 jQuery.ajax() 模擬一個服務請求,以下:

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

(順帶一提,注意這裏的 contentType: 'application/json' 。咱們用 JSON 類型發送數據,這在接口中很是流行。這在 AngularJS $http service 裏是默認的發送數據的類型。)

在咱們舉例子的服務端,咱們簡單的打印一下 $_POST 數組:

// php
var_dump($_POST);

奇怪的是,結果以下:

array(0) { }

爲何?咱們的 JSON 串 {a: 'a', b: 'b'} 究竟發生了什麼?

緣由在於 當內容類型爲 application/x-www-form-urlencoded 或者 multipart/form-data 的時候 PHP 只會自動解析一個 POST 的有效內容。這裏面有歷史的緣由 --- 這兩種內容類型是在 PHP 的 $_POST 實現前就已經在使用了的兩個重要的類型。因此無論使用其餘任何內容類型 (即便是那些如今很流行的,像 application/json), PHP 也不會自動加載到 POST 的有效內容。

既然 $_POST 是一個超級全局變量,若是咱們重寫 一次 (在咱們的腳本里儘量早的),被修改的值(包括 POST 的有效內容)將能夠在咱們的代碼裏被引用。這很重要由於 $_POST 已經被 PHP 框架和幾乎全部的自定義的腳本廣泛使用來獲取和傳遞請求數據。

因此,舉個例子,當處理一個內容類型爲 application/json 的 POST 有效內容的時候 ,咱們須要手動解析請求內容(decode 出 JSON 數據)而且覆蓋 $_POST 變量,以下:

// php
$_POST = json_decode(file_get_contents('php://input'), true);

而後當咱們打印 $_POST 數組的時候,咱們能夠看到他正確的包含了 POST 的有效內容;以下:

array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }

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

閱讀下面的代碼並思考會輸出什麼:

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

若是你的答案是 az,那麼你可能會對這是一個錯誤答案感到吃驚。

沒錯,它確實會輸出 az,可是,它還會繼續輸出 aayz。咱們一塊兒來看一下這是爲何。

PHP 中沒有 char 數據類型; 只能用 string 類型。記住一點,在 PHP 中增長 string 類型的 z 獲得的是 aa

php> $c = 'z'; echo ++$c . "\n";
aa

沒那麼使人混淆的是,aa 的字典順序是 小於  z 的:

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

這也是爲何上面那段簡單的代碼會輸出 a 到 z, 而後 繼續 輸出 aa到 yz。 它停在了 za,那是它遇到的第一個比 z 的:

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

事實上,在 PHP 裏 有合適的 方式在循環中輸出 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: 忽視代碼規範

儘管忽視代碼標準並不直接致使須要去調試 PHP 代碼,但這多是全部須要談論的事情裏最重要的一項。

在一個項目中忽視代碼規範可以致使大量的問題。最樂觀的預計,先後代碼不一致(在此以前每一個開發者都在「作本身的事情」)。但最差的結果,PHP 代碼不能運行或者很難(有時是不可能的)去順利經過,這對於 調試代碼、提高性能、維護項目來講也是困難重重。而且這意味着下降大家團隊的生產力,增長大量的額外(或者至少是本沒必要要的)精力消耗。

幸運的是對於 PHP 開發者來講,存在 PHP 編碼標準建議(PSR),它由下面的五個標準組成:

  • PSR-0: 自動加載標準
  • PSR-1: 基礎編碼標準
  • PSR-2: 編碼風格指導
  • PSR-3: 日誌接口
  • PSR-4: 自動加載加強版

PSR 起初是由市場上最大的組織平臺維護者創造的。 Zend, Drupal, Symfony, Joomla 和 其餘 爲這些標準作出了貢獻,並一直遵照它們。甚至,多年前試圖成爲一個標準的 PEAR ,如今也加入到 PSR 中來。

某種意義上,你的代碼標準是什麼幾乎是不重要的,只要你遵循一個標準並堅持下去,但通常來說,跟隨 PSR 是一個很不錯的主意,除非你的項目上有其餘讓人難以抗拒的理由。愈來愈多的團隊和項目正在聽從 PSR 。在這一點上,大部分的 PHP 開發者達成了共識,所以使用 PSR 代碼標準,有利於使新加入團隊的開發者對你的代碼標準感到更加的熟悉與溫馨。

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

一些 PHP 開發者喜歡對幾乎全部的事情使用 empty() 作布爾值檢驗。不過,在一些狀況下,這會致使混亂。

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

// PHP 5.0 或後續版本:
$array = [];
var_dump(empty($array));        // 輸出 bool(true)
$array = new ArrayObject();
var_dump(empty($array));        // 輸出 bool(false)
// 爲何這兩種方法不產生相同的輸出呢?

更糟糕的是,PHP 5.0以前的結果多是不一樣的:

// PHP 5.0 以前:
$array = [];
var_dump(empty($array));        // 輸出 bool(false)
$array = new ArrayObject();
var_dump(empty($array));        // 輸出 bool(false)

這種方法上的不幸是十分廣泛的。好比,在 Zend Framework 2 下的 Zend\Db\TableGateway 的 TableGateway::select() 結果中調用 current() 時返回數據的方式,正如文檔所代表的那樣。開發者很容易就會變成此類數據錯誤的受害者。

爲了不這些問題的產生,更好的方法是使用 count() 去檢驗空數組結構:

// 注意這會在 PHP 的全部版本中發揮做用 (5.0 先後都是):
$array = [];
var_dump(count($array));        // 輸出 int(0)
$array = new ArrayObject();
var_dump(count($array));        // 輸出 int(0)

順便說一句, 因爲 PHP 將 0 轉換爲 false , count() 可以被使用在 if() 條件內部去檢驗空數組。一樣值得注意的是,在 PHP 中, count() 在數組中是常量複雜度 (O(1) 操做) ,這更清晰的代表它是正確的選擇。

另外一個使用 empty() 產生危險的例子是當它和魔術方法 _get() 一塊兒使用。咱們來定義兩個類並使其都有一個 test 屬性。

首先咱們定義包含 test 公共屬性的 Regular 類。

class Regular
{
    public $test = 'value';
}

而後咱們定義 Magic 類,這裏使用魔術方法 __get() 來操做去訪問它的 test 屬性:

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

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

好了,如今咱們嘗試去訪問每一個類中的 test 屬性看看會發生什麼:

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

到目前爲止還好。

可是如今當咱們對其中的每個都調用 empty() ,讓咱們看看會發生什麼:

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

咳。因此若是咱們依賴 empty() ,咱們極可能誤認爲 $magic 的屬性 test 是空的,而實際上它被設置爲 'value'

不幸的是,若是類使用魔術方法 __get() 來獲取屬性值,那麼就沒有萬無一失的方法來檢查該屬性值是否爲空。
在類的做用域以外,你僅僅只能檢查是否將返回一個 null 值,這並不意味着沒有設置相應的鍵,由於它實際上還可能被設置爲 null

相反,若是咱們試圖去引用 Regular 類實例中不存在的屬性,咱們將獲得一個相似於如下內容的通知:

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() 方法應該被謹慎地使用,由於若是不當心的話它可能致使混亂 -- 甚至潛在的誤導 -- 結果。

總結

PHP 的易用性讓開發者陷入一種虛假的溫馨感,語言自己的一些細微差異和特質,可能花費掉你大量的時間去調試。這些可能會致使 PHP 程序沒法正常工做,並致使諸如此處所述的問題。

PHP 在其20年的歷史中,已經發生了顯著的變化。花時間去熟悉語言自己的微妙之處是值得的,由於它有助於確保你編寫的軟件更具可擴展性,健壯和可維護性。

更多現代化 PHP 知識,請前往 Laravel / PHP 知識社區
相關文章
相關標籤/搜索