简单 PHP + MySQL 数据库动态网站制作(速成篇)

时间:2024-03-08 22:29:37

由于学校社团活动需要一个支持数据库和用户提交信息的动态网站,所以花了几个晚上速成了一下 PHP 和 MySQL 的相关知识,之后就写了一个网站,结果表明运行的很顺利。之前感觉这种动态网站很深奥,以为会很难,可是没想到如果只实现不太复杂的功能的话,其实并不很难。

我争取用最浅显易懂的语言来说明一下是如何实现的。阅读本文需要简单的 HTML 基础知识和(任一编程语言的)编程基础知识(例如变量、值、循环、语句体的概念等)。

1 PHP 基础

1.1 概述

PHP 是一种解释性语言,可用于对网页进行预处理。PHP 脚本在服务器端运行,其运行结果是一个可用来显示的网页。尽管可以完成许多类似工作,但是 Javascript 和 PHP 的一大区别就是,Javascript 是在浏览器端运行的。事实上,浏览器会接收 Javascript 代码,并运行它,所以理论上用户是可以检查 Javascript 代码的。而 PHP 不会将原始代码交给浏览器, 只会将其运行的结果交给浏览器,所以用 PHP 处理用户登陆、用户权限等问题是安全可靠的。

1.2 PHP 与 HTML

实际编写的时候,通常采用的方式是建立扩展名为 php的文件(这里我假定你知道网页文件本质上是文本文件)。编写 php 代码和编写 html 代码并没有多少区别,而最方便的地方在于,在一个 php 文件中,两种代码是可以混编的。

规则:php 代码需要包含在 <?php ... ?> 标签中,就像这样:

提示:这是一个 php 和 html 混编的较为生动的例子。

这里的意思是,如果 php 中的变量 $var 的值为 true,则放置一个标签,否则放置另一个标签。

1.3 关于 PHP 中的操作符

PHP 采用的操作符和 C/C++ 是类似的,例如用 = 表示赋值,== 表示相等性比较,以及 < 和> (小于、大于)比较符、! 取反、&&逻辑与、||逻辑或等。当然,也支持 +-*/ 等数学表达式的运算。

1.4 关于 PHP 中的变量

PHP 中变量的命名一律以符号 $ 开头,可以使用下划线,例如 $is_logged_in 就是一个表意清晰的变量名。和大多数编程语言不同,PHP 中变量没有类型的概念,而且不用声明就可以直接使用。虽然很爽,但是变量多了的时候也容易混乱,这一点需要特别注意。

1.5 关于 PHP 中的语句

这一点 PHP 和许多其他常见的编程语言很类似,也可以用 if...else 选择语句(之前已经见过了),PHP 还包括 while 循环、foreach 循环等,不过不是特别常用(至少和 if...else比起来)。以后遇到了会详细介绍。

 

2 MySQL 基础

使用 MySQL 数据库是存储数据的一种方法,MySQL 需要和 PHP 配合来完成对数据库的查询(这里术语“查询”包括写入、更新、读取等)操作。利用 MySQL,你可以创建许多数据库(database),每个数据库可以包含多个表(table),而每个表包含若干字段。为了高效,一般会采取分类维护多个表的方式,而不是把所有数据都储存在同一个表中。

MySQL 需要服务器支持。使用的第一步是建立一个数据库,可以用相应的图形化工具(例如 phpMyAdmin)来建立数据库,也可以在终端直接使用下列 SQL 语句来创建一个名为 database_name 的数据库:

创建好数据库之后,需要创建表。可以通过下列 SQL 语句来创建一个名为 table_name 的表:

第一句说明在哪个数据库中添加表,第二个说明添加的表的详情。这里我们在表中添加了两个字段,分别叫做 first_name 和 last_name,它们的类型是 varchar(30)。其中 varchar 是一种可变长字符类型,而 30 代表了最大长度。

其他常见的数据类型如下:

你可能已经看出来了,PHP 的注释符为 --。你可能觉得没太大用,但是它却是一种稍后要提到的攻击的关键之处。

3 使 PHP 和 MySQL 协作

3.1 第一种方式

现在你已经创建好了 SQL 数据表,并对 PHP 语言有了一个概览。下面我们直奔主题,学习如何对数据表进行查询(见之前查询这个词的含义)。

为了使 PHP 和 MySQL 进行交互,需要为 PHP 提供你的数据库用户名、密码以及数据库名、数据表名,当然,最重要的,查询操作的 SQL 语句。我们一一来观察是如何实现的。

下面来解释一下这一坨代码的工作原理。

  • 首先 3~6 行为 PHP 中的 define 语句,作用很明显,把 DB_HOST 定义为 localhost,下面的代码中就可以使用 DB_HOST 来代替 localhost。这样做的好处在于,如果这些内容在代码中出现多次,修改的时候非常方便。
  • 然后是一个叫做 mysqli_connect() 的函数,它需要四个变量,分别是主机名、用户名、密码、数据库名。这个函数执行后的返回值传递给变量 $dbc$dbc 包含了一次数据库连接。注意,这个变量名是任意的。
  • 然后,我们把要对数据库执行的操作对应的 SQL 语句以字符串的形式赋给变量 $query。这个变量名也是任意的。应该注意到,这里的 SQL 语句是不以分号结尾的。
  • 最后,我们执行 mysqli_query(); 函数,该函数有两个参数,分别是一个数据库连接,和一个 SQL 查询操作。执行该函数后,相应的查询操作被执行。

事实上,如果把这些代码保存成一个网页,当用户打开网页的时候,如果各项参数正确,它就会完整地运行下去。

这里的 SQL 语句的含义是向叫做 table_name 的表中插入一行,其中把 colume_name# 字段的值相应地设置为 value#。这里只设定了两个字段的数值(并不说明表中只有两个字段。其他字段不说明则留空)。该语句的通用形式为:

如果你要做的仅仅是执行一个 SQL 语句,那么使用这种模式就可以完成。提醒一下,$dbc 变量往往是重复使用的。

另一个常用的 SQL 语句就是修改某一行。它的形式为:

当然,这个语句应该是写到一行的,不过为了清晰我分开来写。它的含义是,修改名为table_name 的表中字段 id 的值是变量 $id 的值的所有行,把 column_name1 字段的值设为 preferred_value1,把 column_name2 字段的值设为 preferred_value2。这里我们还看到,值既可以用常量表示,也可以用变量表示。

注意:会修改所有符合 WHERE 子句限定的条件的行(如果省略 WHERE 子句,就会修改所有行)。WHERE 子句可以设定多个条件,例如写成:

都是合法的(如果你想问 AND 和 OR 为什么不是符号 && 和 || 的话,我想提示你,不要把 PHP 语言和 SQL 语言搞混了。这是 SQL 语言,而我只说过 PHP 语言和 C/C++ 有些类似)。

下面介绍其他 SQL 语句。这些语句都涉及到修改表的结构,所以一般不会在一个 php 中出现。

可见,第一种方式的本质就是编写一条 SQL 语句,然后通过 PHP 来执行它。下面,我们来看第二种方式。

3.2 第二种方式

有时,我们不满足于让服务器去执行一条 SQL 语句。我们会需要从数据库中查询信息,然后把得到的信息储存起来(其实就是储存在变量中)。这样,我们需要一些额外的工作。先看一坨代码:

这里我们省略了 define 语句。

这一坨代码和上一坨的主要区别是,我们使用了 mysqli_query() 函数的返回值,把它保存到$result 变量中。这个变量的命名也是随意的,包括别的变量也是,以后我不再说了。这个$result 变量是执行 SELECT 语句的返回结果。解释一下 SELECT 语句,它的作用是选取table_name 表中符合 WHERE 子句条件的所有行。上面的语句会选定每一行的所有字段(通配符说明了这一点),并且把这些信息全部储存到变量 $result中。然后,用变量 $row 储存mysqli_fetch_array() 函数的返回值。$row 这个变量非常神奇,$row[\'column_name\']这个事儿包含的内容正是刚才选定的行的 column_name 字段的值(事实上,$row 正是一个数组)。这里,我们把它赋给了 $problem_title 变量。

到这里你应该问一个问题:如果满足 WHERE 子句条件的有很多行那么会发生什么?要解答这个问题,需要稍微细致的讲解一下 $row 这个事儿。如果满足条件的只有一行,那么使用 $row = mysqli_fetch_array($result) 自然会把这唯一的一行信息储存到 $row 中。如果有很多行,那么第一次使用 $row = mysqli_fetch_array($result) 会把第一行的信息储存到 $row 中,而第二次使用 $row = mysqli_fetch_array($result) 会把第二行的信息储存到 $row 中。如果这时没有下一行了,再次调用的话 $row 会储存逻辑假(false 或 0)。类似,如果符合WHERE 子句条件的一行都没有,那么执行后 $row 直接存储逻辑假。

最后补充一点刚才没有提到的。如果不需要所有字段的数据,可以只选择需要的字段。方法是把原来 SQL 语句中的通配符换成字段名称。例如:

 

3.3 while 循环在 PHP 中的应用举例

如果我们要把一个数据库的许多行信息都展示在网页中,那么需要用到 while 循环和上面的第二种方式。代码如下:

如果有一定编程基础的话上面的代码很容易看懂。上面新出现了三种用法,说明如下:

  • SELECT 语句可以附加一个 ORDER BY 子句,用来控制顺序。例如这里是按照字段 user_id 升序排列。下面的例子会先按照 user_rank 降序排列,user_rank 相同时按照 user_id 升序排列:

 

 

  • 关于 PHP 中的 echo 语句,它可以用来生成文本,类似于 C 中的 printf() 函数。这里利用它直接生成 HTML 代码。它的用法参考例子就可以了。
  • 关于符号 . 的用法,它的作用是连接字符串(和变量),往往和 echo 配合使用,用法参考示例。

4 从表单获取信息

4.1 概述

这一部分我们演示如何构建一个表单,使用户填写这个表单并把内容储存到数据库。这一技术是用户注册系统和用户互动的基础。可能稍微复杂一些,我会尽量说清楚。

要实现这个功能,需要 HTML 和 PHP 配合完成。HTML 负责表单,而 PHP 负责获取信息并使用 SQL 查询储存信息。首先来看 HTML 部分(就是普通的表单):

属于 HTML 部分的不再解释了,说一说新鲜的。这里的 action 属性后面的$_SERVER[\'PHP_SELF\'](严格地说,$_SERVER),是 PHP 的一个超级全局变量,内容是当前页面的相对路径,例如 signup.php。这个 action 属性的含义是指定用户填写的信息在哪里被处理,这里是在当前页面处理。一般的做法都是将负责处理这部分信息的 PHP 代码和 HTML 代码放在同一页面内。

下面来看一下相应的 PHP 处理部分的代码:

首先仍然是建立数据库连接。当用户点击 sumbit 按钮后,表单的内容会被储存在 PHP 中$_POST 超级全局变量内,这个超级全局变量仍然是一个数组。isset() 函数用来检查变量是否被设置,只有用户点击 submit 后 isset($_POST[\'submit\']) 才返回真,所以不用担心,首次加载表单(那时用户还没有填写任何内容)是不会执行这部分 PHP 代码的,只有用户提交之后才会执行。用户填写的具体内容可以用 $_POST[\'name\'] 来获取。这里的 name 对应的是 HTML 中 name 属性的内容。这一段程序把用户填写的内容赋给变量,然后执行插入到数据库的操作。

这里新出现了一个内容,就是 mysqli_close() 函数,它的作用是关闭数据库连接。当我们不再需要这个连接的时候,及时关闭是一个好主意。

需要注意的是,这仅仅是最简单的代码,而且实际上是不完善的。如果要真正投入使用,我们需要使它更健壮一些。下面逐一讨论这些内容。

4.2 检查用户输入是否合法

如果用户根本没有填写表单,就直接点击提交按钮,会发生什么?在上面的实例中,PHP 依然会乖乖地把空内容插入,而这显然是垃圾信息,不是我们需要的。所以,需要在插入前检查被插入的变量是否为空。例如:

这里出现了 empty() 函数,用于检查内容是否为空。注意这里使用 isset() 是无效的,因为isset() 检查的是是否“被设置”,而被设置为空也属于被设置。

4.3 错误提示

用户输入有误时,上面的改进除了不执行SQL查询,并没有多少直观上的变化。用户不会收到任何信息表明他们的填写是不合适的。所以我们要在这时产生一些提示,引导用户正确填写表单。

 

4.4 防范 SQL 注入攻击

我们执行的 SQL语句中包含变量,执行的时候会直接把变量内容替换进去。而如果攻击者在输入框中输入一些危险的字符(通常包含 SQL 注释符 --,以及其他预先精心设置的内容),就可能导致改次 SQL 查询完全被改写成攻击者需要的意思。为了防范这种攻击,我们需要对可能存在的危险字符进行过滤和转义,较为便捷的方法是使用两个函数。改进后的部分如下:

 

4.5 粘性表单

如果用户第一次填写失败,他们希望能保留已经填写好的内容,只做些修改就好了。这需要使用粘性表单技术。要实现,只需要稍稍改动 HTML 表单部分的代码:

显而易见,如果用户填写后因为某些原因没有提交而是回到了这个表单,并且之前填写了 user 字段的内容,那么此时 $user 变量已经被赋值了。那么就会在 HTML 表单显示这些内容,避免用户再次输入。

5 构造一个注册页面

虽然上面说了很多,但是仅仅满足了我们最基本的输入要求。许多时候我们需要更为复杂的功能。举例来说,要写一个注册页面,必须检查用户名是否重复,还要对密码采取某种技术加密以保证安全。

5.1 检查用户是否重复

基本原理就是,根据需要判重的字段(例如用户名)去数据库搜索。如果发现结果则用户名重复,如果没有找到则允许注册。需要一个新函数 mysqli_num_rows(),返回 SELECT 语句得到的行数,根据其是否等于 0 进行判断。

把 $user 清空是为了配合粘性表单。

需要说明的是 exit(); 函数,它会立刻终止 PHP 的运行。因为用户已经注册成功,没有必要执行后面的任何代码,所以使用这个函数。写自己的程序的时候可以亲自试验是否需要这一行、PHP 和 HTML 在 php 中的顺序不同有何影响。我通常的做法是把 PHP 代码放在前面,HTML 代码放在后面。

5.2 对密码进行加密存储

明文存储密码是对用户很不负责的,不仅数据库管理员可以看到密码,一旦数据库泄漏,密码就会被公开。所以,我们应该加密存储用户密码。在 PHP 中,可以使用 SHA() 函数进行加密,它是一种不可逆的加密,加密后会生成定长的一段字符串,并且是无法由这段字符串还原原密码的。

加密的原理是,用户输入密码后,利用 PHP 把 hash 过的密码储存在数据库中。用户登陆的时候,把用户输入的密码进行 hash 运算,之后和数据库中的进行比对。

使用方法如下:

 

5.3 其他要素

对于注册页面,不要忘记确认密码,即让用户输入两次,比较确定相等后再执行注册。

只注册没有用,必须添加登录功能。登录功能可以使用 Cookie 来实现。这里假定你已经了解 Cookie 的基础知识,只说如何实现。

 

上面的代码用来设置 Cookie,其中函数的第一个参数为 Cookie 名称,第二个参数为数值(这里用一个变量传递),第三个参数为过期时间,单位秒。示例为一个月。

可以用设置多个 Cookie 来存储许多内容,例如用户 ID、用户组(管理员还是普通用户)等。

用户登陆后,我们可以设置一个 Cookie 来存储登录信息(即哪个用户登陆的),然后通过检查这个 Cookie 来设定相应功能。

 

要删除 Cookie,只需要把过期时间设定在过去。

不要问我为什么设定在过去一个小时,设定几个小时都没问题。

设置 Cookie 有其潜在的危险。由于 Cookie 是保存在用户本地的,所以用户完全可以通过篡改 Cookie 来达到他们的目的。所以,把 Cookie 的值设置得“通俗易懂”不是一个好主意。例如,我们要用 Cookie 来保存登陆的用户名,如果单纯把这个用户名存入 Cookie,那么攻击者会很容易通过修改成他人的用户名来伪造 Cookie 登陆。所以,我们需要其他的手段来防止这一点。

我的做法是,用户注册的时候,把用户名按一定手段进行变换,然后使用 SHA() 函数加密生成一个用户密钥,然后把这个密钥储存进数据库。登陆时,再把这个密钥存储到 Cookie 中,通过检查 Cookie 中的密钥和数据库中用户密钥的匹配情况判定是哪位用户登录。这样,只要你的用户名变换方法不泄露,攻击者就很难按他们的想法伪造 Cookie。

7 使用 GET 方法

在网页间传递信息除了刚才介绍的 POST 方法外,还有 GET 方法。GET 方法是通过 URL 来完成信息传递的。例如,构造下列网址:

网址最后有 ?id=2 标记。这个信息会储存在 $_GET[\'id\'] 这个超级全局变量中,并且可以在 PHP 中使用:

这个例子中我们把 2 赋给了变量 $id。当然,也可以构造这样的网址:

除了多一个可以使用的 $_GET[\'message\'] 以外没有任何不同。

这个特性的用处之一就是可以根据网址的不同,配合数据库查询,返回不同的网页内容。例如我做的在线问答系统,就是根据 problem_id 来返回不同题目的。

注意,由于 GET 方法的数值是不可靠的(用户可以手动构造 URL 来传递他们想要的参数),所以应该仅仅用它来做一些无关痛痒的事情(例如显示不同的页面内容)。这里我并没有强调 GET 方法的数值是“透明”的:虽然 POST 方法的数值不会显示在 URL 中,但是它还是会通过 HTTP Header 发送到服务器,用许多插件和小工具都可以查看 HTTP Header 信息。

另外,如果你的表单是用来上传文件的,那么估计你会更喜欢 POST 方法:因为  GET 方法得到的 URL 可能会很长,甚至超过浏览器的限制!

8 POST 和 GET 方法混用

设计较为复杂的页面的时候,我们往往需要在一个页面内同时处理 POST 和 GET 方法的数据。例如一个答题页面,需要 GET 方法来获取题目信息,同时需要 POST 方法来把用户的回答储存到数据库中。同时,这个页面可能还需要针对不同的回答给出不同的反馈。

要处理这个问题,只需要理解并用好 isset() 函数即可。例如,通常可以这样组织页面:

 

9 使用模板

最后一部分,来讲一下使用模板构造一个网站。

事实上,网站的每个页面中,有许多部分是完全相同的,例如每一页的 header 和 footer 部分。这样,我们没必要在每一页内写相同的代码。除了麻烦和浪费空间以外,还有一点很重要的原因,就是修改的时候工作量很大。

PHP 中 require_once() 函数的作用就是把其他文件的全部插入此处。例如:

会把 define.php 中的内容插入当前位置。所以,我们可以建立一个 header.php 和footer.php,写好页面的头部、底部之后在每个其他页面导入就可以了。另外,用于判断用户是否已登录的语句、用于定义数据库信息的语句等都用该使用模板,而不是在每一页分别加入。

10 PHP的错误处理

10.1 分级的错误信息

最后来讲一下 PHP 的错误处理机制。如果你写了有错误的 PHP 代码,那么运行的时候系统会自动生成一些错误提示信息并且打印到屏幕上,以提醒用户修复。通常,这些错误信息是分级的。首先,是 notice。如果屏幕出现了 notice: (...) 的提示说明你有需要修复的小问题(你没有完全按照规则进行),不过问题不大,代码还是会继续执行完毕。而 warning 则更严重一些,如果出现 warning,你可能需要思考一下你是否真的知道自己在做什么,并作出修改。但是,程序仍然会运行。如果出现了 error,那么 PHP 是在跟你说:你是个白痴;这种代码无法执行,程序的运行会中止。

在写 PHP 程序的时候,我们需要这些错误提示来帮助我们改正错误,但是当产品发布的时候,开发人员往往倾向于隐藏错误提示:用户收到这些信息是很让人恼火的,而且,让他人知道你的代码有什么漏洞总归不是一个好主意,因为这可能被某些图谋不轨的攻击者加以利用。

10.2 Suppression Operator

有时,为了代码的简洁性考虑我们可能会故意犯一些无关痛痒的小错误。例如,如果 $_GET 中的某一个元素不一定总会被提交到 PHP,那么理论上应该使用 isset() 函数来进行检测。但是,如果你觉得到处使用这个函数太麻烦了,可以省略 isset() 函数而直接使用这个元素,只不过如果它没有被设置的话会返回一个 notice 错误信息(类似于 C/C++ 中的变量未声明)。这时,为了忽略这一条信息,可以使用错误抑制操作符 @。例如:

 

11 其他提示

  • 编写 PHP 程序需要特别小心,因为一旦一个细小的地方出错,就会导致整个页面无法显示,并且不会提示错误在哪,只能由开发人员需要手工寻找。
  • 尽管没有特别强调,但是有几个函数是需要灵活掌握和使用的,例如 exit()。它可以立刻结束 PHP 程序的运行。例如,有的页面需要一定用户权限才能访问,则可以把验证权限的代码放在页面顶端,如果验证失败则显示错误信息并调用 exit() 函数。
  • 当一个 SQL 连接的使命完成后,不要忘了用 mysqli_close() 关闭它。
  • 设计 SQL 数据库的结构是一件非常重要的事情,设计的原则是高效且便于查询。一旦你的数据库充满各种信息,再想更改它的结构就会变得有些困难。
  • SQL 的知识这里介绍得不多。它有许多特性,比如默认值、主键等。默认值的意思是如果不设定,那么该字段采用默认值;主键则规定该字段每行是不能重复的。默认值除了固定字符以外,还可以设定为时间,甚至自增。例如,要建立一个用户数据库,为每个用户分配一个唯一 ID,则可以把数据库中的 ID 字段设为 AUTO INCREMENT,这样每次不用手工维护这个字段,只要新增一行,这个字段的数值就增 1(默认从 1 开始),很方便。 一般会把这种 ID 字段设为主键。
  • 本来打算简要介绍一下 PHP 和 MySQL 的,但是一写就是 7000 字。即使如此,本文介绍的所有特性也仅仅是构建一个动态网站最基本的知识,而且许多非重要的知识并没有介绍。你应该通过书籍更深入地学习。
  • 额外推荐 PHP 的文档,对新手非常友好,值得一看:http://us3.php.net/manual/en/