防止 SQL 注入的 PHP PDO 准备语句教程

什么是 PDO?

PDO(PHP 数据对象)是数据库查询的抽象层,是 MySQLi 的绝佳替代品,因为它支持12 种不同的数据库驱动程序。这对需要它的人和公司来说是一个巨大的好处。但是,请记住 MySQL 是迄今为止最流行的数据库。它还与 PHP 紧密结合,这就是为什么这个数字在 PHP 世界中要高得多,因为 PHP 和 MYSQL 就像花生酱和果冻一样。NoSQL 是另一回事,Firebase 和 MongoDB 是很好的选择,尤其是前者,因为它是一个实时数据库——PDO 显然不支持两者。

注意:对于本教程,我将严格使用 MySQL 显示非模拟(本机)PDO 准备语句,因此在不同的驱动程序上可能会有一些差异。

如果您知道您将使用的唯一 SQL 数据库是 MySQL 或 MariaDB,那么您可以在 PDO 或 MySQLi 之间进行选择。如果您想学习 MySQLi ,请查看以下教程。其中任何一种都可以完全接受,尽管 PDO 对大多数用户来说是更好的选择,因为它更简单、更通用,而 MySQLi 有时更适合高级用户,因为它有一些 MySQL 特定的功能。

很多人都认为 PDO 的主要优点是它可以从数据库移植到数据库。这是一个被夸大的好处,本质上是无稽之谈。SQL 并不打算以这种方式传输,因为每个 DB 驱动程序都有自己的细微差别;另外,除非您至少是中级大型公司,否则您真正决定在特定项目上切换数据库的频率是多少?即便如此,根据经验,通常还是更愿意坚持使用您正在使用的当前技术,除非有正当理由需要花费大量时间(金钱)来做这件事。

PDO 的真正优势在于,您在它支持的无数数据库中都使用了几乎相似的 API,因此您无需为每个数据库学习新的 API。命名参数无疑也是 PDO 的一大胜利,因为您可以在查询的不同位置重用相同的值。不幸的是,在关闭模拟模式的情况下,您不能多次使用相同的命名参数,因此对于本教程来说它是无用的。

PDO 的一个有争议的优点是您不需要使用bindParam()nor bindValue(),因为您可以简单地将值作为数组直接传递给执行。有些人可能会认为这被认为是不好的做法,因为您不能指定类型(字符串、整数、双精度、blob);一切都将被视为字符串并自动转换为正确的类型。在实践中,这不应该影响您的整数或双精度数,并且不会受到 SQL 注入的影响。本文将直接将值绑定到执行中。与 类似bindValue(),您可以同时使用值和变量。我们不会介绍这两种绑定方法,但如果您想了解两者之间的细微差别,请阅读本文的这一部分

如果您想了解 SQL 注入的工作原理,可以在此处阅读

PDO 准备好的报表如何工作

通俗地说,PDO 准备好的语句是这样工作的:

  1. 使用空值作为占位符准备 SQL 查询
  2. 将值或变量绑定到占位符
  3. 同时执行查询

创建新的 PDO 连接

我建议创建一个名为的文件pdo_connect.php并将其放在根目录之外(例如:html、public_html)。

$dsn = "mysql:host=localhost;dbname=myDatabase;charset=utf8mb4";
$options = [
  PDO::ATTR_EMULATE_PREPARES   => false, // turn off emulation mode for "real" prepared statements
  PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, //turn on errors in the form of exceptions
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, //make the default fetch be an associative array
];
try {
  $pdo = new PDO($dsn, "username", "password", $options);
} catch (Exception $e) {
  error_log($e->getMessage());
  exit('Something weird happened'); //something a user can understand
}
复制

那么这里发生了什么?第一行称为DSN,需要填写三个单独的值,即您的主机名、数据库和字符集。这是推荐的方法,您显然可以将您的字符集设置为您的应用程序需要的任何内容(尽管 utf8mb4 是相当标准的)。现在您可以传入您的 DSN 信息、用户名、密码和选项。

try/catch或者,您可以通过创建全局自定义异常处理程序来省略使用块。如果这包含在您的所有页面中,那么它将使用此自定义处理程序,除非您执行restore_exception_handler()以恢复到内​​置 PHP 异常处理程序或使用新函数和自定义消息调用set_exception_handler() 。

set_exception_handler(function($e) {
  error_log($e->getMessage());
  exit('Something weird happened'); //something a user can understand
});
$dsn = "mysql:host=localhost;dbname=myDatabase;charset=utf8mb4";
$options = [
  PDO::ATTR_EMULATE_PREPARES   => false, // turn off emulation mode for "real" prepared statements
  PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, //turn on errors in the form of exceptions
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, //make the default fetch be an associative array
];
$pdo = new PDO($dsn, "username", "password", $options);
复制

这是非常有争议的,但我喜欢 MySQLi 的一件事是默认情况下错误报告是关闭的。这很聪明,所以初学者不会不小心打印出他的密码。在 PDO 中,即使您可以控制消除错误,也不能对构造函数执行此操作。所以显然你应该首先设置你的 php.ini 用于生产。

为防止密码泄露,您的 php.ini 文件在生产环境中应如下所示:同时执行display_errors = Offlog_errors = On. 然后重启 Apache 或 Ngnix

现在,您网站上的所有错误都只会累积在您的错误日志中,而不是打印出来。

不过,我认为没有理由在您的错误日志中打印出您的密码,所以我建议您执行try/catchor set_exception_handler,同时执行error_log($e->getMessage()), not $e,这仍然包含您的敏感信息。这显然仅适用于创建新连接时。

命名参数

我真的很喜欢这个功能,这对 PDO 来说是一个巨大的优势。您指定一个名为的变量:id并在执行时为其赋值。尽管如前所述,如果关闭仿真模式,它的唯一优点是多次使用将变得毫无用处。

$stmt = $pdo->prepare("UPDATE myTable SET name = :name WHERE id = :id");
$stmt->execute([':name' => 'David', ':id' => $_SESSION['id']]);
$stmt = null;
复制

id从技术上讲,执行部分不需要前导冒号,如此处所述。但是,文档中的任何地方都没有明确说明这一点,因此虽然它应该可以工作,因为一些用户通过查看 C 代码已经敏锐地得出结论,但在技术上不建议这样做。我的预感是 PHP 最终还是会记录这一点,因为似乎有足够多的人省略了前导冒号。

我专门用了一节来介绍使用命名参数,因为本文的其余部分将?改为使用。请记住,绑定值时不能将两者混合在一起。

插入、更新和删除

所有这些都非常相似,因此将它们组合在一起。

插入

$stmt = $pdo->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)");
$stmt->execute([$_POST['name'], 29]);
$stmt = null;
复制

更新

您甚至可以链接prepare()execute(). 尽管您将无法使用任何功能,例如rowCount(),因此在实践中几乎没有用。

$stmt = $pdo->prepare("UPDATE myTable SET name = ? WHERE id = ?")->execute([$_POST['name'], $_SESSION['id']]);
$stmt = null;
复制

删除

$stmt = $pdo->prepare("DELETE FROM myTable WHERE id = ?");
$stmt->execute([$_SESSION['id']]);
$stmt = null;
复制

获取受影响的行数

获取受影响的行数非常简单,因为您需要做的就是$stmt->rowCount(). 通常,如果您使用相同的值更新表,它将返回 0。如果您想更改此行为,那么唯一的方法是在创建新连接 PDO::MYSQL_ATTR_FOUND_ROWS => true时全局添加此选项。

$stmt = $pdo->prepare("UPDATE myTable SET name = ? WHERE id = ?");
$stmt->execute([$_POST['name'], $_SESSION['id']]);
echo $stmt->rowCount();
$stmt = null;
复制

-1 - 查询返回错误。如果已经对 execute() 进行错误处理,则冗余

0 - UPDATE 上没有更新记录,没有与 WHERE 子句匹配的行或没有执行查询;仅匹配的行 ifPDO::MYSQL_ATTR_FOUND_ROWS => true

大于 0 - 返回受影响的行数;行匹配 if PDO::MYSQL_ATTR_FOUND_ROWS => true。如果数据库驱动程序支持,可用于获取 SELECT 中的行数,MySQL 支持

获取插入的最新主键

$stmt = $pdo->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)");
$stmt->execute([$_POST['name'], 29]);
echo $pdo->lastInsertId();
$stmt = null;
复制

检查是否重复输入

有时您可能需要为一列或多列强制使用唯一值。您显然可以简单地使用 SELECT 语句来检查是否已经有一行包含尝试插入的值。这样做不一定是错误的,但是当您可以轻松地检查错误消息时,进行额外的数据库查询是没有意义的。

您可以检查 SQLSTATE 或特定于供应商的错误。对于唯一约束上的重复条目 SQLSTATE 是23000,而 MySQL 错误代码是1062。这是错误列表的一个很好的参考。要获取 SQLSTATE,您可以使用$e->getCode()$e->errorInfo[0]; 要获取 MySQL 错误代码,您必须执行$e->errorInfo[1]注意, 的行为$e->getCode()与 MySQLi 相反,它将打印 MySQL 特定的错误代码。另外,不要使用PDO::errorCodeor PDO::errorInfo。坚持使用 PDOException 类,因为某种原因,PDO 类错误方法只是打印出00000

try {
  $stmt = $pdo->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)");
  $stmt->execute([$_POST['name'], 29]);
  $stmt = null;
} catch(Exception $e) {
  if($e->errorInfo[1] === 1062) echo 'Duplicate entry';
}
复制

如果您想知道,可以通过执行以下操作创建唯一约束:

ALTER TABLE myTable ADD CONSTRAINT unique_person UNIQUE (name, age)
复制

选择

要在 PDO 中获取结果,您可以选择$stmt->fetch()$stmt->fetchAll()。前者更通用,因为它可以用于获取一行,或者如果在循环中使用则全部获取。后者基本上是语法糖,因为它允许使用该命令在数组中获取整个结果集。如果您正在修改该数组,最好$stmt->fetch()在循环中使用,因为它使您不必“重新循环”它。您还可以使用$stmt->setFetchMode()更改默认获取模式,而不是将其传递给fetch()or fetchAll()。但这只是浪费了额外的行,并且只应在需要的情况下进行。

PDO 中的获取模式很容易成为我最喜欢的方面。我会将它们混合到我的示例中,但这里有一些我认为最有用的常量。

注意:其中一些获取模式使用按位运算符,例如. 对于 PDO,您基本上可以将其视为组合获取模式。这被称为包含或,并且是您需要担心的唯一按位运算符。

获取模式

fetch() 和 fetchAll()

  • PDO::FETCH_NUM- 获取数值数组
  • PDO::FETCH_ASSOC- 获取关联数组
  • PDO::FETCH_COLUMN- 只获取一列。标量 iffetch()被使用;如果使用一维数组fetchAll()。如果fetch(), 那么你可以使用fetchColumn()for 语法糖
  • PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE- 根据数据库值获取具有类名的对象。如果类不存在,将默认为stdClass

只需获取()

  • PDO::FETCH_OBJ- 作为通用 stdClass 对象获取。建议改用fetchObject()语法糖,因为它允许您获取匿名和指定的类。你仍然可以使用PDO::FETCH_CLASS,但你必须设置它setFetchMode(PDO::FETCH_CLASS, 'MyClass'),后跟fetch()。可以与 一起使用fetchAll(),但不是首选,因为它的通用性较差
  • PDO::FETCH_LAZY- 获取能够成为对象、关联或数值数组的PDORow引用。内存效率高,因为它会在调用对象、关联或数字索引时严格获取结果
  • PDO::FETCH_INTO- 提取到现有的类。必须使用setFetchMode(PDO::FETCH_INTO, $myClass),后跟fetch()

只需 fetchAll()

  • PDO::FETCH_CLASS- 作为一个通用的 stdClass 对象或者如果指定的话,提取到一个现有的类中;与使用PDO::FETCH_OBJif 匿名对象相同,因此使用 this 更有意义,因为它更灵活。也可以使用fetch(),但不是首选。
  • PDO::FETCH_KEY_PAIR- 获取一个键/值对,第一列作为唯一键,第二列作为单个值
  • PDO::FETCH_UNIQUE- 同PDO::FETCH_KEY_PAIR,只有值部分是数组
  • PDO::FETCH_GROUP- 通过公共列名获取并将所有行分组到该键作为关联数组的数组
  • PDO::FETCH_GROUP | PDO::FETCH_COLUMN- 同PDO::FETCH_GROUP,只是值部分是一维数值数组的数组
  • PDO::FETCH_GROUP | PDO::FETCH_CLASS- 与 相同PDO::FETCH_GROUP,但使用对象数组的数组代替

获取关联数组

由于我们将默认获取类型设置为关联数组,因此在获取结果时我们没有指定任何内容。

$stmt = $pdo->prepare("SELECT * FROM myTable WHERE id <= ?");
$stmt->execute([5]);
$arr = $stmt->fetchAll(PDO::FETCH_ASSOC);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

还有稍长的 while 循环版本,有时便于操作。

$arr = [];
$stmt = $pdo->prepare("SELECT * FROM myTable WHERE id <= ?");
$stmt->execute([5]);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
  $arr[] = $row;
}
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

输出:

[
  ['name' => 'Jerry', 'age' => 14, 'weight' => 129], 
  ['name' => 'Alexa', 'age' => 22, 'weight' => 108]
]
复制

获取数值数组

$stmt = $pdo->prepare("SELECT first_name, squat, bench_press FROM myTable WHERE weight > ?");
$stmt->execute([200]);
$arr = $stmt->fetchAll(PDO::FETCH_NUM);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

输出:

[
  ['Thad', 305, 250], 
  ['Larry', 225, 180]
]
复制

获取对象数组

类似于获取关联数组,但使用对象,因此您可以像访问它一样访问它$arr[0]->age

$stmt = $pdo->prepare("SELECT name, age, weight FROM myTable WHERE id > ?");
$stmt->execute([12]);
$arr = $stmt->fetchAll(PDO::FETCH_CLASS);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

输出:

[
  stdClass Object ['name' => 'Jerry', 'age' => 14, 'weight' => 129], 
  stdClass Object ['name' => 'Alexa', 'age' => 22, 'weight' => 108]
]
复制

您甚至可以将属性值附加到已经存在的类中,就像这样。

class myClass {}
$stmt = $pdo->prepare("SELECT name, age, weight FROM myTable WHERE id > ?");
$stmt->execute([12]);
$arr = $stmt->fetchAll(PDO::FETCH_CLASS, 'myClass');
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

输出:

[
  myClass Object ['name' => 'Jerry', 'age' => 14, 'weight' => 129], 
  myClass Object ['name' => 'Alexa', 'age' => 22, 'weight' => 108]
]
复制

请记住,在构造函数中设置它之前注入属性值具有不可预测的行为(如果你有的话)。这意味着如果您已经在构造函数中使用了其中一个变量名称,那么获取值将被默认值覆盖。此处记录了此行为。要确保在调用构造函数fetchAll(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'myClass')分配值,您必须执行.

另一个意想不到但可能有用的行为是您可以修改私有变量。我真的不确定我对此有何感受,因为这似乎违反了封装原则。

获取单行

$stmt = $pdo->prepare("SELECT id, name, age FROM myTable WHERE name = ?");
$stmt->execute([$_POST['name']]);
$arr = $stmt->fetch();
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

现在您可以访问每个变量,$arr['name']例如。

输出:

['id' => 645, 'name' => 'Joey', 'age' => 28]
复制

像 MySQLi bind_result() 一样获取单行

$stmt = $pdo->prepare("SELECT id, name, age FROM myTable WHERE id = ?");
$stmt->execute([3]);
$arr = $stmt->fetch(PDO::FETCH_NUM); //FETCH_NUM must be used with list
if(!$arr) exit('no rows');
list($id, $name, $age) = $arr;
echo $name; //Output: 'Jeremy'
$stmt = null;
复制

这是为了模仿bind_result()MySQLi 中的(唯一有益的)行为,即能够将值绑定到变量名。现在您可以像这样访问每个变量$name

获取标量(单值)

一个常见的用例是,如果您只想获取行数并将其存储在变量中。使用fetch(PDO::FETCH_COLUMN)布尔值有一个陷阱,因为没有办法区分无行和虚假值。下面的示例使用 MySQLCOUNT()函数,这显然可以只检查真实性。但是,对于其他所有情况,如果列本身是一个布尔值,例如 0,那么您应该使用其中一个$stmt->rowCount() === 0$colVal === false来检查是否没有行。

需要明确的是,当您需要使用 获取数组时,不会fetchAll(PDO::FETCH_COLUMN)发生此行为。尽管如此,如果您要fetch(PDO::FETCH_COLUMN)在循环中使用将值存储在数组中,那么这种意外行为仍然会发生。老实说,我不明白为什么有人会过度使用fetchAll(PDO::FETCH_COLUMN),但应该注意。

$stmt = $pdo->prepare("SELECT COUNT(*) FROM myTable WHERE weight < ?");
$stmt->execute([185]);
$count = $stmt->fetch(PDO::FETCH_COLUMN);
//Syntactic sugar for previous line: $count = $stmt->fetchColumn();
if(!$count) exit('No rows');
echo $count; //Output: 1784
$stmt = null;
复制

现在$count是行数的文字值。

获取多列作为单独的数组变量

这很方便,因为您可以轻松地将其分成一组单独的一维数组,而不仅仅是一个多维数组。请记住,我曾经rowCount()检查是否有任何行。大多数驱动程序无法使用rowCount()onSELECT语句,但 MySQL 可以。如果您使用不同的驱动程序,您可以isset()在 while 循环之后对每个数组变量使用,或者将每个变量声明为一个空数组。

$stmt = $pdo->prepare("SELECT id, age, height FROM myTable WHERE weight > ?");
$stmt->execute([120]);
if($stmt->rowCount() === 0) exit('No rows');
while ($row = $stmt->fetch()) {
  $ids[] = $row['id'];
  $ages[] = $row['age'];
  $names[] = $row['name']
}
var_export($ages);
$stmt = null;
复制

输出:

[8, 12, 28, 64, 43, 29]
复制

获取单列作为数组变量

与之前的示例相同的概念,但如果您需要做的只是获取只有一列的数组,这很方便。

$stmt = $pdo->prepare("SELECT height FROM myTable WHERE id < ?");
$stmt->execute([500]);
$heights = $stmt->fetchAll(PDO::FETCH_COLUMN);
if(!$heights) exit('No rows');
var_export($heights);
$stmt = null;
复制

输出:

[78, 64, 68, 54, 58]
复制

获取键/值对

这将创建一个关联数组,其格式为第一列作为键,第二列作为值。因此,您的第一列必须是唯一值。

$stmt = $pdo->prepare("SELECT id, name FROM myTable WHERE age < ?");
$stmt->execute([25]);
$arr = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

输出:

[7 => 'Jerry', 10 => 'Bill', 29 => 'Bobby']
复制

获取键/值对数组

因为显然没有更好的术语。我的意思是,键将是您的第一列,它必须是唯一值,而该值将是作为关联数组的其余列。

$stmt = $pdo->prepare("SELECT id, max_bench, max_squat FROM myTable WHERE weight < ?");
$stmt->execute([225]);
$arr = $stmt->fetchAll(PDO::FETCH_UNIQUE);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

我不确定为什么PHP 文档上的这条评论指出你必须按位并添加FETCH_GROUP,如下所示$stmt->fetchAll(PDO::FETCH_UNIQUE | PDO::FETCH_GROUP):从我的测试来看,它具有相同的效果。

输出:

[
  17 => ['max_bench' => 230, 'max_squat' => 175],
  84 => ['max_bench' => 195, 'max_squat' => 235],
  136 => ['max_bench' => 135, 'max_squat' => 285]
]
复制

分组获取

例如,假设您想按眼睛颜色进行分组。这种方便的获取模式使您可以非常简单地完成它。

$stmt = $pdo->prepare("SELECT eye_color, name, weight FROM myTable WHERE age < ?");
$stmt->execute([35]);
$arr = $stmt->fetchAll(PDO::FETCH_GROUP);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

输出:

[
  'green' => [
    ['name' => 'Patrick', 'weight' => 178],
    ['name' => 'Olivia', 'weight' => 132]
  ],
  'blue' => [
    ['name' => 'Kyle', 'weight' => 128],
    ['name' => 'Ricky', 'weight' => 143]
  ],
  'brown' => [
    ['name' => 'Jordan', 'weight' => 173],
    ['name' => 'Eric', 'weight' => 198]
  ]
]
复制

分组提取,一列

这个和上一个例子的区别本质上和FETCH_KEY_PAIRvs的情况是一样的FETCH_UNIQUE。前面的示例将第一列与一个数组分组,而这个示例将第一列与第二列中的所有值分组。

$stmt = $pdo->prepare("SELECT eye_color, name FROM myTable WHERE age < ?");
$stmt->execute([35]);
$arr = $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_COLUMN);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

输出:

[
  'green' => ['Patrick', 'Olivia'],
  'blue' => ['Kyle', 'Ricky'],
  'brown' => ['Jordan', 'Eric']
]
复制

获取组、对象数组

与在常规组中获取相同,但使用对象子数组。

$stmt = $pdo->prepare("SELECT eye_color, name, weight FROM myTable WHERE age < ?");
$stmt->execute([35]);
$arr = $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_CLASS);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

输出:

[
  'green' => [
    stdClass Object ['name' => 'Patrick', 'weight' => 178],
    stdClass Object ['name' => 'Olivia', 'weight' => 132]
  ],
  'blue' => [
    stdClass Object ['name' => 'Kyle', 'weight' => 128],
    stdClass Object ['name' => 'Ricky', 'weight' => 143]
  ],
  'brown' => [
    stdClass Object ['name' => 'Jordan', 'weight' => 173],
    stdClass Object ['name' => 'Eric', 'weight' => 198]
  ]
]
复制

您也可以指定一个类名。

class myClass {}
$stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_CLASS, 'myClass');
复制

提取到现有类

这与或几乎相同PDO::FETCH_CLASS。唯一的区别是它会提取到一个已经构建的类中,并且由于某种原因它不会让你修改私有变量。我个人不明白为什么他们为此制定了单独的获取模式,而不是允许您将其传递给with 。另一个令人讨厌的方面是 PDO 强制您使用,然后使用(会给您完全相同的结果)。PDO::FETCH_OBJfetchObject()fetch()PDO::FETCH_OBJ$stmt->setFetchMode(PDO::FETCH_INTO, $myClass)fetch()fetchAll()

class myClass {
  public $max_bench;
  public $max_squat;
}

$myClass = new myClass();
var_export($myClass);

$stmt = $pdo->prepare("SELECT max_bench, max_squat FROM myTable WHERE weight < ?");
$stmt->execute([200]);
$stmt->setFetchMode(PDO::FETCH_INTO, $myClass);
$arr = $stmt->fetch();
if(!$arr) exit('No rows');
var_export($arr); //$arr === $myClass
$stmt = null;
复制

输出:

myClass Object ['max_bench' => null, 'max_squat' => null]
myClass Object ['max_bench' => 225, 'max_squat' => 95]
复制

从数据库值中获取类

对于普通人来说,这可能不是很有用。但是对于在 PDO 中大量使用对象映射的用户来说,这实际上很酷。尽管这些类型的用户希望使用 ORM 或查询构建器,但它仍然展示了 PDO 本身的强大功能。

对于这项工作,您需要声明类的名称,否则它将只使用stdClass。所以你需要知道你的数据库的值,这可能很不方便。您也不允许声明参数参数,就像您PDO::FETCH_CLASS自己声明的那样。

Class California {}
Class Florida {}
Class NewYork {}

$stmt = $pdo->prepare("SELECT state, age, first_name FROM info WHERE id > ?");
$stmt->execute([20]);
$arr = $stmt->fetchAll(PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

输出:

[
  Florida object ['age' => 22, 'first_name' => 'Billy'],
  California object ['age' => 19, 'first_name' => 'Mary'],
  NewYork object['age' => 26, 'first_name' => 'Ryan']
]
复制

懒惰地获取对象、关联和数字数组

我敢肯定这听起来令人困惑,但我想不出更好的方式来描述它。这真的很简洁,因为您要获取一个PDORow对象,该对象本质上是指向结果集的指针。它实际上根本不获取任何东西,直到您使用数组或对象索引(惰性)。实现中最出色的部分是,一旦“获取”它,您就可以选择以最节省内存的方式将其用作对象、关联数组或数值数组。需要注意的是,如果索引越界,它会返回 null 而不是抛出错误。

$stmt = $pdo->prepare("SELECT state, age, first_name FROM info WHERE id > ?");
$stmt->execute([320]);
$arr = $stmt->fetch(PDO::FETCH_LAZY);
if(!$arr) exit('No rows');
//The following all have the same value
echo $arr[2];
echo $arr->first_name;
echo $arr['first_name'];
$stmt = null;
复制

LIKE

您可能会直觉地尝试执行以下操作。

$stmt = $pdo->prepare("SELECT id, name, age FROM myTable WHERE name LIKE %?%"); 
复制

但是,这行不通。这就是你如何以正确的方式做到这一点。

$search = "%{$_POST['search']}%";
$stmt = $pdo->prepare("SELECT id, name, age FROM myTable WHERE name LIKE ?");
$stmt->execute([$search]);
$arr = $stmt->fetchAll();
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

数组中的位置

如您所见,PDO 显然在这方面也很出色,因为代码要短得多,因为不需要用bindValue()or指定类型bindParam()

$inArr = [1, 3, 5];
$clause = implode(',', array_fill(0, count($inArr), '?')); //create 3 question marks
$stmt = $pdo->prepare("SELECT * FROM myTable WHERE id IN ($clause)");
$stmt->execute($inArr);
$resArr = $stmt->fetchAll();
if(!$resArr) exit('No rows');
var_export($resArr);
$stmt = null;
复制

与其他占位符

$inArr = [1, 3, 5];
$clause = implode(',', array_fill(0, count($inArr), '?')); //create 3 question marks
$fullArr = array_merge($inArr, [5]); //merge WHERE IN array with other value(s)
$stmt = $pdo->prepare("SELECT * FROM myTable WHERE id IN ($clause) AND id < ?");
$stmt->execute($fullArr);
$resArr = $stmt->fetchAll();
if(!$resArr) exit('No rows');
var_export($resArr);
$stmt = null;
复制

事务中的多个准备好的语句

如果要确保多个 SQL 调用是并发的,则必须使用事务。这可确保您的所有操作或任何操作都不会成功。例如,这对于将行传输到不同的表可能很有用。您需要将该行复制到新表并删除另一个。如果其中一个操作失败,那么它需要恢复到之前的状态。

try {
  $pdo->beginTransaction();
  $stmt1 = $pdo->prepare("INSERT INTO myTable (name, state) VALUES (?, ?)");
  $stmt2 = $pdo->prepare("UPDATE myTable SET age = ? WHERE id = ?");
  if(!$stmt1->execute(['Rick', 'NY'])) throw new Exception('Stmt 1 Failed');
  else if(!$stmt2->execute([27, 139])) throw new Exception('Stmt 2 Failed');
  $stmt1 = null;
  $stmt2 = null;
  $pdo->commit();
} catch(Exception $e) {
  $pdo->rollback();
  throw $e;
}
复制

重用相同的模板,不同的值

try {
  $pdo->beginTransaction();
  $stmt = $pdo->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)");
  if(!$stmt->execute(['Joe', 19])) throw new Exception('Stmt 1 Failed');
  else if(!$stmt->execute(['Ryan', 44])) throw new Exception('Stmt 2 Failed');
  $stmt = null;
  $pdo->commit();
} catch(Exception $e) {
  $pdo->rollback();
  throw $e;
}
复制

错误处理

为防止密码泄露,您的 php.ini 文件在生产环境中应如下所示:同时执行display_errors = Offlog_errors = On. 然后重启 Apache 或 Ngnix

如果您打开错误并强制它们成为异常,例如在创建新连接部分中,那么处理错误的最简单方法是将它们放在 try/catch 块中。现在您可以访问PDOException 类了。

初学者可能会认为正确的错误处理需要将每个查询块包装在单独的块中,类似于使用语句try/catch进行常规错误处理。if虽然这样做在技术上没有任何问题,但使用单个全局try/catch使用基Exception类或使用set_exception_handler(). 唯一的例外是事务,它应该有一个单独的事务,但随后抛出异常以使其进入全局try/catch

你所有的页面——即使是没有 PDO 的页面——都应该这样设置,因为你通常只需要为整个 php 页面提供一条消息。但是,有时您可能需要捕获特定情况,因此您可以根据需要使用任意数量的特定异常类型,以及Exception $e.

try {
  $stmt = $pdo->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)");
  if(!$stmt->execute(['Justin', 18])) throw new Exception('Stmt Failed');
  $stmt = null;

  $stmt = $pdo->prepare("SELECT * FROM myTable WHERE power_level > ?");
  if(!$stmt->execute([9000])) throw new Exception('Stmt Failed');
  $arr = $stmt->fetchAll();
  $stmt = null;

  try {
    $pdo->beginTransaction();
    $stmt1 = $pdo->prepare("INSERT INTO myTable (name, state) VALUES (?, ?)");
    $stmt2 = $pdo->prepare("UPDATE myTable SET age = ? WHERE id = ?");
    if(!$stmt1->execute(['Rick', 'NY'])) throw new Exception('Stmt 1 Failed');
    else if(!$stmt2->execute([27, 139])) throw new Exception('Stmt 2 Failed');
    $stmt1 = null;
    $stmt2 = null;
    $pdo->commit();
  } catch(Exception $e) {
    $pdo->rollback();
    throw $e;
  }
} catch(Exception $e) {
  error_log($e);
  exit('Error message that user can understand for this page');
}
复制

处理异常的另一种方法是创建一个用户定义的异常处理程序,我在前面提到过包含pdo_connect.php. _ 这样,您几乎可以忽略try/catch事务之外的所有查询,如果出现问题,您将在 ing之后抛出异常catch

set_exception_handler(function($e) {
  error_log($e);
  exit('Error inserting');
});
$stmt = $pdo->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)");
if(!$stmt->execute(['Justin', 18])) throw new Exception('Stmt Failed');
$stmt = null;
复制

为什么要检查真实性?

您可能已经注意到,如果它是快速的,我会抛出一个执行异常,这似乎是多余的,因为我们已经以异常的形式打开了错误处理。尽管如此,我注意到一个奇怪的行为,那就是execute()如果关闭了仿真模式,则在某些情况下只能返回 false,这是本教程讨论的唯一模式。它可能是特定于 MySQL 的,但我将它留在里面,因为当有太多参数必须执行时,我个人经历过这种情况。它只会返回 false 并表现得好像没有出错一样。这将在事务中产生特别不受欢迎的行为,因为查询会默默地失败,而其他查询会起作用,因此违背了其可线性化的目的。这就是为什么您必须检查真实性以防发生这种情况。我实际上找不到太多关于它的信息,但是这个 StackOverflow很好地描述了这个问题。奇怪的是,如果你没有绑定足够多的变量,它会正确地抛出异常。

一些额外的东西

我需要 $stmt = null 吗?

$stmt->close()这与在 MySQLi中使用基本相同,同样适用。不,这当然不是必需的,但被某些人认为是良好的编码实践(显然是主观的)。我更喜欢明确,我也同时做$stmt = null$pdo = null。如果您要关闭 PDO 连接,那么您也必须关闭准备好的语句,如此处所述。虽然这与 using 不完全相同$mysqli->close(),但它非常相似。用于关闭连接的 PDO 功能已被要求多年,并且是否会实现它是值得怀疑的。

如果您重用相同的变量名,关闭准备好的语句会很有用。两者都不是真正必要的,因为无论如何它们都会在脚本执行结束时关闭。

仿真模式与本机准备语句

使用准备好的语句时,您有两个选择:打开或关闭仿真模式。本文严格介绍了原生的prepared statements,因为我认为如果您的驱动程序版本支持它,您应该使用真正的prepared statements。模拟模式似乎更像是不支持本机准备语句的驱动程序/版本的后备解决方案;自版本 4.1 起,MySQL 已支持此功能。

当仿真模式打开时,它本质上就像使用PDO::quote或类型转换来手动格式化你的查询——它会自动地总是安全地做到这一点。虽然这在理论上应该仍然与使用 MySQL 5.5+ 并在创建连接时将字符集设置为utf8mb4一样安全,但我仍然建议使用本机准备好的语句。看看这篇关于一个不起眼的边缘案例攻击的优秀文章。

即使我们谈论的是理论上的威胁,非模拟预准备语句也完全消除了 SQL 注入攻击的可能性。带有虚拟占位符的查询首先发送到服务器,然后是要绑定的值——查询和数据是完全隔离的。

以下是两者之间的一些关键区别。

仿真模式:

  • 可以重用命名占位符
  • 允许多个查询支持
  • 需要bindValue()用于某些边缘情况,例如 LIMIT
  • 单个语句更快,但不能运行准备一次,执行多个
  • 每条语句花费n次客户端-服务器往返
  • 执行语句时报告错误

原生准备语句:

  • 可以运行准备一次,执行多次以提高效率
  • 每条语句花费1+n次客户端-服务器往返
  • 每个命名占位符只能使用一次
  • 无法运行多个查询(尽管您可以使用事务)
  • 理论上,由于查询和值被隔离,因此更安全
  • 准备语句时报告错误

下面是一个示例,说明如何在启用仿真模式的情况下使用 LIMIT。它发生的原因是因为 MySQL 最终将其解释为LIMIT '23'。因此,您可以使用本机准备好的语句,也可以使用bindValue()将其显式定义为int

$stmt = $pdo->prepare("SELECT full_name, gender FROM myTable WHERE id > ? LIMIT ?");
$stmt->bindValue(1, 39, PDO::PARAM_INT);
$stmt->bindValue(2, 23, PDO::PARAM_INT);
$arr = $stmt->fetchAll();
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

bindValue() 与 bindParam()

本教程并没有真正过多地介绍,因为您实际上并不需要这些,除非在需要强制数据类型的边缘情况下。尽管如此,了解这些差异还是值得的,因为您永远不知道何时会遇到可能有用的情况。

这两种方法都用于手动绑定到准备好的语句。不同之处在于bindValue()它更加通用,因为您可以绑定变量值,而bindParam()只能接受变量。因此,bindParam()bind_param()MySQLi 中的相同。那么,如果这种方法只有缺点,为什么还会存在呢?考虑以下情况。

$id = 25;
$stmt = $pdo->prepare("SELECT * FROM myTable WHERE id < ?");
$stmt->bindParam($id);
$id = 98;
$id = 39; //Final value
$stmt->execute();
$arr = $stmt->fetchAll();
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

使用bindParam(),您可以不断更改变量并重新执行。情况并非如此bindValue(),因为您需要再次调用该方法。我怀疑我是否会需要这个,但有这个选项很好。如果您查看文档,它的行为原因很明显,因为它是通过引用函数参数传递的。

$id = 25; //Final value
$stmt = $pdo->prepare("SELECT * FROM myTable WHERE id < ?");
$stmt->bindValue($id);
$id = 98;
$id = 39;
$stmt->execute();
$arr = $stmt->fetchAll();
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

那么使用准备好的语句意味着我可以免受攻击者的攻击吗?

虽然您可以避免 SQL 注入,但您仍然需要验证和清理用户输入的数据。您可以使用filter_var()之类的函数在将其插入数据库之前进行验证,并在检索后使用htmlspecialchars()进行清理。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了防止 SQL 注入攻击,PHP 提供了一些安全措施和最佳实践。下面是一些常见的方法: 1. 使用预处理语句(Prepared Statements):预处理语句是使用占位符来代替用户输入,然后将输入参数与 SQL 查询分离。这样可以防止恶意用户通过输入特殊字符来改变 SQL 查询的结构。使用 PDO 或者 MySQLi 扩展库提供的预处理语句功能可以有效防止 SQL 注入。 示例代码(使用 PDO 扩展库): ```php $stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username"); $stmt->bindParam(':username', $username); $stmt->execute(); ``` 2. 使用参数化查询:参数化查询是指在 SQL 查询中使用参数的方式,而不是直接将用户输入拼接到查询语句中。这样可以确保用户输入被正确地转义和处理,从而防止 SQL 注入。 示例代码(使用 PDO 扩展库): ```php $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); $stmt->execute([$username]); ``` 3. 输入验证和过滤:对用户输入进行验证和过滤,以确保输入符合预期的格式和类型。可以使用过滤器函数(如 filter_var)或自定义验证函数来验证输入数据的合法性。同时,对于需要存储到数据库的数据,应该使用适当的转义函数(如 mysqli_real_escape_string)对特殊字符进行转义。 示例代码: ```php $username = filter_var($_POST['username'], FILTER_SANITIZE_STRING); $password = mysqli_real_escape_string($conn, $_POST['password']); ``` 4. 使用安全的数据库连接:确保使用安全的数据库连接方式,如使用 PDO 或者 MySQLi 扩展库,并配置正确的连接参数。避免使用不安全的连接方式,如使用不可靠的连接库或者直接拼接用户输入的字符串作为连接参数。 需要注意的是,以上方法仅仅是防止 SQL 注入的基本措施,对于复杂的应用程序还需要综合考虑其他安全性措施,并定期更新和维护应用程序以防止新的安全漏洞。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值