在这篇文章中,我尽量用最浅显易懂的语言来说明使用 PHP, MySQL 制作一个动态网站的基本技术。阅读本文需要简单的 HTML 基础知识和(任一编程语言的)编程基础知识(例如变量、值、循环、语句块的概念等)。
PHP 基础
概述
PHP 是一种解释性语言,可用于对网页进行预处理。PHP 脚本在服务器端运行,其运行结果是一个可用来显示的网页。尽管可以完成许多类似工作,但是 JavaScript 和 PHP 的一大区别就是,JavaScript 是在浏览器端运行的。事实上,浏览器会接收 JavaScript 代码并运行它,所以用户是可以查看 JavaScript 代码的。而 PHP 不会将原始代码交给浏览器, 只会将其运行的结果交给浏览器,所以用 PHP 处理用户登陆、用户权限等问题是安全可靠的。
PHP 与 HTML
实际编写的时候,通常采用的方式是建立扩展名为 php
的文件(网页文件本质上是文本文件)。编写 php 代码和编写 html 代码并没有多少区别,而最方便的地方在于,在一个 php 文件中,两种代码是可以混编的。
规则:php 代码需要包含在 <?php ... ?>
标签中,就像这样:
提示:这是一个 php 和 html 混编的较为生动的例子。
这里的意思是,如果 php 中的变量 $var
的值为 true
,则放置一个标签,否则放置另一个标签。PHP 的 if 语句可以像上面那样写,也可以写成C/C++风格的:
关于 PHP 中的操作符
PHP 采用的操作符和 C/C++ 是类似的,例如用 =
表示赋值,==
表示相等性比较,以及 <
和 >
(小于、大于)比较符、!
取反、&&
逻辑与、||
逻辑或等。当然,也支持 +-*/
等数学表达式的运算。
关于 PHP 中的变量
PHP 中变量的命名一律以符号 $
开头,可以使用下划线,例如 $is_logged_in
就是一个表意清晰的变量名。和大多数编程语言不同,PHP 中变量没有类型的概念,而且不用声明就可以直接使用。虽然很爽,但是变量多了的时候也容易混乱,这一点需要特别注意。
关于 PHP 中的语句
这一点 PHP 和许多其他常见的编程语言很类似,也可以用 if...else
选择语句(之前已经见过了),PHP 还包括 while
循环、foreach
循环等,以后遇到了会详细介绍。
MySQL 基础
使用 MySQL 数据库是存储数据的一种方法,MySQL 需要和 PHP 配合来完成对数据库的查询(这里术语“查询”包括写入、更新、读取等)操作。利用 MySQL,你可以创建许多数据库(database),每个数据库可以包含多个表(table),而每个表包含若干字段。为了高效,一般会采取分类维护多个表的方式,而不是把所有数据都储存在同一个表中。
MySQL 需要服务器支持。使用的第一步是建立一个数据库,可以用相应的图形化工具(例如 phpMyAdmin)来建立数据库,也可以在终端直接使用下列 SQL 语句来创建一个名为 database_name 的数据库:
CREATE DATABASE database_name;
创建好数据库之后,需要创建表。可以通过下列 SQL 语句来创建一个名为 table_name 的表:
USE database_name;CREATE TABLE table_name ( first_name varchar(30), last_name varchar(30),);
第一句说明在哪个数据库中添加表,第二个说明添加的表的详情。这里我们在表中添加了两个字段,分别叫做 first_name
和 last_name
,它们的类型是 varchar(30)
。其中 varchar
是一种可变长字符类型,而 30
代表了最大长度。
其他常见的数据类型如下:
VARCHAR(100) --可变字符 CHARACTER(1) --定长INTEGER --整数DECIMAL(10, 2) --小数(小数点前后的位数)TIMESTAMP --日期和时间DATE --日期
你可能已经看出来了,MySQL 的注释符为 --
。你可能觉得没太大用,但是它却是一种稍后要提到的攻击的关键之处。此外,一般字符串都应该使用变长的VARCHAR
类型,而非定长的CHARACTER
类型,因为后者会占据更多的空间,而这是不必要的。
使 PHP 和 MySQL 协作
第一种方式
现在你已经创建好了 SQL 数据表,并对 PHP 语言有了一个概览。下面我们直奔主题,学习如何对数据表进行查询。
为了使 PHP 和 MySQL 进行交互,需要为 PHP 提供你的数据库用户名、密码、数据库名和数据表名。当然,最重要的,查询操作的 SQL 语句。我们一一来观察是如何实现的。
下面来解释一下这一坨代码的工作原理。
- 首先 3~6 行为 PHP 中的
define
语句,作用很明显,把DB_HOST
定义为localhost
,下面的代码中就可以使用DB_HOST
来代替localhost
。这样做的好处在于,如果mysqli_connect
函数在代码中出现多次,修改参数的时候则只需修改define
语句,非常方便。 - 然后是一个叫做
mysqli_connect()
的函数,它需要四个变量,分别是主机名、用户名、密码、数据库名。这个函数执行后的返回值传递给变量$dbc
,$dbc
包含了一次数据库连接。注意,这个变量名是任意的,并不强制要求叫做$dbc
。 - 然后,我们把要对数据库执行的操作对应的 SQL 语句以字符串的形式赋给变量
$query
。这个变量名也是任意的。应该注意到,这里的 SQL 语句是不以分号结尾的。 - 最后,我们执行
mysqli_query();
函数,该函数有两个参数,分别是一个数据库连接,和一个 SQL 查询操作。执行该函数后,相应的查询操作被执行。
如果把这些代码保存成一个网页,当用户打开网页的时候,如果各项参数正确,它就会完整地运行下去。
这里的 SQL 语句的含义是向叫做 table_name
的表中插入一行,其中把 colume#
字段的值相应地设置为 value#
。这里只设定了两个字段的数值(表中还可以有其他字段;没有显式说明的字段则留空或者使用数据表指定的默认值)。该语句的通用形式为:
INSERT INTO table_name (column1, column2, ...) VALUES ('value1', 'value2', ...)
如果你要做的仅仅是执行一个 SQL 语句,那么使用这种模式就可以完成。提醒一下,$dbc
变量往往是重复使用的。
另一个常用的 SQL 语句就是修改某一行。它的形式为:
UPDATE table_name SETcolumn1 = 'preferred_value1',column2 = 'preferred_value2',...,WHERE id = '$id'
当然,这个语句应该是写到一行的,不过为了清晰我分开来写。它的含义是,修改名为 table_name
的表中字段 id
的值是变量 $id
的值的所有行,把 column1
字段的值设为 preferred_value1
,把 column2
字段的值设为 preferred_value2,依此类推。
这里我们还看到,值既可以用常量表示,也可以用变量表示。
注意:会修改所有符合 WHERE
子句限定的条件的行(如果省略 WHERE
子句,就会修改所有行)。WHERE
子句可以设定多个条件,也可以使用比较运算符。例如:
WHERE age > 20 AND gender = 'male'WHERE is_admin = 'true' OR id = '$id'
(如果你想问 AND
和 OR
为什么不是符号 &&
和 ||
的话,我想提示你,不要把 PHP 语言和 SQL 语言搞混了。这是 SQL 语言,而我只说过 PHP 语言和 C/C++ 有些类似)。
下面介绍其他 SQL 语句。
-删除table_name表中的所有行DELETE FROM table_name--删除table_name表中email字段为david@example.com的所有行DELETE FROM table_name WHERE email = 'david@example.com'--删除table_name表DROP TABLE table_name--删除table_name表中的score字段ALTER TABLE table_name DROP COLUMN score--给table_name表添加一个叫age的字段,类型为 INTEGERALTER TABLE table_name ADD COLUMN age INTEGER
可见,第一种方式的本质就是编写一条 SQL 语句,然后通过 PHP 来执行它。下面,我们来看第二种方式。
第二种方式
有时,我们不满足于让服务器去执行一条 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 语句中的通配符换成字段名称。例如:
SELECT problem_name, problem_type FROM table_name WHERE problem_id='$id'
while 循环在 PHP 中的应用举例
如果我们要把一个数据库的许多行信息都展示在网页中,那么需要用到 while
循环和上面的第二种方式。代码如下:
"; echo "".$user_id.""; echo ""; }?>
如果有一定编程基础的话上面的代码很容易看懂。上面新出现了三种用法,说明如下:
SELECT
语句可以附加一个ORDER BY
子句,用来控制顺序。例如这里是按照字段 user_id 升序排列。下面的例子会先按照 user_rank 降序排列,user_rank 相同时按照 user_id 升序排列:
SELECT user_id, user_rank FROM table_name ORDER BY user_rank DESC, user_id ASC
- 关于 PHP 中的
echo
语句,它可以用来生成文本,类似于 C 中的printf()
函数。这里利用它直接生成 HTML 代码。它的用法参考例子就可以了。 - 关于符号
.
的用法,它的作用是连接字符串(和变量),往往和echo
配合使用,用法参考示例。
从表单获取信息
概述
这一部分我们演示如何构建一个表单,使用户填写这个表单并把内容储存到数据库。这一技术是用户注册系统和用户互动的基础。
要实现这个功能,需要 HTML 和 PHP 配合完成。HTML 负责表单,而 PHP 负责获取信息并使用 SQL 查询储存信息。首先来看 HTML 部分(就是普通的表单):
属于 HTML 部分的不再解释了,说一说新鲜的。这里的 action 属性后面的 $_SERVER['PHP_SELF']
(严格地说,$_SERVER
),是 PHP 的一个超级全局变量,内容是当前页面的相对路径,例如 signup.php
。这个 action 属性的含义是指定用户填写的信息在哪里被处理,这里是在当前页面处理。一般的做法都是将负责处理这部分信息的 PHP 代码和 HTML 代码放在同一页面内。
下面来看一下相应的 PHP 处理部分的代码:
提交成功"; } mysqli_close($dbc);?>
首先仍然是建立数据库连接。当用户点击 sumbit 按钮后,表单的内容会被储存在 PHP 中 $_POST
超级全局变量内,这个超级全局变量仍然是一个数组。isset()
函数用来检查变量是否被设置,只有用户点击 submit 后 isset($_POST['submit'])
才返回真,所以不用担心,首次加载表单(那时用户还没有填写任何内容)是不会执行这部分 PHP 代码的,只有用户提交之后才会执行。用户填写的具体内容可以用 $_POST['name']
来获取。这里的 name 对应的是 HTML 中 name 属性的内容。这一段程序把用户填写的内容赋给变量,然后执行插入到数据库的操作。
这里新出现了一个内容,就是 mysqli_close()
函数,它的作用是关闭数据库连接。当我们不再需要这个连接的时候,及时关闭是一个好主意。
需要注意的是,这仅仅是最简单的代码,而且实际上是不完善的。如果要真正投入使用,我们需要使它更健壮一些。下面逐一讨论这些内容。
检查用户输入是否合法
如果用户根本没有填写表单,就直接点击提交按钮,会发生什么?在上面的实例中,PHP 依然会乖乖地把空内容插入,而这显然是垃圾信息,不是我们需要的。所以,需要在插入前检查被插入的变量是否为空。例如:
这里出现了 empty()
函数,用于检查内容是否为空。注意这里使用 isset()
是无效的,因为 isset()
检查的是是否“被设置”,而被设置为空也属于被设置。
错误提示
用户输入有误时,上面的改进除了不执行SQL查询,并没有多少直观上的变化。用户不会收到任何信息表明他们的填写是不合适的。所以我们要在这时产生一些提示,引导用户正确填写表单。
防范 SQL 注入攻击
我们执行的 SQL语句中包含变量,执行的时候会直接把变量内容替换进去。而如果攻击者在输入框中输入一些危险的字符(通常包含 SQL 注释符 --
,以及其他预先精心设置的内容),就可能导致该次 SQL 查询完全被改写成攻击者需要的意思。为了防范这种攻击,我们需要对可能存在的危险字符进行过滤和转义,较为便捷的方法是使用两个函数。改进后的部分如下:
粘性表单
如果用户第一次填写失败,他们希望能保留已经填写好的内容,只做些修改就好了。这需要使用粘性表单技术。要实现,只需要稍稍改动 HTML 表单部分的代码:
显而易见,如果用户填写后因为某些原因没有提交而是回到了这个表单,并且之前填写了 user 字段的内容,那么此时 $user
变量已经被赋值了。那么就会在 HTML 表单显示这些内容,避免用户再次输入。
构造一个注册页面
虽然上面说了很多,但是仅仅满足了我们最基本的输入要求。许多时候我们需要更为复杂的功能。举例来说,要写一个注册页面,必须检查用户名是否重复,还要对密码采取某种技术加密以保证安全。
检查用户是否重复
基本原理就是,根据需要判重的字段(例如用户名)去数据库搜索。如果发现结果则用户名重复,如果没有找到则允许注册。需要一个新函数 mysqli_num_rows()
,返回 SELECT
语句得到的行数,根据其是否等于 0 进行判断。
把 $user
清空是为了配合粘性表单。
需要说明的是 exit()
; 函数,它会立刻终止 PHP 的运行。因为用户已经注册成功,没有必要执行后面的任何代码,所以使用这个函数。写自己的程序的时候可以亲自试验是否需要这一行、PHP 和 HTML 在 php 中的顺序不同有何影响。我通常的做法是把 PHP 代码放在前面,HTML 代码放在后面。
对密码进行加密存储
明文存储密码是对用户很不负责的,不仅数据库管理员可以看到密码,一旦数据库泄漏,密码就会被公开。所以,我们应该加密存储用户密码。在 PHP 中,可以使用 sha1()
函数进行加密(sha 即 secure hash algorithm 的首字母缩写),它是一种不可逆的加密,加密后会生成定长的一段字符串,并且是无法由这段字符串还原原密码的。
加密的原理是,用户输入密码后,利用 PHP 把 hash 过的密码储存在数据库中。用户登陆的时候,把用户输入的密码进行 hash 运算,之后和数据库中的进行比对。
使用方法如下:
sha1($password)
识别用户登录:Cookie
只注册没有用,必须添加登录功能。登录功能可以使用 Cookie 来实现。这里假定你已经了解 Cookie 的基础知识,只说如何实现。
设置 Cookie
上面的代码用来设置 Cookie,其中函数的第一个参数为 Cookie 名称,第二个参数为数值(这里用一个变量传递),第三个参数为过期时间,单位秒。示例为一个月。
可以用设置多个 Cookie 来存储许多内容,例如用户 ID、用户组(管理员还是普通用户)等。
验证 Cookie
用户登陆后,我们可以设置一个 Cookie 来存储登录信息(即哪个用户登陆的),然后通过检查这个 Cookie 来设定相应功能。
删除 Cookie
要删除 Cookie,只需要把过期时间设定在过去。
不要问我为什么设定在过去一个小时,设定几个小时都没问题。
Cookie 的安全性
设置 Cookie 有其潜在的危险。由于 Cookie 是保存在用户本地的,所以用户完全可以通过篡改 Cookie 来达到他们的目的。所以,把 Cookie 的值设置得“通俗易懂”不是一个好主意。例如,我们要用 Cookie 来保存登陆的用户名,如果单纯把这个用户名存入 Cookie,那么攻击者会很容易通过修改成他人的用户名来伪造 Cookie 登陆。所以,我们需要其他的手段来防止这一点。
我的做法是,用户注册的时候,把用户名按一定手段进行变换,然后使用 SHA()
函数加密生成一个用户密钥,然后把这个密钥储存进数据库。登陆时,再把这个密钥存储到 Cookie 中,通过检查 Cookie 中的密钥和数据库中用户密钥的匹配情况判定是哪位用户登录。这样,只要你的用户名变换方法不泄露,攻击者就很难按他们的想法伪造 Cookie。
使用 GET 方法
在网页间传递信息除了刚才介绍的 POST
方法外,还有 GET
方法。GET
方法是通过 URL 来完成信息传递的。例如,构造下列网址:
http://www.renfei.org/index.php?id=2
网址最后有 ?id=2
标记。这个信息会储存在$_GET['id']
这个超级全局变量中,并且可以在 PHP 中使用:
这个例子中我们把 2
赋给了变量 $id
。当然,也可以构造这样的网址:
http://www.renfei.org/index.php?id=2&message=10
除了多一个可以使用的 $_GET['message']
以外没有任何不同。
这个特性的用处之一就是可以根据网址的不同,配合数据库查询,返回不同的网页内容。例如我做的在线问答系统,就是根据 problem_id
来返回不同题目的。
注意,由于 GET
方法的数值是不可靠的(用户可以手动构造 URL 来传递他们想要的参数),所以应该仅仅用它来做一些无关痛痒的事情(例如显示不同的页面内容)。这里我并没有强调 GET
方法的数值是“透明”的:虽然 POST
方法的数值不会显示在 URL 中,但是它还是会通过 HTTP Header 发送到服务器,用许多插件和小工具都可以查看 HTTP Header 信息。
另外,如果你的表单是用来上传文件的,那么估计你会更喜欢 POST
方法:因为 GET
方法得到的 URL 可能会很长,甚至超过浏览器的限制!
使用模板
最后一部分,来讲一下使用模板构造一个网站。
事实上,网站的每个页面中,有许多部分是完全相同的,例如数据库连接常量(就是那些define
语句)以及每一页的 header 和 footer 部分等。这样,我们没必要在每一页内写相同的代码。除了麻烦和浪费空间以外,还有一点很重要的原因,就是修改的时候工作量很大。
PHP 中 require_once
语句作用就是把其他文件的内容插入此处。例如,我们可以创建一个define.php
,把define
语句全部写到里面,并在每个页面顶部添加如下语句:
这样一来,会把 define.php
中的内容插入当前位置。同理,我们可以建立一个 header.php
和 footer.php
,写好页面的头部、底部之后在每个其他页面导入就可以了。
PHP的错误处理
分级的错误信息
最后来讲一下 PHP 的错误处理机制。如果你写了有错误的 PHP 代码,那么运行的时候系统会自动生成一些错误提示信息并且打印到屏幕上,以提醒用户修复。通常,这些错误信息是分级的。首先,是 notice
。如果屏幕出现了 notice: (...)
的提示说明你有需要修复的小问题(你没有完全按照规则进行),不过问题不大,代码还是会继续执行完毕。而 warning
则更严重一些,如果出现 warning
,你可能需要思考一下你是否真的知道自己在做什么,并作出修改。但是,程序仍然会运行。如果出现了 error
,那么 PHP 是在跟你说:你是个白痴;这种代码无法执行,程序的运行会中止。
在写 PHP 程序的时候,我们需要这些错误提示来帮助我们改正错误,但是当产品发布的时候,开发人员往往倾向于隐藏错误提示:用户收到这些信息是很让人恼火的,而且,让他人知道你的代码有什么漏洞总归不是一个好主意,因为这可能被某些图谋不轨的攻击者加以利用。
Suppression Operator
有时,为了代码的简洁性考虑我们可能会故意犯一些无关痛痒的小错误。例如,如果 $_GET
中的某一个元素不一定总会被提交到 PHP,那么理论上应该使用 isset()
函数来进行检测。但是,如果你觉得到处使用这个函数太麻烦了,可以省略 isset()
函数而直接使用这个元素,只不过如果它没有被设置的话会返回一个 notice
错误信息(类似于 C/C++ 中的变量未声明)。这时,为了忽略这一条信息,可以使用错误抑制操作符 @
。例如:
其他提示
- 尽管没有特别强调,但是有几个函数是需要灵活掌握和使用的,例如
exit()
。它可以立刻结束 PHP 程序的运行。例如,有的页面需要一定用户权限才能访问,则可以把验证权限的代码放在页面顶端,如果验证失败则显示错误信息并调用exit()
函数。 - 当一个 SQL 连接的使命完成后,不要忘了用
mysqli_close()
关闭它。 - 设计 SQL 数据库的结构是一件非常重要的事情,设计的原则是高效且便于查询。一旦你的数据库充满各种信息,再想更改它的结构就会变得有些困难。
- SQL 的知识这里介绍得不多。它有许多特性,比如默认值、主键等。默认值的意思是如果不设定,那么该字段采用默认值;主键则规定该字段每行是不能重复的。默认值除了固定字符以外,还可以设定为时间,甚至自增。例如,要建立一个用户数据库,为每个用户分配一个唯一 ID,则可以把数据库中的 ID 字段设为 AUTO INCREMENT,这样每次不用手工维护这个字段,只要新增一行,这个字段的数值就增 1(默认从 1 开始),很方便。 一般会把这种 ID 字段设为主键。
- 本来打算简要介绍一下 PHP 和 MySQL 的,但是一写就是 7000 字。即使如此,本文介绍的所有特性也仅仅是构建一个动态网站最基本的知识,而且许多非重要的知识并没有介绍。你应该通过书籍更深入地学习。
- 额外推荐 PHP 的文档,对新手非常友好,值得一看: