来自:mjsws > 馆藏分类
配色: 字号:
十个PHP开发者最容易犯的错误
2018-12-25 | 阅:  转:  |  分享 
  
十个PHP开发者最容易犯的错误PHP语言让WEB端程序设计变得简单,这也是它能流行起来的原因。但也是因为它的简单,PHP也慢慢发展
成一个相对复杂的语言,层出不穷的框架,各种语言特性和版本差异都时常让搞的我们头大,不得不浪费大量时间去调试。这篇文章列出了十个最容
易出错的地方,值得我们去注意。易犯错误#1:在foreach循环后留下数组的引用还不清楚PHP中foreach遍
历的工作原理?如果你在想遍历数组时操作数组中每个元素,在foreach循环中使用引用会十分方便,例如$arr=array(
1,2,3,4);foreach($arras&$value){$value=$value2;}//$
arr现在是array(2,4,6,8)问题是,如果你不注意的话这会导致一些意想不到的负面作用。在上述例子,在代码执行
完以后,$value仍保留在作用域内,并保留着对数组最后一个元素的引用。之后与$value相关的操作会无意中修改数组中最后一
个元素的值。你要记住foreach并不会产生一个块级作用域。因此,在上面例子中$value是一个全局引用变量。在for
each遍历中,每一次迭代都会形成一个对$arr下一个元素的引用。当遍历结束后,$value会引用$arr的最后一个
元素,并保留在作用域中这种行为会导致一些不易发现的,令人困惑的bug,以下是一个例子$array=[1,2,3];ech
oimplode('','',$array),"\n";foreach($arrayas&$value){}//
通过引用遍历echoimplode('','',$array),"\n";foreach($arrayas$value)
{}//通过赋值遍历echoimplode('','',$array),"\n";以上代码会输出1,2,31,2
,31,2,2你没有看错,最后一行的最后一个值是2,而不是3,为什么?在完成第一个foreach遍历后,$arr
ay并没有改变,但是像上述解释的那样,$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($arras&$value){$value=$value2;}unset($v
alue);//$value不再引用$arr[3]常见错误#2:误解?isset()?的行为尽管名字叫iss
et,但是?isset()?不仅会在变量不存在的时候返回false,在变量值为null的时候也会返回false。这种行为
比最初出现的问题更为棘手,同时也是一种常见的错误源。看看下面的代码:?$data=fetchRecordFromStorag
e($storage,$identifier);if(!isset($data[''keyShouldBeSet'']){//
dosomethinghereif''keyShouldBeSet''isnotset}开发者想必是想确认?keyS
houldBeSet?是否存在于?$data中。然而,正如上面说的,如果?$data[''keyShouldBeSet'']存在并
且值为null的时候,?isset($data[''keyShouldBeSet''])?也会返回false。所以上面的逻辑是不
严谨的。我们来看另外一个例子:if($_POST[''active'']){$postData=extractSometh
ing($_POST);}//...if(!isset($postData)){echo''postnotactive
'';}上述代码,通常认为,假如?$_POST[''active'']?返回?true,那么?postData?必将存在,因此?iss
et($postData)?也将返回?true。反之,?isset($postData)返回?false?的唯一可能是?$_PO
ST[''active'']?也返回?false。然而事实并非如此!如我所言,如果$postData?存在且被设置为?null,
isset($postData)?也会返回?false?。也就是说,即使?$_POST[''active'']?返回?true,?i
sset($postData)?也可能会返回?false?。再一次说明上面的逻辑不严谨。http://v-8.cc顺便一提,如
果上面代码的意图真的是再次确认?$_POST[''active'']是否返回true,依赖?isset()?来做,不管对于哪种场景
来说都是一种糟糕的决定。更好的做法是再次检查$_POST[''active''],即:if($_POST[''active'']){
$postData=extractSomething($_POST);}//...if($_POST[''active'']
){echo''postnotactive'';}对于这种情况,虽然检查一个变量是否真的存在很重要(即:区分一个变量是未被
设置还是被设置为?null);但是使用?array_key_exists()?这个函数却是个更健壮的解决途径。比如,我们可以像下
面这样重写上面第一个例子:$data=fetchRecordFromStorage($storage,$identifier
);if(!array_key_exists(''keyShouldBeSet'',$data)){//dothisi
f''keyShouldBeSet''isn''tset}另外,通过结合array_key_exists()和?get_de
fined_vars(),我们能更加可靠的判断一个变量在当前作用域中是否存在:?if(array_key_exists(''va
rShouldBeSet'',get_defined_vars())){//variable$varShouldBeSet
existsincurrentscope}常见错误#3:关于通过引用返回与通过值返回的困惑考虑下面的代码片段:cla
ssConfig{private$values=[];publicfunctiongetValues(){re
turn$this->values;}}$config=newConfig();$config->getValues()
[''test'']=''test'';echo$config->getValues()[''test''];如果你运行上面的代码,将
得到下面的输出:PHPNotice:Undefinedindex:testin/path/to/my/script.
phponline21出了什么问题?上面代码的问题在于没有搞清楚通过引用与通过值返回数组的区别。除非你明确告诉PHP
通过引用返回一个数组(例如,使用&),否则PHP默认将会「通过值」返回这个数组。这意味着这个数组的一份拷贝将会被返回,因此被
调函数与调用者所访问的数组并不是同样的数组实例。所以上面对getValues()的调用将会返回$values数组的一份拷
贝,而不是对它的引用。考虑到这一点,让我们重新回顾一下以上例子中的两个关键行://getValues()返回了一个$valu
es数组的拷贝//所以`test`元素被添加到了这个拷贝中,而不是$values数组本身。$config->getValu
es()[''test'']=''test'';//getValues()又返回了另一份$values数组的拷贝//且这份拷
贝中并不包含一个`test`元素(这就是为什么我们会得到「未定义索引」消息)。echo$config->getValues(
)[''test''];一个可能的修改方法是存储第一次通过getValues()返回的$values数组拷贝,然后后续操作都
在那份拷贝上进行;例如:$vals=$config->getValues();$vals[''test'']=''test'';e
cho$vals[''test''];这段代码将会正常工作(例如,它将会输出test而不会产生任何「未定义索引」消息),但是这个方
法可能并不能满足你的需求。特别是上面的代码并不会修改原始的$values数组。如果你想要修改原始的数组(例如添加一个test元素)
,就需要修改getValues()函数,让它返回一个$values数组自身的引用。通过在函数名前面添加一个&来说明这个函数将返回一
个引用;例如:classConfig{private$values=[];//返回一个$values数组的引用
publicfunction&getValues(){return$this->values;}}$config=
newConfig();$config->getValues()[''test'']=''test'';echo$config->
getValues()[''test''];这会输出期待的test。但是现在让事情更困惑一些,请考虑下面的代码片段:classC
onfig{private$values;//使用数组对象而不是数组publicfunction__construc
t(){$this->values=newArrayObject();}publicfunctiongetVal
ues(){return$this->values;}}$config=newConfig();$config->g
etValues()[''test'']=''test'';echo$config->getValues()[''test''];如果
你认为这段代码会导致与之前的数组例子一样的「未定义索引」错误,那就错了。实际上,这段代码将会正常运行。原因是,与数组不同,PHP
永远会将对象按引用传递。(ArrayObject是一个SPL对象,它完全模仿数组的用法,但是却是以对象来工作。)像以上例子
说明的,你应该以引用还是拷贝来处理通常不是很明显就能看出来。因此,理解这些默认的行为(例如,变量和数组以值传递;对象以引用传递)并
且仔细查看你将要调用的函数API文档,看看它是返回一个值,数组的拷贝,数组的引用或是对象的引用是必要的。尽管如此,我们要认识
到应该尽量避免返回一个数组或ArrayObject,因为这会让调用者能够修改实例对象的私有数据。这就破坏了对象的封装性。所以最好
的方式是使用传统的「getters」和「setters」,例如:classConfig{private$values=[
];publicfunctionsetValue($key,$value){$this->values[$key]=
$value;}publicfunctiongetValue($key){return$this->values[
$key];}}$config=newConfig();$config->setValue(''testKey'',''tes
tValue'');echo$config->getValue(''testKey'');//输出『testValue』这
个方法让调用者可以在不对私有的$values数组本身进行公开访问的情况下设置或者获取数组中的任意值。常见的错误#4:在循环中执
行查询如果像这样的话,一定不难见到你的PHP无法正常工作。$models=[];foreach($inputValue
sas$inputValue){$models[]=$valueRepository->findByValue($in
putValue);}这里也许没有真正的错误,但是如果你跟随着代码的逻辑走下去,你也许会发现这个看似无害的调用$valueR
epository->findByValue()?最终执行了这样一种查询,例如:$result=$connection->qu
ery("SELECT`x`,`y`FROM`values`WHERE`value`=".$inputValue);
结果每轮循环都会产生一次对数据库的查询。因此,假如你为这个循环提供了一个包含1000个值的数组,它会对资源产生1000
单独的请求!如果这样的脚本在多个线程中被调用,他会有导致系统崩溃的潜在危险。因此,至关重要的是,当你的代码要进行查询时,应该尽可
能的收集需要用到的值,然后在一个查询中获取所有结果。一个我们平时常常能见到查询效率低下的地方(例如:在循环中)是使用一个数组中
的值(比如说很多的ID)向表发起请求。检索每一个ID的所有的数据,代码将会迭代这个数组,每个ID进行一次SQL查询请
求,它看起来常常是这样:$data=[];foreach($idsas$id){$result=$connect
ion->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;}}因
此在你的代码直接或间接进行查询请求时,一定要认出这种查询。尽可能的通过一次查询得到想要的结果。然而,依然要小心谨慎,不然就可能会出
现下面我们要讲的另一个易犯的错误...http://www.77884.net常见问题#5:内存使用欺骗与低效一次取多条记
录肯定是比一条条的取高效,但是当我们使用PHP的?mysql?扩展的时候,这也可能成为一个导致?libmysqlclient?
出现『内存不足』(outofmemory)的条件。我们在一个测试盒里演示一下,该测试盒的环境是:有限的内存(512MBRA
M),MySQL,和php-cli。我们将像下面这样引导一个数据表://连接mysql$connection=new
mysqli(''localhost'',''username'',''password'',''database'');//创建400
个字段$query=''CREATETABLE`test`(`id`INTNOTNULLPRIMARYKEYA
UTO_INCREMENT'';for($col=0;$col<400;$col++){$query.=",
`col$col`CHAR(10)NOTNULL";}$query.='');'';$connection->query($
query);//写入2百万行数据for($row=0;$row<2000000;$row++){$que
ry="INSERTINTO`test`VALUES($row";for($col=0;$col<400
;$col++){$query.='',''.mt_rand(1000000000,9999999999);}$
query.='')'';$connection->query($query);}OK,现在让我们一起来看一下内存使用情况:/
/连接mysql$connection=newmysqli(''localhost'',''username'',''pass
word'',''database'');echo"Before:".memory_get_peak_usage()."\
n";$res=$connection->query(''SELECT`x`,`y`FROM`test`LIMIT1''
);echo"Limit1:".memory_get_peak_usage()."\n";$res=$conne
ction->query(''SELECT`x`,`y`FROM`test`LIMIT10000'');echo"Limi
t10000:".memory_get_peak_usage()."\n";输出结果是:Before:224704
Limit1:224704Limit10000:224704Cool。看来就内存使用而言,内部安全地管理了这个查询的内
存。为了更加明确这一点,我们把限制提高一倍,使其达到100,000。额~如果真这么干了,我们将会得到如下结果:PHPWar
ning:mysqli::query():(HY000/2013):LostconnectiontoMySQLse
rverduringqueryin/root/test.phponline11究竟发生了啥?这就涉及到PHP
的?mysql模块的工作方式的问题了。它其实只是个libmysqlclient的代理,专门负责干脏活累活。每查出一部分数据后
,它就立即把数据放入内存中。由于这块内存还没被PHP管理,所以,当我们在查询里增加限制的数量的时候,memory_get_p
eak_usage()?不会显示任何增加的资源使用情况?。我们被『内存管理没问题』这种自满的思想所欺骗了,所以才会导致上面的演示出
现那种问题。老实说,我们的内存管理确实是有缺陷的,并且我们也会遇到如上所示的问题。如果使用?mysqlnd?模块的话,你至少可
以避免上面那种欺骗(尽管它自身并不会提升你的内存利用率)。?mysqlnd被编译成原生的PHP扩展,并且确实会使用PH
P的内存管理器。因此,如果使用?mysqlnd?而不是?mysql,我们将会得到更真实的内存利用率的信息:Before:23
2048Limit1:324952Limit10000:32572912顺便一提,这比刚才更糟糕。根据PHP的文档所
说,mysql?使用?mysqlnd?两倍的内存来存储数据,所以,原来使用?mysql?那个脚本真正使用的内存比这里显示的更多(
大约是两倍)。为了避免出现这种问题,考虑限制一下你查询的数量,使用一个较小的数字来循环,像这样:$totalNumberToFe
tch=10000;$portionSize=100;for($i=0;$i<=ceil($totalNumb
erToFetch/$portionSize);$i++){$limitFrom=$portionSize$i
;$res=$connection->query("SELECT`x`,`y`FROM`test`LIMIT$l
imitFrom,$portionSize");}当我们把这个常见错误和上面的?常见错误#4?结合起来考虑的时候,就会意识
到我们的代码理想需要在两者间实现一个平衡。是让查询粒度化和重复化,还是让单个查询巨大化。生活亦是如此,平衡不可或缺;哪一个极端都不
好,都可能会导致PHP无法正常运行。常见错误#6:忽略Unicode/UTF-8的问题从某种意义上说,这实际上是P
HP本身的一个问题,而不是你在调试PHP时遇到的问题,但是它从未得到妥善的解决。PHP6的核心就是要做到支持Unico
de。但是随着PHP6在2010年的暂停而搁置了。这并不意味着开发者能够避免?正确处理UTF-8?并避免做出所有字符
串必须是『古老的ASCII』的假设。没有正确处理非ASCII字符串的代码会因为引入粗糙的?海森堡bug(heisenbug
s)?而变得臭名昭著。当一个名字包含『Schr?dinger』的人注册到你的系统时,即使简单的?strlen($_POST[''n
ame''])调用也会出现问题。下面是一些可以避免出现这种问题的清单:如果你对UTF-8还不了解,那么你至少应该了解下基础的
东西。?这儿?有个很好的引子。确保使用?mb_?函数代替老旧的字符串处理函数(需要先保证你的PHP构建版本开启了『多字节』(
multibyte)扩展)。确保你的数据库和表设置了Unicode编码(许多MySQL的构建版本仍然默认使用?latin1
?)。记住?json_encode()?会转换非ASCII标识(比如:『Schr?dinger』会被转换成『Schru0
0f6dinger』),但是serialize()?不会转换。确保PHP文件也是UTF-8编码,以避免在连接硬编码字符
串或者配置字符串常量的时候产生冲突。常见错误#7:认为$_POST总是包含你POST的数据不管它的名称,$_PO
ST数组不是总是包含你POST的数据,他也有可能会是空的。为了理解这一点,让我们来看一下下面这个例子。假设我们使用jQu
ery.ajax()模拟一个服务请求,如下://js$.ajax({url:''http://my.site/some/pa
th'',method:''post'',data:JSON.stringify({a:''a'',b:''b''}),cont
entType:''application/json''});(顺带一提,注意这里的contentType:''applicat
ion/json''。我们用JSON类型发送数据,这在接口中非常流行。这在?AngularJS$httpservice?里
是默认的发送数据的类型。)在我们举例子的服务端,我们简单的打印一下?$_POST数组://phpvar_dump($_POS
T);奇怪的是,结果如下:array(0){}为什么?我们的JSON串{a:''a'',b:''b''}究竟发生了什
么?原因在于?当内容类型为application/x-www-form-urlencoded或者multipart/for
m-data的时候PHP只会自动解析一个POST的有效内容。这里面有历史的原因---这两种内容类型是在PHP的
$_POST实现前就已经在使用了的两个重要的类型。所以不管使用其他任何内容类型(即使是那些现在很流行的,像?applicati
on/json),PHP也不会自动加载到POST的有效内容。既然$_POST是一个超级全局变量,如果我们重写一次
(在我们的脚本里尽可能早的),被修改的值(包括POST的有效内容)将可以在我们的代码里被引用。这很重要因为?$_POST已经
被PHP框架和几乎所有的自定义的脚本普遍使用来获取和传递请求数据。所以,举个例子,当处理一个内容类型为?applicatio
n/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";}如果你的答案是a到z,那么你可能会对这是一个错误答案感到吃惊。没错,它确实会输出a到z,但
是,它还会继续输出aa到yz。我们一起来看一下这是为什么。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里有合适的方式在循环中输出a到z的值:for($i=ord(''a'');
$i<=ord(''z'');$i++){echochr($i)."\n";}或者是这样:$letters=r
ange(''a'',''z'');for($i=0;$ietters[$i]."\n";}常见错误#9:忽视代码规范尽管忽视代码标准并不直接导致需要去调试PHP代码,但
这可能是所有需要谈论的事情里最重要的一项。在一个项目中忽视代码规范能够导致大量的问题。最乐观的预计,前后代码不一致(在此之前每个
开发者都在“做自己的事情”)。但最差的结果,PHP代码不能运行或者很难(有时是不可能的)去顺利通过,这对于调试代码、提升性能、
维护项目来说也是困难重重。并且这意味着降低你们团队的生产力,增加大量的额外(或者至少是本不必要的)精力消耗。幸运的是对于PHP
开发者来说,存在PHP编码标准建议(PSR),它由下面的五个标准组成:PSR-0:自动加载标准PSR-1:基础编码标准P
SR-2:编码风格指导PSR-3:日志接口PSR-4:自动加载增强版PSR起初是由市场上最大的组织平台维护者创造的。Z
end,Drupal,Symfony,Joomla和?其他?为这些标准做出了贡献,并一直遵守它们。甚至,多年前试图成为一个
标准的PEAR,现在也加入到PSR中来。某种意义上,你的代码标准是什么几乎是不重要的,只要你遵循一个标准并坚持下去,但一
般来讲,跟随PSR是一个很不错的主意,除非你的项目上有其他让人难以抗拒的理由。越来越多的团队和项目正在遵从PSR。在这一点
上,大部分的PHP开发者达成了共识,因此使用PSR代码标准,有利于使新加入团队的开发者对你的代码标准感到更加的熟悉与舒适。
常见错误#10:?滥用empty()一些PHP开发者喜欢对几乎所有的事情使用?empty()?做布尔值检验。不过,在
一些情况下,这会导致混乱。首先,让我们回到数组和?ArrayObject?实例(和数组类似)。考虑到他们的相似性,很容易假设它们
的行为是相同的。然而,事实证明这是一个危险的假设。举例,在PHP5.0中://PHP5.0或后续版本:$array=
[];var_dump(empty($array));//输出bool(true)$array=new
ArrayObject();var_dump(empty($array));//输出bool(false)//
为什么这两种方法不产生相同的输出呢?更糟糕的是,PHP5.0之前的结果可能是不同的://PHP5.0之前:$array
=[];var_dump(empty($array));//输出bool(false)$array=n
ewArrayObject();var_dump(empty($array));//输出bool(false
)这种方法上的不幸是十分普遍的。比如,在ZendFramework2下的?Zend\Db\TableGateway?的
TableGateway::select()结果中调用?current()时返回数据的方式,正如文档所表明的那样。开发者很容易
就会变成此类数据错误的受害者。为了避免这些问题的产生,更好的方法是使用count()去检验空数组结构://注意这会在PH
P的所有版本中发挥作用(5.0前后都是):$array=[];var_dump(count($array));
//输出int(0)$array=newArrayObject();var_dump(count($array))
;//输出int(0)顺便说一句,由于PHP将0转换为false,count()能够被使用
在if()条件内部去检验空数组。同样值得注意的是,在PHP中,count()在数组中是常量复杂度(O(1)操作)
,这更清晰的表明它是正确的选择。另一个使用empty()产生危险的例子是当它和魔术方法_get()一起使用。我们来定义两
个类并使其都有一个test属性。http://www.44226.net首先我们定义包含test公共属性的?Regula
r?类。classRegular{public$test=''value'';}然后我们定义?Magic?类,这里使用魔术
方法?__get()?来操作去访问它的?test?属性:classMagic{private$values=[''test
''=>''value''];publicfunction__get($key){if(isset($this->val
ues[$key])){return$this->values[$key];}}}好了,现在我们尝试去访问每个类中的test属性看看会发生什么:$regular=newRegular();var_dump($regular->test);//输出string(4)"value"$magic=newMagic();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:Undefinedproperty:Regular::$nonExistantTestin/path/to/test.phponline10CallStack:0.00122347041.{main}()/path/to/test.php:0所以这里的主要观点是empty()方法应该被谨慎地使用,因为如果不小心的话它可能导致混乱--甚至潜在的误导--结果。总结PHP的易用性让开发者陷入一种虚假的舒适感,语言本身的一些细微差别和特质,可能花费掉你大量的时间去调试。这些可能会导致PHP程序无法正常工作,并导致诸如此处所述的问题。PHP在其20年的历史中,已经发生了显著的变化。花时间去熟悉语言本身的微妙之处是值得的,因为它有助于确保你编写的软件更具可扩展性,健壮和可维护性。
献花(0)
+1
(本文系mjsws首藏)