在本节中,我们将把注意力转向学分保存方案。我们web 站点上的这个区域的url 是http://pit-viper.snake.net/gp/,应该为它编写一个简短的主页i n d e x . p h p,下面的页面就正在做这件事。它包括了与第7 章编写的score_browser 脚本的连接,因为这个脚本适合于学分保存方案。
现在让我们考虑如何设计和实现脚本score _ e n t r y. p h p,它将让我们输入一组新的测试或测验分数,或者修改一组已经存在的分数。后者的性能对于处理由于生病或者其他原因缺席(或者,放弃这个想法以免输入分数失败)造成考试或测验比其他学生晚的学生的分数是必要的。分数项脚本的概要是这样的: 1) 最初的页面代表一系列已知的登记事件,并允许选择一个事件或者指定应该创建的新事件。 2) 如果选择创建一个新事件,脚本就给出允许指定日期和事件类型的页面。创建这个事件记录之后,脚本重新显示事件列表页面来显示这个新事件。 3) 当选择了事件后,脚本给出在顶部(事件id、日期、类型)显示事件信息的分数项页面,后接每个学生一项的列表。对于新事件,项将是空白的。对于已存在的事件,项将显示每个学生已存在的分数。选择提交按钮时,分数输入到score 表中。 脚本需要执行几个不同的操作,这意味着我们需要从一个页面到另一个页面周而复始地传递状态变量,以便脚本在每次调用时能够知道假设要做什么。在php 中很容易做到这一点,因为php 处理作为url 参数传递的变量,并把它们转换为与参数具有相同名称的变量。例 如,可以在脚本url 的末尾对参数action 进行如下编码: http://pit-viper.snake.net/gp/score_entry.php?action=value 当调用score _ e n t r y.php 时,参数action 作为变量$action 来编码,这样就可以直接访问它了。这也适用于格式中的域。设想一个包括域name 和address 的表格,当客户机传递表格时,web 服务器就调用脚本访问表格的内容。脚本能够找出通过检查变量$name 和$address 的值而输入到表格中的值。对于包括许多域的表格,全部给出唯一的命名是有困难的。php 很容易地把数组在表格中传入和传出。如果使用了如x [ 0 ]、x[1] 等等的域名,则php 把它们作为$x 数组的元素进行编码。可以将这些元素作为$ x [ 0 ]、$x[1] 等等来访问。 我们通过使用页面中的action 参数,可以将信息从score _ e n t r y.php 脚本的一个调用传送到另一个调用,并在脚本中用变量$action 检查它的值。脚本的框架是这样的:
变量$action 可以取若干值,我们已在switch() 语句中测试过了(为避免在脚本中使用文字的数字,可以用php 的define() 构造来定义常量)。php switch() 语句与它在c 中相应的部分相类似。在score _ e n t r y.php 中,它用来确定采用什么操作,并且调用实现这个操作的函数。 检查一下每次处理一个操作的函数。第一个函数d i s p l a y _ e v e n t s ( ),检索来自mysql的event 表的行并加以显示。表的每一行都列出了事件id、日期和时间类型(测试或测验),还有编写事件id作为可以选择用来修改事件分数的连接:
表中的连接用$php_self 来构造。这个变量包括了脚本自己的u r l,它为脚本再次调用自己提供了一个方便的方法。然而,请注意函数开始处的global 行: global $php_self; 在php 函数中,全局变量是不可访问的,除非显式地声明要使用它们。没有global 行,$php_self 将被看成局部变量(因为我们没有将值赋给它,因此是空的)。在函数内部,使用global 来访问依靠url 参数或者作为表格域传递到脚本中的参数也是必需的。 用来生成表的函数display_cell() 与第7 章编写的同名dbi 函数相类似。php 版本如下:
如果在display_events() 给出的表中选择了“ new event ”连接,则脚本通过操作solicit_ event进行再次调用。它引发了对solicit_event_info() 的调用,这个函数显示了允许输入新事件信息的表格:
由solocit_event_info() 生成的表格包括输入数据的编辑域、指定新事件是测试还是测验的两个单选按钮、submit 按钮。当递交表格时, add_event 操作将调用score _ e n t r y. p h p。调用add_new_event() 函数在event 表中输入一个新的行:
在add_new_event() 中,我们使用global 访问在新事件项表格中使用的域值( date 和type,用变量$date 和$type 访问)。做出最低限度的安全检查,确定数据为非空白之后,在event 表中输入一个新记录。输入这个事件记录之后,主程序将再次显示事件列表,这样就可以选择新事件并开始输入分数了。 函数display_scores() 为给定的事件查找已存在的分数,并列出显示他们的表格,包括学生姓名:
display_scores() 用于检索所选事件的分数信息的查询并不是表之间的简单连接,因为它不会为事件中没有分数的学生选择行。特别是,对于新的事件,连接会选择无记录,这就有了一个空项表格!我们使用left join 强迫为每个学生检索行,无论学生是否在score表中已经有了分数。与display_scores() 用来检索来自于mysql的分数记录相类似的查询背景,已在3 . 8 . 2节“检查表中未给出的值”中给出了介绍。那里的查询只选择缺失分数,这里的查询只选择特殊事件的分数。 分数在表格中使用了有名称的域进行编码,如score [n],这里的n是student_id 的值。当表格送回web 服务器时,php 将这些域转换为$score 数组的元素,我们可以访问数组元素以恢复表格的内容。 当完成输入或者编辑分数,并提交给表格后,enter_scores 操作调用score _ e n t r y. p h p,并且调用函数enter_scores() 处理表格信息:
学生id 的值和相关的分数通过迭代php 的each 函数的$score 数组来获得。每个分数处理如下: 如果分数是空白的,则表明什么也没有输入,但是我们还要试图删除这个分数,以免它以前曾经存在(也许以前我们为缺席的学生错误地输入了分数) 如果分数不是空白的,就对值进行一些根本的确认。用函数trim() 去掉前后的空格之后,如果剩余部分是空白或者整数,就接受这个结果。然而,表格值通常作为字符串来编码,因此不能用is_long() 或者is_int() 检查值是否为整数。即使值只包括数字,这些函数也会返回fa l s e。既然这样,最好用模型匹配操作。如果字符串从开始到结束每个字符都是数字,则下面的测试为t r u e: ereg("^[0-9]+$",$str) 如果分数检查完毕,我们就将它加到score 表中。查询使用replace 而不用insert,因为我们可能替换了已存在的分数而不是输入一个新的分数( replace 在两种情况下都适用)。 注意score _ e n t r y.php 脚本。现在所有的分数项和编辑项都能从web 浏览器执行。一个明显的缺点是:脚本没有提供安全措施,连接到web 服务器的任何人都可以对分数进行编辑。以后,我们用编辑历史同盟成员项编写的脚本来说明这个脚本所采取的简单确认方案。也可以使用phplib 程序包来提供更完善的确认。
美国总统测验
历史同盟web 站点的目标之一就是用它给出测验的在线版本,这类似于同盟在时事通信“美国编年史”的儿童部分发表的一些测验。实际上我们创建了president 表,因此对基于历史的测验可以用它作为问题的来源。为了给出这个测验,我们将编写称为pres_quiz.php 的脚本。 基本的想法是随机挑选一个总统,问一个关于他的问题,然后请求用户回答并且察看答案是否正确。为了简单一点,可以把主题限制为询问总统出生在哪里。另外一种简单的衡量就是以多个选择的格式给出这个问题。这对用户来讲很容易,他只需从一组选择中挑选一个,而不用将之键入等待回应。这对我们来讲也是容易的,因为我们不需做任何棘手的匹配字符串来检查用户可能键入的内容,而只需对用户的选择和我们寻找的值做一个简单的比较。 显示这个测验的脚本必须执行两个函数。第一个,对于它最初的调用,将从p r e s i d e n t表中查阅信息来生成并显示一个新的问题。第二个,如果脚本已经被调用是因为用户正提交一个回答,那么就需要检查这些答案并给出一些反馈信息来指出它是否正确。如果正确,脚本会生成并显示一个新的问题。如果回答不正确,将再次显示同一问题。 为了生成这些问题,我们将使用mysql3.23 中出现的一个order by rand()特性。使用这个函数就能从p r e s i d e n t表中随机地进行行选择。例如,为了随机地挑选总统的姓名和出生地,查询将执行这样的操作:
为了给出测验问题的信息,我们使用了显示总统姓名、一组列出可能选择的单选按钮和一个s ub m i t按钮的表格。这个表格需要做两件事情:必须对客户机给出测验信息;当用户提交回答时必须将信息传送回web 服务器,以便检查回答是否正确。 为了安排表格执行这些操作,我们使用了隐藏域把测验信息包括在表格中。把域称为name、place 和c h o i c e,它们代表总统的姓名、出生地和一组可能的选择。使用implode() 连接值和特殊字符,这样,这些选择可以很容易地作为单个字符串来编码(我们需要特殊字符,以便如果需要重新显示问题时可以用explode() 分离字符串)。显示表格的函数如下:
我们仍然需要编写check_response() 函数来将用户的回答与正确答案做比较。我们将正确答案在表格的place 域进行编码,用户的回答则在表格的response 域进行编码,因此我们所要做的就是比较$place 和$ r e s p o n s e。在比较结果的基础上,我们提供了一些反馈信息,之后每次都生成显示一个新的问题,或者再次显示相同的问题:
最终的脚本e d i t _ member.php 允许历史同盟成员编辑他们自己的联机项。无论何时,成员都可以校正或者更新他们的成员信息,而不必向同盟部提交这些更改。这个性能使成员目录总是保持最新的,而且减少了秘书的工作量。 我们需要采取的一个防范措施就是:除了该项目的成员之外,防止任何其他人修改项目。这意味着我们需要一些安全性的表单。作为一个简单的身份确认表单的示范,我们将使用mysql存放每个成员的口令,并要求成员提供正确的口令以访问脚本给出的编辑表单。该脚本操作如下: 当初次调用时,edit_script.php 给出包括成员id 和口令域的表单。 当提交初始表单时,脚本用成员id 作为关键字寻找相关的口令来搜索口令表。如果口令相符,脚本将从member 表中查找成员项,并显示要编辑的内容。 当提交编辑过的表单后,我们就用表单的内容更新项。 e d i t _ member.php 的框架如下所示:
我们将一个特殊项加到这个表中作为编号0,它有一个用于管理的(超级用户)口令。可以使用这个口令访问所有想要访问的项: insert into member_pass (member_id,password) values (0,"secret"); 在创建口令表之后,您可以停止使用第7章中编写的samp_browse 脚本,该脚本允许任何人在samp_db 数据库中浏览任何表的内容,其中包括member_pass 表。 当成员输入id 和口令并提交该表单时, e d i t _ member.php 显示该编辑的项:
display_entry() 需要做的第一件事就是校验口令。对于给定的成员id,如果表单中输入的口令与member_pass 表中存放的口令相符,或者如果它与管理口令相符(即成员0 的口令),e d i t _ member.php 就显示编辑的项。口令检查函数check_pass() 将执行一个简单的查询从member_pass 表中移出一条记录:
因为不能修改它,所以编辑表单作为只读文本显示成员id 的值。对于正常的成员,截止日期也作为只读文本显示,因为不能让成员改动它。然而,如果给出管理口令,则截止日期就成为可编辑的,允许同盟秘书为成员更新日期来重新更新他们的会员资格。 member 表项的列由display_column() 函数显示。它按照第三个参数值把列作为可编辑的文本或作为只读文本加到编辑表单中:
display_entry() 函数在格式中作为隐藏字段嵌入了member_id 和pass w o r d,因此当成员提交编辑的项时将继续edit_script.php 的下一个调用。这允许自动校验id 的口令,而不用请求成员再次输入(请注意,我们的简单的确认身份的方法是以文本形式来回传递口令。通常这不是个好主意,但是历史同盟不是对安全性要求很高的运作机构,因此这种方法足够满足要求。如果在运行金融业务,可能需要更强的安全性操作)。 更新项的函数如下:
首先,重新校验口令,确定没人发送假表单来愚弄我们,然后更新项。更新时需要注意,因为如果表单中的字段是空白的,则可能需要作为null 而不是作为空字符串输入。expiration 列就是这样的例子。null的成员截止日期具有特殊的含义,即“终生会员”。如果将一个空字符串插入到此列中,值转换成“ 0 0 0 0 - 0 0 - 0 0”,则成员不再具有终生会员资格。 为了处理这个问题,我们查找该列的元数据并检查它是作为null 还是作为not null进行声明的。该信息由函数mysql_fetch_field() 返回。不幸地是,此函数通过数值的索引查找列。在member 表中按名称访问列会更方便,因此我们编写一个小函n ul l a b l e ( ),它获取一个列名并查找相应的元数据对象:
mysql_fetch_field() 函数需要包含检查列所在表的结果集标识符。这可通过执行简单的不返回行的select 查询来获得。虽然该查询返回一个空结果集,但是,对于检索要评估member 表中列的空性能(n ul l a b i l i t y)的元数据来说,这种方法足够了: select * from member where 1=0 安装脚本,让成员们知道他们的口令,这样他们就能更新自己的成员信息了。