6、PHP 之道 - 数据库

2019-07-03

通常PHP代码使用数据库来持久化存储数据,并有多种方式去连接和操作数据库。在_PHP 5.1.0_之前,推荐的方式有mysqlmysqlipgsql等。

如果应用只是使用一个数据库的话,原生驱动就工作的非常好,否则使用MySQL的同时,还需要使用MSSQL或Oracle数据库的话,那么 就没有办法只使用一个原生驱动了,只能分别学习各个数据库驱动的API,这非常令人生厌。

另外需要注意,mysql这个原生驱动已经不在活跃开发状态了,从PHP 5.4.0开始被标记为不推荐使用,意味着将来版本如PHP 5.6可能会 移除这个扩展。如果你正在使用mysql_connect()mysql_query(),那么将来可能要重写部分代码,所以最好用mysqli或PDO来 代替。如果你正在开发新项目,请不要用mysql扩展,尝试用MySQLi扩展或PDO来替代

PHP: 选择MySQL API

PDO

PDO是数据库连接抽象库,从PHP 5.1.0开始提供,提供多种数据库的统一的操作接口。PDO不会转化你的SQL查询或者模拟缺失特性; 它只是提供统一的API去连接不同的数据库而已。

更重要的是,PDO允许你绑定SQL查询语句中的变量,而无需担心SQL注入问题,这主要通过PDO statements和变量绑定来实现。

假设PHP脚本接收一个数字ID作为查询参数,从数据库取回一条记录。下面是一种错误的做法:

<?php
$pdo = new PDO('sqlite:users.db');
$pdo->query("SELECT name FROM users WHERE id = " . $_GET['id']); // <-- NO!

这是非常糟糕的代码,直接在SQL中插入一个原始输入变量,导致潜在的SQL注入风险。假如黑客构造URL: http://domain.com/?id=1%3BDELETE+FROM+users来传入恶意参数id,则$_GET['id']的变量值为1;DELETE FROM users, 这将删除数据表中的所有用户!因此,你应该使用PDO的绑定参数功能来处理ID输入参数。

prepare('SELECT name FROM users WHERE id = :id');
    $stmt->bindParam(':id', $_GET['id'], PDO::PARAM_INT); // <-- Automatically sanitized by PDO
    $stmt->execute();

这才是正确的代码,在PDO statement中绑定一个参数,使得查询被发给数据库之前,对输入参数进行转义,防止SQL注入攻击。

学习PDO

另外一个要注意的问题是,如果数据库连接没有隐式地关闭,那么数据库连接数可能会超过数据库服务器的限制而连接失败,这种 错误在其他编程语言中比较常见。PDO对象在销毁的时候会隐式的关闭数据库连接,只要你把指向它的引用全部删除即可,如设置 为NULL。如果没有,PHP也会在脚本结束时关闭所有非持久化的数据库连接。

了解更多PDO连接

抽象层

很多框架都提供了自己的数据库抽象层,有的是基于PDO,有的不是。它们通过PHP方法来包装实际的查询,能够模拟出只存在于 某些数据库系统的特性,给你一个真正的数据库抽象层。这么做会带来一些性能的损失,但是在一个需要支持MySQL、PostgreSQL 和SQLite的应用中,这个损失相对于由此带来的代码一致性而言是可以接受的。

有些抽象层遵循PSR-0PSR-4命名空间标准,可以集成在任意的应用中:

Back to Top

异常

异常是大部分流行语言的标准特性,但是PHP开发者却不太重视。其他语言如 Ruby极度倚赖异常,在任何错误发生的时候,如HTTP请求失败 、DB查询错误,甚至图片资源未找到,都会抛出一个异常,以及时提示那里发生了一个错误。

PHP则对此很宽松,如调用file_get_contents()失败,只是返回FALSE并提示一个warning信息而已。很多老的PHP框架,如 CodeIgniter会返回false,然后在自己的日志里记录一个消息,开发者需要使用如$this->upload->get_error()的方式来查看发生了什么 错误。这么做需要你自己检查是否有错误,并需要根据不同类调用不同的方法来获取错误消息,而不能让错误明显的显示出来。

这种做法的另外一个弊端是当类自动在屏幕打印一个错误,然后退出进程,阻止了其他开发者动态处理该错误的机会。而异常则是让开发者知道 发生了错误,并让他们选择如何处理:

<?php
$email = new Fuel\Email;
$email->subject('My Subject');
$email->body('How the heck are you?');
$email->to('guy@example.com', 'Some Guy');

try
{
    $email->send();
}
catch(Fuel\Email\ValidationFailedException $e)
{
    // The validation failed
}
catch(Fuel\Email\SendingFailedException $e)
{
    // The driver could not send the email
}
finally
{
    // Executed regardless of whether an exception has been thrown, and before normal execution resumes
}

SPL异常

默认的异常类Exception包含的上下文信息很少,对于debug不方便,常见的做法是创建更具体的子类:

<?php
class ValidationException extends Exception {}

这使得你可以包含多个catch子句来处理不同的异常,但是这又会导致创建_很多的_自定义异常类,可以用SPL中的异常类来缓解这个问题 SPL扩展.

如使用__call()魔术方法,对不存在的方法调用抛出一个throw new BadFunctionCallException;,既避免了抛出含义模糊的 Exception异常,也省去了自定义异常类的麻烦。