PHP进阶
长沙北大青鸟作者:科泰校区KevinYank
摘要:PHP功能的强大首先在于它的大量的内置函数库,它可以让初学者也能执行许多复杂的任务,而不必安装新的库和关心底层的详细情况,而这恰恰是其它流行的诸如Perl这样的客户端语言所不具备的。由于这个教程的限制,我们仅
PHP功能的强大首先在于它的大量的内置函数库,它可以让初学者也能执行许多复杂的任务,而不必安装新的库和关心底层的详细情况,而这恰恰是其它流行的诸如Perl这样的客户端语言所不具备的。由于这个教程的限制,我们仅仅学习了一些与MySQL数据库相关的一些函数(事实上,即使是这种函数,我们也没有学全)。在这最后的部分,我们会稍微扩大一下范围来看看其它对于建立一个数据库驱动的网站有用的PHP的特征。
我们首先将学习PHP的include函数,使用这个函数,我们可以在许多页面中重复使用同一个PHP代码段。我们还看到如何利用这个函数提高我们的网站的安全性。
虽然PHP一般是相当快速和有效率的,但是它会加重服务器的运行时间和工作量。对于一个高流量的站点来说,这种负担可能会达到无法接受的程度。但是这并不意味着我们需要放弃我们的站点的数据库驱动的特征。我们会看到如何使用PHP在后台建立半动态页面而不必过分加重服务器的负担。
经常有人问论坛是如何利用一个<INPUT TYPE=FILE>标记来接受文件的上载的。我们也将学习到如何用PHP实现这种功能,而且我们还会看到如果将其有效地结合到一个数据库驱动的站点中。
最后,PHP还有一个相当强大的功能是可以很简单地将动态生成的内容很方便地作为email的信息发送出去。不论你是想要使用PHP使得访问者将你的站点的内容发送给它们的朋友,还是仅仅想提供一种方法让用户可以得到他们遗忘的口令,PHP的email函数都可以很好地实现这种功能!
PHP的服务器端包含
如果你已经在Internet上工作过一段时间,你也许接触过服务器端包含(SSI)这个术语;如果你没有接触过,你可以参看有关这个问题的Matt Mickiewicz的迷你指南。
从本质上说,SSI允许你将存储在你的Web服务器上的某一个文件的内容插入到另一个文件中去,最常见的应用是将一个网站的公用设计部分写入一个小的HTML文件当中,这个文件可以被Web页面所包含。对这个小文件的所有变动立即会影响所有包含它的文件。就象一个PHP脚本一样,Web的浏览者不需要对此有所了解,因为Web服务器会在将被请求的页面发送到浏览器之前做好所有的工作。
PHP有一个函数可以提供类似的功能。但是在包含文件中除了可以含有正式的HTML以及其它静态的内容以外,还可以含有脚本程序。让我们来看看下面这个例子:
<!-- include-me.inc --><br>
<?php<br>
echo( "<P>Soylent Green is made from people! ");<br>
?><br>
在上面的文件中,include-me.inc包含了一些简单的PHP代码。请注意这个文件的文件名的结尾是.inc,而不是.php。这表示这个文件与一般的Web服务器所认为的PHP脚本有所不同。这会保证这个文件只有被插入到一个.php文件中才会被执行,此外这也有助于你分辨你的PHP Web页面和PHP包含文件。
你还需要下面的文件:
<!-- testinclude.php -->
<HTML>
<HEAD>
<TITLE> Test of PHP Includes </TITLE>
</HEAD>
<BODY>
<?php
include("include-me.inc");
?>
</BODY>
</HTML>
这个文件和我们以前使用过的PHP脚本差不多,它的扩展名是.php extension(如果你的服务器需要,也可以是.php3)。请注意对include函数的调用。我们指定了我们要插入的文件名(include-me.inc),PHP会试图获取这个文件并将其插入到现在的文件中以取代include。将这两个文件都上载到你的Web服务器(或者将它们拷贝到你的Web服务器的文档文件夹,如果你正在这个服务器上工作的话),然后用你的浏览器装载testinclude.php。你会看到一个包含我们的插入文件信息的Web页面的,一切和你当初预料的没有什么两样。
如果这个例子不能工作,你也许需要配置你的php.ini文件中的include_path选项。用你常用的文本编辑器打开这个文件找到以include_path开始的一行(一般是在文件的中间)。就象你所熟悉的系统PATH环境变量的设置一样,这里包含了一个目录的列表,PHP会从这些位置寻找你所要包含的文件。它应该包括"."(当前目录)。
根据你的服务器的操作系统是Windows还是UNIX,你也许需要确定是否要用到引号:
对于UNIX:
include_path=.:/another/directory
对于Windows:
include_path=".;c:anotherdirectory"
利用插入文件提高安全性
PHP脚本有时会包含一些诸如用户名、口令以及其它一些你不想公开的敏感信息。你也许已经使用过mysql_connect函数,这个函数要求你在需要访问数据库的PHP脚本中输入你的MySQL用户名和口令。虽然你可以简单地对MySQL进行设置以使得这个用户名和口令只能供PHP使用,而为会被可能出现的黑客利用(通过在第八章中学习的方法对用户表的主机字段进行设置),你也许还是需要其它的比较方便的对你的用户名和口令的保护。
“但是等一下,”可能你会这样说,“因为PHP是由服务器处理的,没人会看到我的口令,对吗?”不错,但是你是否考虑到这样一个情况,服务器对PHP的解析可能会因为偶然的原因而停止。这可能是因为某个善意的同事对软件的错误配置,也可能是因为其它的因素,如果发生了这样的情况,PHP页面会当成纯文本文件来处理,于是你的所有的PHP代码(包括你的口令)将是完全公开的!
为了预防这样的安全漏洞,你可以将包含敏感信息的代码放到一个插入文件中,然后将它放到一个不属于你的Web服务的目录结构的目录中去。将这个目录添加到你的PHP的include_path中(在php.ini中添加),你可以指定PHP包含函数所使用的目录,而不必担心其中文件的安全,因为你的Web服务器不会将其作为Web页来显示。
例如,如果你的Web服务器定义所有的Web页面必须存在于/home/httpd/及其子目录中,你可以建立一个名为/home/phplib/来保存你的包含文件。将这个目录添加到你的include_path中,这样就行了!下面的例子显示了如何将你的数据库连接代码放置到一个包含文件中:
<!-- dbConnect.inc (in /home/phplib/) -->
<?php
$cnx = mysql_connect("localhost",
"root", "rootpassword");
?>
以及一个使用这个包含文件的文件:
<!-- dbSample.php (in /home/httpd/) -->
<?php
// Connect to MySQL
include("dbConnect.inc");
mysql_select_db("myDatabase",$cnx);
...
正如你看到的,如果你的服务器的PHP停止了工作,被暴露的仅仅是对包含函数的调用。用户名和口令被安全地存储在dbConnect.inc中,而这个文件不能从网站直接访问。
半动态页面
作为一个成功的(或者是即将成功的)网站的所有者,你肯定希望你的网站的访问量越来越高。不幸的是,大的访问量恰恰也是Web服务器的管理者所担心的事--特别是当网站主要是由动态生成的、由数据库驱动的页面组成的时候,情况更是这样。这样的页面与处理传统的HTML文件相比,意味着运行Web服务软件的计算机的巨大负担。因为对每一个页面的请求都相当于在计算机上运行一个小程序。
虽然数据库驱动的站点的有些页面必须严格地实时地从数据库中调用相关的数据,但是大部分页面的要求并不这么严格。例如对于一个Web站点的首页来说,典型的情况是,它会向访问者简要介绍这个站点最近做了哪些更新。但是事实上这样的更新多长时间做一次呢?一天一次?还是一周一次?而且对于你的站点的访问者来说非常及时地了解这些更新又有多大价值?也许对于这类变动在你网站上的反应稍微有点滞后也不会有多大问题。
通过将高访问量的动态页面转换成“半动态”的页面,也就是一个可以按照一定时间间隔“刷新”内容以动态地重新生成的静态页面,你可以大大减轻你的Web服务器处理数据库驱动的页面的工作量。
例如,我们有一个名为index.php的首页,它提供你的网站的新内容的摘要。通过对服务器日志的检查,你可以发现这是你的网站中被访问得最多的页面之一。通过前面的讨论,你应该已经意识到对于这个页面我们并不需要在每次请求时动态地生成。只在在每次将新内容添加到你的站点的同时更新这个页面,就可以充分保证它的动态效果了。使用一个PHP程序,你可以生成动态输出的页面一个静态的“映像”,你可以将其命名为index.html以取代原来的动态页面。
我们需要学习一些有关文件读取、写入以及修改的知识。PHP可以很好地完成这些工作,只是我们之前没看到过这些我们所需要的函数:
fopen
打开一个文件,以用来进行读写操作。这个文件可以存储在服务器的硬盘上,也可以通过一个URL装载。
fclose
通知PHP你将不再对某一文件进行读写操作,释放它以备其它程序或脚本使用。
fread
将一个文件的数据读入到一个PHP变量中。允许你指定读取多少信息(也就是多少字符或多少字节)。
fwrite
将一个来自于PHP变量的数据写入到文件中。
copy
执行一个文件拷贝的操作。
unlink
从硬盘中删除一个文件。
你明白了吗?如果还没有,不要担心--等一下我们会进行详细讲解。
建立一个名为generateindex.php的文件。它用来模拟一个Web浏览器从index.php(你的首页的动态版本)读取内容,并将其以静态文本的格式写入index.html中。如果在这个处理过程中发生了什么错误,我们需要避免对原来的index.html“好的”拷贝的破坏,所以我们的这个脚本先将新的静态版本写入到一个临时文件(tempindex.html)中,如果中途没发生什么问题,再用其覆盖index.html。
下面是generateindex.php的代码,我们加入足够多的注解以保证你能顺利地读懂这段程序:
<!-- generateindex.php -->
<?php
//设置我们将要使用的文件
$srcurl = "http://localhost/index.php";
$tempfilename = "tempindex.html";
$targetfilename = "index.html";
?>
<HTML>
<HEAD>
<TITLE>
Generating <?php echo("$targetfilename"); ?>
</TITLE>
</HEAD>
<BODY>
<P>Generating <?php echo("$targetfilename"); ?>...</P>
<?php
//首先删除上次操作可能遗留下来的临时文件。
//这个过程可能会提示错误,所以我们使用@以防止报错。
@unlink($tempfilename);
//通过一个URL的请求装入动态版本。
//在我们接收到相关内容之前,Web服务器会对PHP进行处理
//(因为本质上我们是在模拟一个Web浏览器),
//所以我们将获得的是一个静态的HTML页面。
//'r'指出我们只要求对这个“文件”进行读操作。
$dynpage = fopen($srcurl, 'r');
//处理错误
if (!$dynpage) {
echo("<P>Unable to load $srcurl. Static page ".
"update aborted!</P>");
exit();
}
//将这个URL的内容读入到一个PHP变量中。
//指定我们将读取1MB的数据(超过这个数据量一般是意味着出错了)。
$htmldata = fread($dynpage, 1024*1024);
//当我们完成工作后,关闭到源“文件”的连接。
fclose($dynpage);
//打开临时文件(同时在这个过程中建立)以用来写入(注意'w'的用法).
$tempfile = fopen($tempfilename, 'w');
//处理错误
if (!$tempfile) {
echo("<P>Unable to open temporary file ".
"($tempfilename) for writing. Static page ".
"update aborted!</P>");
exit();
}
//将静态页面的数据写入到临时文件中
fwrite($tempfile, $htmldata);
//完成写入后,关闭临时文件。
fclose($tempfile);
//如果到了这里,我们应该已经成功地写好了一个临时文件,
//现在我们可以用它来覆盖原来的静态页面了。
$ok = copy($tempfilename, $targetfilename);
//最后删除这个临时文件。
unlink($tempfilename);
?>
<P>Static page successfully updated!</P>
</BODY>
</HTML>
上面的代码看上去很令人恐怖,其实这只是因为我们在其中包含了大量的注释。删除这些注释。你会看到这段代码其实很简单。
现在,每当我们运行generateindex.php(也就是说通过一个浏览器请求这个页面),就会从index.php生成一个新刷新的index.html。通过将index.php和generateindex.php移动到一个访问有限制的目录,你可以保证只有站点管理者能够更新你的主页。将这个脚本作一下扩充,你可以生成你的站点上的所有的半动态页面,你还可以在你的内容管理系统中增加“更新主页”的连接!
如果你希望你的主页能够自动地刷新,你只需要设置你的服务器定期地运行generateindex.php(例如说,每隔一个小时)。在新的Windows 9x下,你可以使用任务管理程序(对于旧的版本,你必须使用补丁包)每隔一个小时自动运行php.exe。你只需要建立一个包含以下行的名为generateindex.bat的批处理文件。
C:PHPphp.exe C:WWWgenerateindex.php
如果必要的话,对路径和文件名进行调整,然后设置任务管理程序每隔一个小时执行一次generateindex.bat(你需要设置24个每天在固定时间运行的任务)。
在Linux下(或其它基于UNIX的平台下)你可以使用cron--一个可以在各种UNIX系统下用来定义任务和运行时间的程序来完成类似的工作。如果你对cron还不了解的话,你可以请教你熟悉的Linux专家,或者查阅你熟悉的Linux网站。
你使用cron设置任务和我们之前讨论的在Windows下的用法很类似。但是,你需要一个单机版的PHP,这不是指在第一章中编译的Apache中可导入的PHP模块。你需要对用来编译Apache模块的同一软件包中单独地进行编译。如果你需要帮助,你可以参看软件包提供的说明,也可以查阅PHP官方网站!
对于一个有经验的cron使用者来说,你只需要在你的crontab文件中增加下面的这一行:
0 0-23 * * * php /path/to/generateindex.php > /dev/null
处理文件的上载
到目前为止,这个教程中数据库驱动的站点的所有例子都是处理的文本的数据。笑话、文章、作者...所有的这些都可以完全由文本形式的字符串来表示。但是如果你在运行时,有一个在线的数字图库需要人们能够上载用数字照相机拍摄的图片,我们就需要允许访问者向我们的站点上载他们的图片,我们也需要相应的处理程序。
让我们先从基本的开始:写一个HTML表单用来供用户上载文件。在HTML中,这很简单,只需要用一个<INPUT TYPE=FILE>标志就行了。但是,默认情况是只有用户选择的文件名被发送了。要通过表单数据提交文件自身,我们需要在<FORM>标志中增加ENCTYPE="multipart/form-data":
<FORM ACTION="fileupload.php" METHOD=POST
ENCTYPE="multipart/form-data">
<P>Select file to upload:
<INPUT TYPE=FILE NAME="uploadedfile"></P>
<P><INPUT TYPE=SUBMIT NAME="submit" VALUE="Submit"></P>
</FORM>
正如我们看到的,一个PHP脚本(fileupload.php)会处理前面的表单提交的数据。也正如你所预计的,一个名为$uploadedfile(由<INPUT>标志的NAME属性确定)的PHP变量会自动被建立。但是,$uploadedfile中存储的不是上载文件的内容,而是存储在Web服务器上的硬盘中的文件的文件名,这个文件被保存在TEMP环境变量所指定的目录中(例如,对于绝大多数的Windows 9x系统,这个目录将是C:WindowsTEMP)。这个文件仅仅在用来处理该表单提交的PHP脚本运行时存在,所以如果你想有什么其它用途(例如,将它存起来以供网站显示),你需要使用我们在前面提到的copy函数在其它地方做一份拷贝。
伴随着$uploadedfile,三个其它的变量也会同时被建立。$uploadedfile_name包含了文件提交前的文件名(已提交的文件将在TEMP目录中以phpx的文件名存储,这里x是一个数字),$uploadedfile_size说明了文件的大小(以字节表示),而$uploadedfile_type说明了MIME类型(例如text/plain、image/gif等等)。记住,"uploadedfile"仅仅是提交文件的INPUT标志中的NAME,实际的文件名将存在上述变量中。
你可以根据这些变量以决定是否接受一个上载的文件。例如,在我们的图库中我们仅仅只对JPEG或GIF文件感兴趣。这些文件的MIME类型应该是image/pjpeg或image/gif,所以确认上载文件的代码大致上是这样大:
if ("image/pjpeg" == $uploadedfile_type
or "image/gif" == $uploadedfile_type) {
// Handle the file...
} else {
echo("<P>Please submit a JPEG or GIF image file. ");
}
虽然你可以使用一个类似的技术以拒绝过大的文件(通过检查$uploadedfile_size变量),但是通常这不是一个好主意。在得到这个变量之前,这个文件已经被上载并保存在TEMP目录中。如果你试图因为磁盘容量或者是带宽的原因拒绝文件的上载,事实上那个大文件还是被上载了(尽管它们立即又被删除了),这对于你来说也许是个问题。
更好的方法是,你可以提前告诉PHP你希望可以接受的文件的大小的上限。有两个方法。第一个是调整你的php.ini文件中的upload_max_filesize设置。默认值是2MB,所以如果你想要接受更大的文件,你需要立即改变这个值。
第二个方法是在你的表单中包含一个隐含INPUT域,它的名字是MAX_FILE_SIZE,在其中你可以定义你所能接受的最大的文件的大小。出于安全的原因,这个值不能超过你的php.ini文件中的upload_max_filesize的设置,但是它提供了一种方法在不同页面中各自定义上载文件大小的上限。例如,下面的表单,只允许我们最大上载1K字节的文件(1024字节):
<FORM ACTION="fileupload.php" METHOD=POST
ENCTYPE="multipart/form-data">
<P>Select file to upload:
<INPUT TYPE=FILE NAME="uploadedfile"></P>
<P><INPUT TYPE=SUBMIT NAME="submit" VALUE="Submit"></P>
<INPUT TYPE=HIDDEN NAME=MAX_FILE_SIZE VALUE=1024>
</FORM>
指定唯一的文件名
正如我们前面提到的,要保存一个上载的文件,我们需要将它拷贝到另一个目录是保存。当我们从$uploadedfile_name获取每个上载的文件的文件名时,我们不能保证不会上传两个同名的文件。在这种情况下,使用它的原名存储文件将导致新上载的文件覆盖原来上载的文件。
因为这个原因,你通过想要采用一种方法对所有上载的文件指定一个唯一的文件名。使用系统时间(它可以通过使用PHP的time函数获得),我们可以容易地取得一个其于从1/1/1970到目前的秒数的名字。但是如果两个文件恰巧在同一秒同时被上载呢?这了防止这个问题,我们在文件名中同时使用客户端的IP地址(由PHP自动存储在$REMOTE_HOST中)。因为我们不太可能同一秒从同一个IP地址接受两个文件,这个方案应该是可行的。
// Pick a file extension
if ( "image/pjpeg" == $uploadedfile_type )
$extension = ".jpg";
else
$extension = ".gif";
// The complete path/filename
$filename = "C:Uploads" . time() .
$REMOTE_HOST . $extension;
// Copy the file
if (copy($uploadedfile, $filename)) {
echo("<P>File stored successfully as $filename.");
} else {
echo("<P>Could not save file as $filename!");
}
请注意如果是在Windows下,我们必须在路径中使用双反斜杠(),因为反斜杠用来在PHP文本字符串中表示特殊字符。而在UNIX下,我们只要象通常那样使用一个斜杠(/)。
将上载的文件保存到数据库中
我们已经建立了一个访问者可以上传JPEG和GIF图象,并将其存储在我们的服务器上的系统,但是为什么不是这个教程所介绍的数据库驱动呢?如果我们这个系统就这样维持现状不变,将不得不有人负责从文件夹中收集提交的图象并手工将其添加到Web站点!回头看看第七章,在那里我们开发了一个系统使得站点的访问者可以提交笑话将将其存到数据库中,以备管理者通过,我们知道这是一种更好的方法!
MySQL有一些列类型可以用来存储二进制数据。在数据库术语中,这些列类型可以存储BLOB(二进制大对象)。然而,在一个关系型数据库中储存大的文件并不是一个好主意。尽管将所有数据放在一个地方会给我们带来方便,但是大的文件导致大的数据库,而大的数据库会导致性能的降低和太大的备份文件。
通常最好的选择是将文件名存储在数据库中。只要你记得在数据中删除记录时删除相应的文件就可以了。
PHP中的Email
Email在Internet上有着强大的影响力。不论你是想要给你的用户提供一个"what's new"周刊,还是考虑一个途径让你的用户可以获得丢失的口令,你都会用到email。PHP便得使用email非常的简单,你只需要简单地调用mail函数就可以发出信息。
在你使用mail函数发送email之前,你首先必须设置PHP的与email相关的选项。这儿是在Windows下的php.ini文件中的相关行:
[mail function]
SMTP = localhost ;仅对win32有效
sendmail_from = me@localhost.com ;仅对win32有效
;sendmail_path = ;仅对unix有效 ...
取决于你使用的是Windows还是UNIX,PHP会通过一个SMTP服务器或一个本地的sendmail系统发送email。对这些的设置不是本教程的讨论范围,你可以从其它地方找到有关这二者的大量信息。然而,如果你是在Windows上运行,有可能你的ISP已经为你提供了一个SMTP服务器。这也就是你发送信息设置你的email程序所用的服务器。将SMTP设置为那个服务器的主机名或IP地址。
sendmail_from会设置你的emails发出时默认的发出的email地址。如果你正在管理这个服务器,你可以将你的email地址放在这儿。
最后,sendmail_path在UNIX下将不会被注释(也就是说,删除这一行前面的分号),你需要将其设置为你的系统上的sendmail程序的路径和文件名。在Linux下,这通常是指/usr/sbin/sendmail。
做好这些配置后重新启动你的Web服务器,PHP将具有全部的email功能。现在在PHP中发送一个email是相当容易的:
mail("to-address@somewhere.com", "Message Subject", "This is the body of the message.");
要发送给多个收件人只需要将多个地址用逗号分开:
mail("to1@mail.net, to2@mail.net, ...", "Message Subject", "Message body");
在标题中指定From:或者Reply-To:的地址也非常简单。将其作为第四个参数带入,其中以回车-换行符分隔:
mail("to@mail.net", "Message Subject", "Message body", "From: webmaster@host.com Reply-to:admin@host.com");
与一个数据库相结合,一个邮件列表变得非常容易管理!只需要从数据库中取出地址列表并使用mail函数发送信息就行了。个人化的信息也非常简单。参看下面的例子:
// Retrieve $email and $password from the database based
// on the $username provided in a form.
mail($email, "Your Password",
"Hi there!
You just filled out a form on our Web site
indicating that you had lost your password.
As requested, we are sending it to you by
email.
username: $username
password: $password
Please record this information in a safe
place so you have it on hand for your next
visit to pingpongballs.com!
-The Webmaster.
");
如果你在在UNIX下运行,而且你没有一个本地的sendmail系统可以发送email,这也不要紧。PHP具有完善的TCP/IP网络性能,如果需要,你可以连接到一个SMTP服务器以发送信息。同样的,如果你需要在发出的信息中包含附件,PHP也可以实现这种功能。
不幸的是,内置的mail函数不支持这些特征,如果你需要它们,你将不得不从头编写你自己的email函数。WROX Press编写的"专业PHP程序设计"已经为你完成了这些工作,在该书的第17章你可以找到全部的代码。尽管这是我高度推荐的一本好书(见我的回顾),但是如果你只是想获得这项功能,你也可以不购买这本书,这个源代码你可以从WROX的网站自由地下载。
除了这两个小问题外,PHP的内置mail函数为你的Web页面发送email信息提供了令人难以相信的方便。