26.1.2 运行时错误

运行时错误更难检测到,也更难修改。一个脚本可能包含语法错误,也可能没有语法错误。如果脚本包含一个语法错误,解析器能够检测到它。而运行时错误则不会只由脚本的内容导致。它们可能出现在脚本交互的过程中或者其他的事件或条件下。

如下所示的语句:


require('filename.php');


这是一个完全有效的PHP语句,它没有任何语法错误。

然而,上述语句却可能产生运行时错误。如果执行该语句时,filename.php并不存在或者运行脚本的用户对该文件没有“读”权限,就可能出现如下所示的出错信息:


Fatal error:main()[function.require]:Failed opening required'filename.php'

(include_path='.:/usr/local/lib/php')in

/home/book/public_html/phpmysql4e/chapter26/error.php on line 1


尽管代码没有任何错误,但是由于运行代码所依赖的文件在代码运行的不同时刻可能存在,也可能不存在,所以就可能产生运行时的错误。

如下所示的3个语句都是有效的PHP语句。不幸的是,当3个语句结合到一起的时候,它们是不可能得到运行结果的——因为出现了被0除的现象:


$i=10;

$j=0;

$k=$i/$j;


该代码将产生如下所示的警告:


Warning:Division by zero in

/home/book/public_html/phpmysql4e/chapter26/div0.php on line 3


这种错误很容易修改。没有人会故意编写被0除的代码,但是忽略检查用户输入却经常会导致这种错误类型。

如下所示的代码有时候将产生相同的错误,但是可能会很难隔离和修改,因为它只发生在某些时候:


$i=10;

$k=$i/$_REQUEST['input'];


这是在测试代码时最经常看到的运行时错误之一。

通常,以下操作易导致运行时的错误:

■调用不存在的函数

■读写文件

■与MySQL或其他数据库的交互

■连接到网络服务

■检查输入数据失败

在接下来的内容中,我们将简单介绍以上每一种容易导致错误的原因。

1.调用不存在的函数

在偶然的情况下,调用不存在的函数的操作比较容易发生。此外,内置函数也经常会出现命名不一致的情况。为什么strip_tags()函数名称中间有一个下划线,但是stripslashes()却没有呢?

不但在当前脚本里容易调用自己编写的、不存在的函数,而且在其他地方也容易这样调用。如果代码包含了对一个不存在函数的调用,例如:


nonexistent_function();



mispeled_function();


将出现如下所示的出错信息:


Fatal error:Call to undefined function:nonexistent_function()

in/home/book/public_html/phpmysql4e/chapter26/error.php on line 1


相似地,如果调用一个存在的函数,但是使用的参数个数不对,也将遇到一个警告。

函数strstr()要求输入两个字符串:haystack用来搜索,needle用来查找。如果我们按如下方式调用它:


strstr();


将得到如下所示警告:


Warning:Wrong parameter count for strstr()in

/home/book/public_html/phpmysql4e/chapter26/error.php on line 1


如下所示的语句同样是错误的:


<?php

if($var==4){

strstr();

}

?>


除了极少的情况(也就是,变量$var值为4)外,对strstr()的调用将不会发生,警告也不会出现。PHP解释器不会将时间花费在解析那些不必要在当前的执行脚本中执行的代码。我们必须确认进行了仔细的测试。

不正确的函数调用很容易发生,但是因为导致的出错信息可以确切地识别出错误行和导致错误的函数,所以它们都比较容易纠正。只有在测试过程非常糟糕并且没有测试所有条件执行的代码时,才很难查出错误。进行测试的时候,测试的目标之一是代码的每一行都要执行一次。另一个目标是测试所有的临界条件和各种类型的输入。

2.读写文件

尽管在一定程度上说,程序的使用周期里什么错误都可能出现,但是一些错误还是比其他错误更容易出现。由于访问文件错误是经常出现的,因此非常有必要巧妙地处理它们。硬盘驱动器出错或写满,人为操作错误导致目录权限改变等。

通常,与fopen()函数一样,可能偶尔失败的函数都有一个返回值指示出现的错误。对于fopen()函数来说,返回值为false表示失败。

对提供失败指示的函数,我们需要小心检查每次调用的返回值,并根据返回值进行接下来的操作。

3.与MySQL或其他数据库的交互

连接和使用MySQL可能产生许多错误。函数mysqli_connect()就可能至少产生下列一些错误:

■Warning:mysqli_connect()[function.mysqli-connect]:Can't connect to MySQL server on'localhost'(10061)(无法连接到"localhost"上的MySQL服务器)

■Warning:mysqli_connect()[function.mysqli-connect]:Unknown MySQL Server Host'hostname'(11001)(未知的MySQL服务器主机"hostname")

■Warning:mysqli_connect()[function.mysqli-connect]:Access denied for user:'username'@'localhost'(Using password:YES)

(用户username@localhost的访问被拒绝,使用密码:YES)

mysqli_connect()函数在出现错误的时候将返回值false。这就意味着用户可以很容易地检测和处理这类常见的错误。

如果希望不终止脚本的正常执行而解决这些错误,脚本将继续与数据库交互。没有一个有效的MySQL连接就试图运行查询和得到结果,将导致访问者看到满屏幕的有失专业水准的错误信息。

许多其他常用的、与MySQL相关的PHP函数,例如mysqli_query(),也将返回false来表示函数调用出现错误。

如果出现错误,可以通过调用函数mysqli_error()来获得错误信息文本,或者调用函数mysqli_errno()获得函数调用的错误代码。如果MySQL函数的调用没有出现错误,mysqli_error()将返回空字符串,mysqli_errno()将返回0。

例如,假设我们已经连接到服务器,并且选择了一个要使用的数据库,如下所示的代码段:


$result=mysqli_query($db,'select*from does_not_exist');

echo mysqli_errno($db);

echo'<br/>';

echo mysqli_error($db);


的可能输出结果是:


1146

Table'dbname.does_not_exist'doesn't exist


请注意,这些函数的输出是指最后执行的MySQL函数(而不是mysqli_error()或mysqli_errno()的结果)。如需要知道命令的结果,需确认在运行其他命令之前检查它。

就像文件交互错误一样,数据库交互造成的错误也会时常发生。即使在完成某个服务的开发和测试之后,也会偶尔发现MySQL后台程序(mysqld)崩溃或不能正常连接问题。如果服务器运行于另一台物理机器,那么它还要依赖于另一套硬件和软件组件,而这些组件可能出错——Web服务器和数据库服务器之间的网络连接、网卡、路由器等。

请记住,在试图使用这些结果之前需检查一下数据库请求是否成功。连接数据库失败之后试图检索结果是没有意义的,而检索失败后试图提取和处理检索的结果也是没有意义的。

在这里,值得注意的是,检索失败与仅仅是没有返回任何数据,也没有对任何记录行产生影响的检索是有区别的。

一个包含SQL语法错误的SQL检索,或者对不存在的数据库、表和列的访问也可能会导致错误。如下所示的查询:


select*from does_not_exist;


也可能会失败,因为表名称并不存在,这样将产生一个错误号和错误消息,可以通过mysqli_errno()函数或mysqli_error()函数获得。

通常,一个SQL查询如果只是在语法上是合法的,而且只对存在的数据库、表和列进行访问,它是不会产生错误的。但是,如果它检索一个空的数据库表或检索一个不存在的数据,它将返回空值。假设已经成功地连接了数据库,并且存在表t1和列c1,如下所示的检索:


select*from t1 where c1='not in database';


该代码将成功执行但是不返回任何结果。

在使用检索结果之前,要检查检索是否失败和返回空结果。

4.连接到网络服务

尽管系统中的设备和其他程序偶尔可能出错,但如果质量不是很差,它们应该很少出错。当在网络上连接其他机器和这些机器上的软件时,应该意识到系统的某些部分会经常出错。从一台机器连接到另一机器依赖于许多我们无法控制的设备和服务。

尽管操作可能重复,但我们还是要仔细检查试图与网络服务连接的函数的返回值。

如下所示的函数调用:


$sp=fsockopen('localhost'',5000);


如果它无法正确连接localhost机器的端口5000,该函数将给出一个警告信息。但是该警告信息将以默认的格式给出,同时不会为代码提供能够巧妙处理该警告的选择。

将以上函数调用重写为:


$sp=@fsockopen('localhost',5000,&$errorno,&$errorstr);

if(!$sp){

echo"ERROR:".$errorno.":".$errorstr;

}


以上代码将抑制内置错误消息,并且通过函数的返回值检查函数调用是否出现错误,同时使用自己的代码来处理错误消息。运行以上代码后,它将显示一个可以帮助解决问题的错误消息。在这个例子中,将产生如下所示的输出:


ERROR:10035:A non-blocking socket operation could not be completed immediately.


与语法错误相比较,运行时错误更难消除,因为在代码首次运行的时候解析器并不能指示出错误所在。因为运行时错误通常出现在一连串的事件响应中,因此很难检查到,也很难解决。解析器并不能自动指出哪一行会产生错误。测试的时候需要提供一个能够产生该类错误的情形。

解决运行时错误需要一定程度的预见性;不仅要检查所有可能出现的不同类型的错误,然后采取适当的措施;也需要仔细检测和模拟可能出现的每一类运行时错误。

这并不表示必须模拟每一个可能出现的错误。例如,MySQL可以提供大约200种不同的错误编号和错误信息,我们需要的是在每一个可能导致错误的每个函数调用中模拟函数错误,每种不同类型的错误将由特定的代码来解决。

5.检查输入数据失败

通常,我们会对用户可能输入的数据进行一些假设。如果输入的数据不是原先所预料的数据类型,它就可能会导致错误,该错误或者是运行时错误,或者是逻辑错误(将在下一节里详细介绍)。

典型的运行时错误的例子是在处理用户输入数据的时候忘记对该数据执行addslashes()函数。这样,如果有一个叫O'Grady的用户,名字中包含一个撇号,当在数据库插入语句中通过单引号使用这个输入,那么数据库函数将产生一个错误。

在接下来的一节中,我们将介绍由于对用户输入数据不同假设所导致的错误。