本篇文章給大家介紹10個 PHP 開發者最容易犯的錯誤。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有所幫助。
PHP 語言讓 WEB 端程序設計變得簡單,這也是它能流行起來的原因。但也是因為它的簡單,PHP 也慢慢發展成一個相對復雜的語言,層出不窮的框架,各種語言特性和版本差異都時常讓搞的我們頭大,不得不浪費大量時間去調試。這篇文章列出了十個最容易出錯的地方,值得我們去注意。
易犯錯誤 #1: 在 foreach
循環后留下數組的引用
還不清楚 PHP 中 foreach
遍歷的工作原理?如果你在想遍歷數組時操作數組中每個元素,在 foreach
循環中使用引用會十分方便,例如
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr 現在是 array(2, 4, 6, 8)
問題是,如果你不注意的話這會導致一些意想不到的負面作用。在上述例子,在代碼執行完以后,$value
仍保留在作用域內,并保留著對數組最后一個元素的引用。之后與 $value
相關的操作會無意中修改數組中最后一個元素的值。
你要記住 foreach
并不會產生一個塊級作用域。因此,在上面例子中 $value
是一個全局引用變量。在 foreach
遍歷中,每一次迭代都會形成一個對 $arr
下一個元素的引用。當遍歷結束后, $value
會引用 $arr
的最后一個元素,并保留在作用域中
這種行為會導致一些不易發現的,令人困惑的bug,以下是一個例子
$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
你沒有看錯,最后一行的最后一個值是 2 ,而不是 3 ,為什么?
在完成第一個 foreach
遍歷后, $array
并沒有改變,但是像上述解釋的那樣, $value
留下了一個對 $array
最后一個元素的危險的引用(因為 foreach
通過引用獲得 $value
)
這導致當運行到第二個 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()
掉這個保留著引用的變量。例如
$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
存在且被設置為 null
, isset($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
那個腳本真正使用的內存比這里顯示的