本篇文章給大家介紹10個(gè) PHP 開發(fā)者最容易犯的錯(cuò)誤。有一定的參考價(jià)值,有需要的朋友可以參考一下,希望對(duì)大家有所幫助。
PHP 語(yǔ)言讓 WEB 端程序設(shè)計(jì)變得簡(jiǎn)單,這也是它能流行起來(lái)的原因。但也是因?yàn)樗暮?jiǎn)單,PHP 也慢慢發(fā)展成一個(gè)相對(duì)復(fù)雜的語(yǔ)言,層出不窮的框架,各種語(yǔ)言特性和版本差異都時(shí)常讓搞的我們頭大,不得不浪費(fèi)大量時(shí)間去調(diào)試。這篇文章列出了十個(gè)最容易出錯(cuò)的地方,值得我們?nèi)プ⒁狻?/p>
易犯錯(cuò)誤 #1: 在 foreach
循環(huán)后留下數(shù)組的引用
還不清楚 PHP 中 foreach
遍歷的工作原理?如果你在想遍歷數(shù)組時(shí)操作數(shù)組中每個(gè)元素,在 foreach
循環(huán)中使用引用會(huì)十分方便,例如
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr 現(xiàn)在是 array(2, 4, 6, 8)
問(wèn)題是,如果你不注意的話這會(huì)導(dǎo)致一些意想不到的負(fù)面作用。在上述例子,在代碼執(zhí)行完以后,$value
仍保留在作用域內(nèi),并保留著對(duì)數(shù)組最后一個(gè)元素的引用。之后與 $value
相關(guān)的操作會(huì)無(wú)意中修改數(shù)組中最后一個(gè)元素的值。
你要記住 foreach
并不會(huì)產(chǎn)生一個(gè)塊級(jí)作用域。因此,在上面例子中 $value
是一個(gè)全局引用變量。在 foreach
遍歷中,每一次迭代都會(huì)形成一個(gè)對(duì) $arr
下一個(gè)元素的引用。當(dāng)遍歷結(jié)束后, $value
會(huì)引用 $arr
的最后一個(gè)元素,并保留在作用域中
這種行為會(huì)導(dǎo)致一些不易發(fā)現(xiàn)的,令人困惑的bug,以下是一個(gè)例子
$array = [1, 2, 3]; echo implode(',', $array), "n"; foreach ($array as &$value) {} // 通過(guò)引用遍歷 echo implode(',', $array), "n"; foreach ($array as $value) {} // 通過(guò)賦值遍歷 echo implode(',', $array), "n";
以上代碼會(huì)輸出
1,2,3 1,2,3 1,2,2
你沒(méi)有看錯(cuò),最后一行的最后一個(gè)值是 2 ,而不是 3 ,為什么?
在完成第一個(gè) foreach
遍歷后, $array
并沒(méi)有改變,但是像上述解釋的那樣, $value
留下了一個(gè)對(duì) $array
最后一個(gè)元素的危險(xiǎn)的引用(因?yàn)?foreach
通過(guò)引用獲得 $value
)
這導(dǎo)致當(dāng)運(yùn)行到第二個(gè) foreach
,這個(gè)"奇怪的東西"發(fā)生了。當(dāng) $value
通過(guò)賦值獲得, foreach
按順序復(fù)制每個(gè) $array
的元素到 $value
時(shí),第二個(gè) foreach
里面的細(xì)節(jié)是這樣的
- 第一步:復(fù)制
$array[0]
(也就是 1 )到$value
($value
其實(shí)是$array
最后一個(gè)元素的引用,即$array[2]
),所以$array[2]
現(xiàn)在等于 1。所以$array
現(xiàn)在包含 [1, 2, 1] - 第二步:復(fù)制
$array[1]
(也就是 2 )到$value
($array[2]
的引用),所以$array[2]
現(xiàn)在等于 2。所以$array
現(xiàn)在包含 [1, 2, 2] - 第三步:復(fù)制
$array[2]
(現(xiàn)在等于 2 ) 到$value
($array[2]
的引用),所以$array[2]
現(xiàn)在等于 2 。所以$array
現(xiàn)在包含 [1, 2, 2]
為了在 foreach
中方便的使用引用而免遭這種麻煩,請(qǐng)?jiān)?foreach
執(zhí)行完畢后 unset()
掉這個(gè)保留著引用的變量。例如
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value 不再引用 $arr[3]
常見錯(cuò)誤 #2: 誤解 isset()
的行為
盡管名字叫 isset,但是 isset()
不僅會(huì)在變量不存在的時(shí)候返回 false
,在變量值為 null
的時(shí)候也會(huì)返回 false
。
這種行為比最初出現(xiàn)的問(wèn)題更為棘手,同時(shí)也是一種常見的錯(cuò)誤源。
看看下面的代碼:
$data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }
開發(fā)者想必是想確認(rèn) keyShouldBeSet
是否存在于 $data
中。然而,正如上面說(shuō)的,如果 $data['keyShouldBeSet']
存在并且值為 null
的時(shí)候, isset($data['keyShouldBeSet'])
也會(huì)返回 false
。所以上面的邏輯是不嚴(yán)謹(jǐn)?shù)摹?/p>
我們來(lái)看另外一個(gè)例子:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }
上述代碼,通常認(rèn)為,假如 $_POST['active']
返回 true
,那么 postData
必將存在,因此 isset($postData)
也將返回 true
。反之, isset($postData)
返回 false
的唯一可能是 $_POST['active']
也返回 false
。
然而事實(shí)并非如此!
如我所言,如果$postData
存在且被設(shè)置為 null
, isset($postData)
也會(huì)返回 false
。 也就是說(shuō),即使 $_POST['active']
返回 true
, isset($postData)
也可能會(huì)返回 false
。 再一次說(shuō)明上面的邏輯不嚴(yán)謹(jǐn)。
順便一提,如果上面代碼的意圖真的是再次確認(rèn) $_POST['active']
是否返回 true
,依賴 isset()
來(lái)做,不管對(duì)于哪種場(chǎng)景來(lái)說(shuō)都是一種糟糕的決定。更好的做法是再次檢查 $_POST['active']
,即:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }
對(duì)于這種情況,雖然檢查一個(gè)變量是否真的存在很重要(即:區(qū)分一個(gè)變量是未被設(shè)置還是被設(shè)置為 null
);但是使用 array_key_exists()
這個(gè)函數(shù)卻是個(gè)更健壯的解決途徑。
比如,我們可以像下面這樣重寫上面第一個(gè)例子:
$data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }
另外,通過(guò)結(jié)合 array_key_exists()
和 get_defined_vars()
, 我們能更加可靠的判斷一個(gè)變量在當(dāng)前作用域中是否存在:
if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }
常見錯(cuò)誤 #3:關(guān)于通過(guò)引用返回與通過(guò)值返回的困惑
考慮下面的代碼片段:
class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
如果你運(yùn)行上面的代碼,將得到下面的輸出:
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
出了什么問(wèn)題?
上面代碼的問(wèn)題在于沒(méi)有搞清楚通過(guò)引用與通過(guò)值返回?cái)?shù)組的區(qū)別。除非你明確告訴 PHP 通過(guò)引用返回一個(gè)數(shù)組(例如,使用 &
),否則 PHP 默認(rèn)將會(huì)「通過(guò)值」返回這個(gè)數(shù)組。這意味著這個(gè)數(shù)組的一份拷貝將會(huì)被返回,因此被調(diào)函數(shù)與調(diào)用者所訪問(wèn)的數(shù)組并不是同樣的數(shù)組實(shí)例。
所以上面對(duì) getValues()
的調(diào)用將會(huì)返回 $values
數(shù)組的一份拷貝,而不是對(duì)它的引用??紤]到這一點(diǎn),讓我們重新回顧一下以上例子中的兩個(gè)關(guān)鍵行:
// getValues() 返回了一個(gè) $values 數(shù)組的拷貝 // 所以`test`元素被添加到了這個(gè)拷貝中,而不是 $values 數(shù)組本身。 $config->getValues()['test'] = 'test'; // getValues() 又返回了另一份 $values 數(shù)組的拷貝 // 且這份拷貝中并不包含一個(gè)`test`元素(這就是為什么我們會(huì)得到 「未定義索引」 消息)。 echo $config->getValues()['test'];
一個(gè)可能的修改方法是存儲(chǔ)第一次通過(guò) getValues()
返回的 $values
數(shù)組拷貝,然后后續(xù)操作都在那份拷貝上進(jìn)行;例如:
$vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];
這段代碼將會(huì)正常工作(例如,它將會(huì)輸出test
而不會(huì)產(chǎn)生任何「未定義索引」消息),但是這個(gè)方法可能并不能滿足你的需求。特別是上面的代碼并不會(huì)修改原始的$values
數(shù)組。如果你想要修改原始的數(shù)組(例如添加一個(gè)test
元素),就需要修改getValues()
函數(shù),讓它返回一個(gè)$values
數(shù)組自身的引用。通過(guò)在函數(shù)名前面添加一個(gè)&
來(lái)說(shuō)明這個(gè)函數(shù)將返回一個(gè)引用;例如:
class Config { private $values = []; // 返回一個(gè) $values 數(shù)組的引用 public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
這會(huì)輸出期待的test
。
但是現(xiàn)在讓事情更困惑一些,請(qǐng)考慮下面的代碼片段:
class Config { private $values; // 使用數(shù)組對(duì)象而不是數(shù)組 public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
如果你認(rèn)為這段代碼會(huì)導(dǎo)致與之前的數(shù)組
例子一樣的「未定義索引」錯(cuò)誤,那就錯(cuò)了。實(shí)際上,這段代碼將會(huì)正常運(yùn)行。原因是,與數(shù)組不同,PHP 永遠(yuǎn)會(huì)將對(duì)象按引用傳遞。(ArrayObject
是一個(gè) SPL 對(duì)象,它完全模仿數(shù)組的用法,但是卻是以對(duì)象來(lái)工作。)
像以上例子說(shuō)明的,你應(yīng)該以引用還是拷貝來(lái)處理通常不是很明顯就能看出來(lái)。因此,理解這些默認(rèn)的行為(例如,變量和數(shù)組以值傳遞;對(duì)象以引用傳遞)并且仔細(xì)查看你將要調(diào)用的函數(shù) API 文檔,看看它是返回一個(gè)值,數(shù)組的拷貝,數(shù)組的引用或是對(duì)象的引用是必要的。
盡管如此,我們要認(rèn)識(shí)到應(yīng)該盡量避免返回一個(gè)數(shù)組或 ArrayObject
,因?yàn)檫@會(huì)讓調(diào)用者能夠修改實(shí)例對(duì)象的私有數(shù)據(jù)。這就破壞了對(duì)象的封裝性。所以最好的方式是使用傳統(tǒng)的「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』
這個(gè)方法讓調(diào)用者可以在不對(duì)私有的$values
數(shù)組本身進(jìn)行公開訪問(wèn)的情況下設(shè)置或者獲取數(shù)組中的任意值。
常見的錯(cuò)誤 #4:在循環(huán)中執(zhí)行查詢
如果像這樣的話,一定不難見到你的 PHP 無(wú)法正常工作。
$models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }
這里也許沒(méi)有真正的錯(cuò)誤, 但是如果你跟隨著代碼的邏輯走下去, 你也許會(huì)發(fā)現(xiàn)這個(gè)看似無(wú)害的調(diào)用$valueRepository->findByValue()
最終執(zhí)行了這樣一種查詢,例如:
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
結(jié)果每輪循環(huán)都會(huì)產(chǎn)生一次對(duì)數(shù)據(jù)庫(kù)的查詢。 因此,假如你為這個(gè)循環(huán)提供了一個(gè)包含 1000 個(gè)值的數(shù)組,它會(huì)對(duì)資源產(chǎn)生 1000 單獨(dú)的請(qǐng)求!如果這樣的腳本在多個(gè)線程中被調(diào)用,他會(huì)有導(dǎo)致系統(tǒng)崩潰的潛在危險(xiǎn)。
因此,至關(guān)重要的是,當(dāng)你的代碼要進(jìn)行查詢時(shí),應(yīng)該盡可能的收集需要用到的值,然后在一個(gè)查詢中獲取所有結(jié)果。
一個(gè)我們平時(shí)常常能見到查詢效率低下的地方 (例如:在循環(huán)中)是使用一個(gè)數(shù)組中的值 (比如說(shuō)很多的 ID )向表發(fā)起請(qǐng)求。檢索每一個(gè) ID 的所有的數(shù)據(jù),代碼將會(huì)迭代這個(gè)數(shù)組,每個(gè) ID 進(jìn)行一次SQL查詢請(qǐng)求,它看起來(lái)常常是這樣:
$data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }
但是 只用一條 SQL 查詢語(yǔ)句就可以更高效的完成相同的工作,比如像下面這樣:
$data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }
因此在你的代碼直接或間接進(jìn)行查詢請(qǐng)求時(shí),一定要認(rèn)出這種查詢。盡可能的通過(guò)一次查詢得到想要的結(jié)果。然而,依然要小心謹(jǐn)慎,不然就可能會(huì)出現(xiàn)下面我們要講的另一個(gè)易犯的錯(cuò)誤…
常見問(wèn)題 #5: 內(nèi)存使用欺騙與低效
一次取多條記錄肯定是比一條條的取高效,但是當(dāng)我們使用 PHP 的 mysql
擴(kuò)展的時(shí)候,這也可能成為一個(gè)導(dǎo)致 libmysqlclient
出現(xiàn)『內(nèi)存不足』(out of memory)的條件。
我們?cè)谝粋€(gè)測(cè)試盒里演示一下,該測(cè)試盒的環(huán)境是:有限的內(nèi)存(512MB RAM),MySQL,和 php-cli
。
我們將像下面這樣引導(dǎo)一個(gè)數(shù)據(jù)表:
// 連接 mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // 創(chuàng)建 400 個(gè)字段 $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 百萬(wàn)行數(shù)據(jù) 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,現(xiàn)在讓我們一起來(lái)看一下內(nèi)存使用情況:
// 連接 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";
輸出結(jié)果是:
Before: 224704 Limit 1: 224704 Limit 10000: 224704
Cool。 看來(lái)就內(nèi)存使用而言,內(nèi)部安全地管理了這個(gè)查詢的內(nèi)存。
為了更加明確這一點(diǎn),我們把限制提高一倍,使其達(dá)到 100,000。 額~如果真這么干了,我們將會(huì)得到如下結(jié)果:
PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11
究竟發(fā)生了啥?
這就涉及到 PHP 的 mysql
模塊的工作方式的問(wèn)題了。它其實(shí)只是個(gè) libmysqlclient
的代理,專門負(fù)責(zé)干臟活累活。每查出一部分?jǐn)?shù)據(jù)后,它就立即把數(shù)據(jù)放入內(nèi)存中。由于這塊內(nèi)存還沒(méi)被 PHP 管理,所以,當(dāng)我們?cè)诓樵兝镌黾酉拗频臄?shù)量的時(shí)候, memory_get_peak_usage()
不會(huì)顯示任何增加的資源使用情況 。我們被『內(nèi)存管理沒(méi)問(wèn)題』這種自滿的思想所欺騙了,所以才會(huì)導(dǎo)致上面的演示出現(xiàn)那種問(wèn)題。 老實(shí)說(shuō),我們的內(nèi)存管理確實(shí)是有缺陷的,并且我們也會(huì)遇到如上所示的問(wèn)題。
如果使用 mysqlnd
模塊的話,你至少可以避免上面那種欺騙(盡管它自身并不會(huì)提升你的內(nèi)存利用率)。 mysqlnd
被編譯成原生的 PHP 擴(kuò)展,并且確實(shí) 會(huì) 使用 PHP 的內(nèi)存管理器。
因此,如果使用 mysqlnd
而不是 mysql
,我們將會(huì)得到更真實(shí)的內(nèi)存利用率的信息:
Before: 232048 Limit 1: 324952 Limit 10000: 32572912
順便一提,這比剛才更糟糕。根據(jù) PHP 的文檔所說(shuō),mysql
使用 mysqlnd
兩倍的內(nèi)存來(lái)存儲(chǔ)數(shù)據(jù), 所以,原來(lái)使用 mysql
那個(gè)腳本真正使用的內(nèi)存比這里顯示的