天天看點

PHP 程式設計中 10 個最常見的錯誤,你犯過幾個?

PHP 程式設計中 10 個最常見的錯誤,你犯過幾個?

<a target="_blank"></a>

在foreach循環中,如果我們需要更改疊代的元素或是為了提高效率,運用引用是一個好辦法:

<code>$arr = array(1,2,3,4);</code>

<code>foreach($arr as&amp;$value){</code>

<code>    $value = $value *2;</code>

<code>}</code>

<code>// $arr is now array(2, 4, 6, 8)</code>

這裡有個問題很多人會迷糊。循環結束後,$value并未銷毀,$value其實是數組中最後一個元素的引用,這樣在後續對$value的使用中,如果不知道這一點,會引發一些莫名奇妙的錯誤:)看看下面這段代碼:

<code>$array =[1,2,3];</code>

<code>echo implode(',', $array),"\n";</code>

<code></code>

<code>foreach($array as&amp;$value){}    // by reference</code>

<code>foreach($array as $value){}     // by value (i.e., copy)</code>

上面代碼的運作結果如下:

<code>1,2,3</code>

<code>1,2,2</code>

你猜對了嗎?為什麼是這個結果呢?

我們來分析下。第一個循環過後,$value是數組中最後一個元素的引用。第二個循環開始:

第一步:複制$arr[0]到$value(注意此時$value是$arr[2]的引用),這時數組變成[1,2,1]

第二步:複制$arr[1]到$value,這時數組變成[1,2,2]

第三步:複制$arr[2]到$value,這時數組變成[1,2,2]

綜上,最終結果就是1,2,2

避免這種錯誤最好的辦法就是在循環後立即用unset函數銷毀變量:

<code>unset($value);   // $value no longer references $arr[3]</code>

對于isset()函數,變量不存在時會傳回false,變量值為null時也會傳回false。這種行為很容易把人弄迷糊。。。看下面的代碼:

<code>$data = fetchrecordfromstorage($storage, $identifier);</code>

<code>if(!isset($data['keyshouldbeset']){</code>

<code>    // do something here if 'keyshouldbeset' is not set</code>

寫這段代碼的人本意可能是如果$data[‘keyshouldbeset’]未設定,則執行對應邏輯。但問題在于即使$data[‘keyshouldbeset’]已設定,但設定的值為null,還是會執行對應的邏輯,這就不符合代碼的本意了。

下面是另外一個例子:

<code>if($_post['active']){</code>

<code>    $postdata = extractsomething($_post);</code>

<code>// ...</code>

<code>if(!isset($postdata)){</code>

<code>    echo 'post not active';</code>

上 面的代碼假設$_post[‘active’]為真,那麼$postdata應該被設定,是以isset($postdata)會傳回true。反之,上 面代碼假設isset($postdata)傳回false的唯一途徑就是$_post[‘active’]也傳回false。

真是這樣嗎?當然不是!

即使$_post[‘active’]傳回true,$postdata也有可能被設定為null,這時isset($postdata)就會傳回false。這就不符合代碼的本意了。

如果上面代碼的本意僅是檢測$_post[‘active’]是否為真,下面這樣實作會更好:

判斷一個變量是否真正被設定(區分未設定和設定值為null),array_key_exists()函數或許更好。重構上面的第一個例子,如下:

<code>if(! array_key_exists('keyshouldbeset', $data)){</code>

<code>    // do this if 'keyshouldbeset' isn't set</code>

另外,結合get_defined_vars()函數,我們可以更加可靠的檢測變量在目前作用域内是否被設定:

<code>if(array_key_exists('varshouldbeset', get_defined_vars())){</code>

<code>    // variable $varshouldbeset exists in current scope</code>

考慮下面的代碼:

<code>classconfig</code>

<code>{</code>

<code>    private $values =[];</code>

<code>    publicfunction getvalues(){</code>

<code>        return $this-&gt;values;</code>

<code>    }</code>

<code>$config =newconfig();</code>

<code>$config-&gt;getvalues()['test']='test';</code>

<code>echo $config-&gt;getvalues()['test'];</code>

運作上面的代碼,将會輸出下面的内容:

<code>php notice:  undefined index: test in/path/to/my/script.php on line 21</code>

問題出在哪呢?問題就在于上面的代碼混淆了傳回值和傳回引用。在php中,除非你顯示的指定傳回引用,否則對于數組php是值傳回,也就是數組的拷貝。是以上面代碼對傳回數組指派,實際是對拷貝數組進行指派,非原數組指派。

<code>// getvalues() returns a copy of the $values array, so this adds a 'test' element</code>

<code>// to a copy of the $values array, but not to the $values array itself.</code>

<code>// getvalues() again returns another copy of the $values array, and this copy doesn't</code>

<code>// contain a 'test' element (which is why we get the "undefined index" message).</code>

下面是一種可能的解決辦法,輸出拷貝的數組,而不是原數組:

<code>$vals = $config-&gt;getvalues();</code>

<code>$vals['test']='test';</code>

<code>echo $vals['test'];</code>

如果你就是想要改變原數組,也就是要反回數組引用,那應該如何處理呢?辦法就是顯示指定傳回引用即可:

<code>    // return a reference to the actual $values array</code>

<code>    publicfunction&amp;getvalues(){</code>

經過改造後,上面代碼将會像你期望那樣會輸出test。

我們再來看一個例子會讓你更迷糊的例子:

<code>    private $values;</code>

<code>    // using arrayobject rather than array</code>

<code>    publicfunction __construct(){</code>

<code>        $this-&gt;values =newarrayobject();</code>

如果你想的是會和上面一樣輸出“ undefined index”錯誤,那你就錯了。代碼會正常輸出“test”。原因在于php對于對象預設就是按引用傳回的,而不是按值傳回。

綜上所述,我們在使用函數傳回值時,要弄清楚是值傳回還是引用傳回。php中對于對象,預設是引用傳回,數組和内置基本類型預設均按值傳回。這個要與其它語言差別開來(很多語言對于數組是引用傳遞)。

像其它語言,比如java或c#,利用getter或setter來通路或設定類屬性是一種更好的方案,當然php預設不支援,需要自己實作:

<code>    publicfunction setvalue($key, $value){</code>

<code>        $this-&gt;values[$key]= $value;</code>

<code>    publicfunction getvalue($key){</code>

<code>        return $this-&gt;values[$key];</code>

<code>$config-&gt;setvalue('testkey','testvalue');</code>

<code>echo $config-&gt;getvalue('testkey');    // echos 'testvalue'</code>

上面的代碼給調用者可以通路或設定數組中的任意值而不用給與數組public通路權限。感覺怎麼樣:) 

在php程式設計中發現類似下面的代碼并不少見:

<code>$models =[];</code>

<code>foreach($inputvalues as $inputvalue){</code>

<code>    $models[]= $valuerepository-&gt;findbyvalue($inputvalue);</code>

當然上面的代碼是沒有什麼錯誤的。問題在于我們在疊代過程中$valuerepository-&gt;findbyvalue()可能每次都執行了sql查詢:

<code>$result = $connection-&gt;query("select `x`,`y` from `values` where `value`=". $inputvalue);</code>

如果疊代了10000次,那麼你就分别執行了10000次sql查詢。如果這樣的腳本在多線程程式中被調用,那很可能你的系統就挂了。。。

在編寫代碼過程中,你應該要清楚什麼時候應該執行sql查詢,盡可能一次sql查詢取出所有資料。

有一種業務場景,你很可能會犯上述錯誤。假設一個表單送出了一系列值(假設為ids),然後為了取出所有id對應的資料,代碼将周遊ids,分别對每個id執行sql查詢,代碼如下所示:

<code>$data =[];</code>

<code>foreach($ids as $id){</code>

<code>    $result = $connection-&gt;query("select `x`, `y` from `values` where `id` = ". $id);</code>

<code>    $data[]= $result-&gt;fetch_row();</code>

但同樣的目的可以在一個sql中更加高效的完成,代碼如下:

<code>if(count($ids)){</code>

<code>    $result = $connection-&gt;query("select `x`, `y` from `values` where `id` in (". implode(',', $ids));</code>

<code>    while($row = $result-&gt;fetch_row()){</code>

<code>        $data[]= $row;</code>

一次sql查詢擷取多條記錄比每次查詢擷取一條記錄效率肯定要高,但如果你使用的是php中的mysql擴充,那麼一次擷取多條記錄就很可能會導緻記憶體溢出。

我們可以寫代碼來實驗下(測試環境: 512mb ram、mysql、php-cli):

<code>// connect to mysql</code>

<code>$connection =new mysqli('localhost','username','password','database');</code>

<code>// create table of 400 columns</code>

<code>$query ='create table `test`(`id` int not null primary key auto_increment';</code>

<code>for($col =0; $col &lt;400; $col++){</code>

<code>    $query .=", `col$col` char(10) not null";</code>

<code>$query .=');';</code>

<code>$connection-&gt;query($query);</code>

<code>// write 2 million rows</code>

<code>for($row =0; $row &lt;2000000; $row++){</code>

<code>    $query ="insert into `test` values ($row";</code>

<code>    for($col =0; $col &lt;400; $col++){</code>

<code>        $query .=', '. mt_rand(1000000000,9999999999);</code>

<code>    $query .=')';</code>

<code>    $connection-&gt;query($query);</code>

現在來看看資源消耗:

<code>echo "before: ". memory_get_peak_usage()."\n";</code>

<code>$res = $connection-&gt;query('select `x`,`y` from `test` limit 1');</code>

<code>echo "limit 1: ". memory_get_peak_usage()."\n";</code>

<code>$res = $connection-&gt;query('select `x`,`y` from `test` limit 10000');</code>

<code>echo "limit 10000: ". memory_get_peak_usage()."\n";</code>

輸出結果如下:

<code>before:224704</code>

<code>limit1:224704</code>

<code>limit10000:224704</code>

根據記憶體使用量來看,貌似一切正常。為了更加确定,試着一次擷取100000條記錄,結果程式得到如下輸出:

<code>php warning:  mysqli::query():(hy000/2013):</code>

<code>     lost connection to mysql server during query in/root/test.php on line 11</code>

這是怎麼回事呢?

問 題出在php的mysql子產品的工作方式,mysql子產品實際上就是libmysqlclient的一個代理。在查詢擷取多條記錄的同時,這些記錄會直接 儲存在記憶體中。由于這塊記憶體不屬于php的記憶體子產品所管理,是以我們調用memory_get_peak_usage()函數所獲得的值并非真實使用記憶體 值,于是便出現了上面的問題。

我們可以使用mysqlnd來代替mysql,mysqlnd編譯為php自身擴充,其記憶體使用由php記憶體管理子產品所控制。如果我們用mysqlnd來實作上面的代碼,則會更加真實的反應記憶體使用情況:

<code>before:232048</code>

<code>limit1:324952</code>

<code>limit10000:32572912</code>

更加糟糕的是,根據php的官方文檔,mysql擴充存儲查詢資料使用的記憶體是mysqlnd的兩倍,是以原來的代碼使用的記憶體是上面顯示的兩倍左右。

為了避免此類問題,可以考慮分幾次完成查詢,減小單次查詢資料量:

<code>$totalnumbertofetch =10000;</code>

<code>$portionsize =100;</code>

<code>for($i =0; $i &lt;= ceil($totalnumbertofetch / $portionsize); $i++){</code>

<code>    $limitfrom = $portionsize * $i;</code>

<code>    $res = $connection-&gt;query(</code>

<code>                         "select `x`,`y` from `test` limit $limitfrom, $portionsize");</code>

聯系上面提到的錯誤4可以看出,在實際的編碼過程中,要做到一種平衡,才能既滿足功能要求,又能保證性能。

php程式設計中,在處理非ascii字元時,會遇到一些問題,要很小心的去對待,要不然就會錯誤遍地。舉個簡單的例子,strlen($name),如果$name包含非ascii字元,那結果就有些出乎意料。在此給出一些建議,盡量避免此類問題:

最好使用mb_*函數來處理字元串,避免使用老的字元串處理函數。這裡要確定php的“multibyte”擴充已開啟。

資料庫和表最好使用unicode編碼。

知道jason_code()函數會轉換非ascii字元,但serialize()函數不會。

php代碼源檔案最好使用不含bom的utf-8格式。

php中的$_post并非總是包含表單post送出過來的資料。假設我們通過 jquery.ajax() 方法向伺服器發送了post請求:

<code>// js</code>

<code>$.ajax({</code>

<code>    url:'http://my.site/some/path',</code>

<code>    method:'post',</code>

<code>    data: json.stringify({a:'a', b:'b'}),</code>

<code>    contenttype:'application/json'</code>

<code>});</code>

注意代碼中的 contenttype: ‘application/json’ ,我們是以json資料格式來發送的資料。在服務端,我們僅輸出$_post數組:

<code>// php</code>

<code>var_dump($_post);</code>

你會很驚奇的發現,結果是下面所示:

<code>array(0){}</code>

為什麼是這樣的結果呢?我們的json資料 {a: ‘a’, b: ‘b’} 哪去了呢?

答案就是php僅僅解析content-type為 application/x-www-form-urlencoded 或 multipart/form-data的http請求。之是以這樣是因為曆史原因,php最初實作$_post時,最流行的就是上面兩種類型。是以雖說現在有些類型(比如application/json)很流行,但php中還是沒有去實作自動處理。

因為$_post是全局變量,是以更改$_post會全局有效。是以對于content-type為 application/json的請求,我們需要手工去解析json資料,然後修改$_post變量。

<code>$_post = json_decode(file_get_contents('php://input'),true);</code>

此時,我們再去輸出$_post變量,則會得到我們期望的輸出:

<code>array(2){["a"]=&gt;string(1)"a"["b"]=&gt;string(1)"b"}</code>

看看下面的代碼,猜測下會輸出什麼:

<code>for($c ='a'; $c &lt;='z'; $c++){</code>

<code>    echo $c ."\n";</code>

如果你的回答是輸出’a’到’z’,那麼你會驚奇的發現你的回答是錯誤的。

不錯,上面的代碼的确會輸出’a’到’z’,但除此之外,還會輸出’aa’到’yz’。我們來分析下為什麼會是這樣的結果。

在php中不存在char資料類型,隻有string類型。明白這點,那麼對’z’進行遞增操作,結果則為’aa’。對于字元串比較大小,學過c的應該都知道,’aa’是小于’z’的。這也就解釋了為何會有上面的輸出結果。

如果我們想輸出’a’到’z’,下面的實作是一種不錯的辦法:

<code>for($i = ord('a'); $i &lt;= ord('z'); $i++){</code>

<code>    echo chr($i)."\n";</code>

或者這樣也是ok的:

<code>$letters = range('a','z');</code>

<code>for($i =0; $i &lt; count($letters); $i++){</code>

<code>    echo $letters[$i]."\n";</code>

<code>}</code> 

雖說忽略編碼标準不會導緻錯誤或是bug,但遵循一定的編碼标準還是很重要的。

沒有統一的編碼标準會使你的項目出現很多問題。最明顯的就是你的項目代碼不具有一緻性。更壞的地方在于,你的代碼将更加難以調試、擴充和維護。這也就意味着你的團隊效率會降低,包括做一些很多無意義的勞動。

對于php開發者來說,是比較幸運的。因為有php編碼标準推薦(psr),由下面5個部分組成:

<a href="http://www.php-fig.org/psr/psr-0/" target="_blank">psr-0:自動加載标準</a>

<a href="http://www.php-fig.org/psr/psr-1/" target="_blank">psr-1:基本編碼标準</a>

<a href="http://www.php-fig.org/psr/psr-2/" target="_blank">psr-2:編碼風格指南</a>

<a href="http://www.php-fig.org/psr/psr-3/" target="_blank">psr-3:日志接口标準</a>

<a href="http://www.php-fig.org/psr/psr-4/" target="_blank">psr-4:自動加載</a>

psr最初由php社群的幾個大的團體所建立并遵循。zend, drupal, symfony, joomla及其它的平台都為此标準做過貢獻并遵循這個标準。即使是pear,早些年也想讓自己成為一個标準,但現在也加入了psr陣營。

在 某些情況下,使用什麼編碼标準是無關緊要的,隻要你使用一種編碼風格并一直堅持使用即可。但是遵循psr标準不失為一個好辦法,除非你有什麼特殊的原因要 自己弄一套。現在越來越多的項目都開始使用psr,大部分的php開發者也在使用psr,是以使用psr會讓新加入你團隊的成員更快的熟悉項目,寫代碼時 也會更加舒适。 

一些php開發人員喜歡用empty()函數去對變量或表達式做布爾判斷,但在某些情況下會讓人很困惑。

首先我們來看看php中的數組array和數組對象arrayobject。看上去好像沒什麼差別,都是一樣的。真的這樣嗎?

<code>// php 5.0 or later:</code>

<code>$array =[];</code>

<code>var_dump(empty($array));        // outputs bool(true)  </code>

<code>$array =newarrayobject();</code>

<code>var_dump(empty($array));        // outputs bool(false)</code>

<code>// why don't these both produce the same output?</code>

讓事情變得更複雜些,看看下面的代碼:

<code>// prior to php 5.0:</code>

<code>var_dump(empty($array));        // outputs bool(false)  </code>

很不幸的是,上面這種方法很受歡迎。例如,在zend framework 2中,zend\db\tablegateway 在 tablegateway::select() 結果集上調用 current() 方法傳回資料集時就是這麼幹的。開發人員很容易就會踩到這個坑。

為了避免這些問題,檢查一個數組是否為空最後的辦法是用 count() 函數:

<code>// note that this work in all versions of php (both pre and post 5.0):</code>

<code>var_dump(count($array));        // outputs int(0)</code>

在這順便提一下,因為php中會将數值0認為是布爾值false,是以 count() 函數可以直接用在 if 條件語句的條件判斷中來判斷數組是否為空。另外,count() 函數對于數組來說複雜度為o(1),是以用 count() 函數是一個明智的選擇。

再來看一個用 empty() 函數很危險的例子。當在魔術方法 __get() 中結合使用 empty() 函數時,也是很危險的。我們來定義兩個類,每個類都有一個 test 屬性。

首先我們定義 regular 類,有一個 test 屬性:

<code>classregular</code>

<code>    public $test ='value';</code>

然後我們定義 magic 類,并用 __get() 魔術方法來通路它的 test 屬性:

<code>classmagic</code>

<code>    private $values =['test'=&gt;'value'];</code>

<code>    publicfunction __get($key)</code>

<code>    {</code>

<code>        if(isset($this-&gt;values[$key])){</code>

<code>            return $this-&gt;values[$key];</code>

<code>        }</code>

好了。我們現在來看看通路各個類的 test 屬性會發生什麼:

<code>$regular =newregular();</code>

<code>var_dump($regular-&gt;test);    // outputs string(4) "value"</code>

<code>$magic =newmagic();</code>

<code>var_dump($magic-&gt;test);      // outputs string(4) "value"</code>

到目前為止,都還是正常的,沒有讓我們感到迷糊。

但在 test 屬性上使用 empty() 函數會怎麼樣呢?

<code>var_dump(empty($regular-&gt;test));    // outputs bool(false)</code>

<code>var_dump(empty($magic-&gt;test));      // outputs bool(true)</code>

結果是不是很意外?

很不幸的是,如果一個類使用魔法 __get() 函數來通路類屬性的值,沒有簡單的方法來檢查屬性值是否為空或是不存在。在類作用域外,你隻能檢查是否傳回 null 值,但這并不一定意味着沒有設定相應的鍵,因為鍵值可以被設定為 null 。

相比之下,如果我們通路 regular 類的一個不存在的屬性,則會得到一個類似下面的notice消息:

<code>notice:undefined property:regular::$nonexistanttest in/path/to/test.php on line 10</code>

<code>callstack:</code>

<code>    0.0012     234704   1.{main}()/path/to/test.php:0</code>

是以,對于 empty() 函數,我們要小心的使用,要不然的話就會結果出乎意料,甚至潛在的誤導你。

本文來自雲栖社群合作夥伴“linux中國”,原文釋出日期:2015-10-13