如何避免PHP SQL预准备语句的代码重复?

jucafojl  于 2023-09-29  发布在  PHP
关注(0)|答案(2)|浏览(107)

在我看到的大多数SQL PHP准备语句的例子中,例如:

$sql = 'INSERT INTO tasks(task_name, start_date, completed_date) VALUES(:task_name, :start_date, :completed_date)';
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
        ':task_name' => $taskName,
        ':start_date' => $startDate,
        ':completed_date' => $completedDate,
    ]);

字段名称几乎重复...四次!

  • INSERT INTO(...)之后一次:task_name(SQL中的列名)
  • VALUES(...)之后::task_name
  • 一次在字典键中::task_name
  • 一次在字典值中:$taskName(局部变量)

我知道这些都有不同的含义,但这种冗余真的很烦人:如果我们想改变查询中的某些内容,我们必须改变它4次!

如何在PHP中有一个更好的准备语句避免这么多冗余?

x8diyxa7

x8diyxa71#

这是一个很好的问题,我有几个答案。

原始PHP

首先,您可以使用几种技巧来减少冗长,例如在查询中省略fields子句(并在values子句中为缺少的字段添加默认值)以及使用位置占位符:

$data = [$taskName, $startDate, $completedDate];
$sql = 'INSERT INTO tasks VALUES(null, ?, ?, ?)';
$db->prepare($sql)->execute($data);

我称之为技巧,因为它们并不总是适用的。
请注意,必须为表中的所有列提供一个值。它可以是一个简单的null值,或者,为了使它与省略的字段100%等效,您可以将它作为DEFAULT(field_name),以便它将插入表定义中定义的默认值。

辅助函数

下一个层次是为插入创建一个辅助函数。当这样做时,必须敏锐地意识到SQL Injection through field names
因此,这样的helper函数必须有自己的helper函数:

function escape_mysql_identifier($field){
    return "`".str_replace("`", "``", $field)."`";
}

有了这样一个函数,我们可以创建一个助手函数,它接受一个表名和一个包含field name => value对的数据数组:

function prepared_insert($conn, $table, $data) {
    $keys = array_keys($data);
    $keys = array_map('escape_mysql_identifier', $keys);
    $fields = implode(",", $keys);
    $table = escape_mysql_identifier($table);
    $placeholders = str_repeat('?,', count($keys) - 1) . '?';
    $sql = "INSERT INTO $table ($fields) VALUES ($placeholders)";
    $conn->prepare($sql)->execute(array_values($data));
}

我故意不使用命名的占位符,因为它使代码更短,占位符名称中可能不允许使用字符,但对于列名完全有效,例如空格或破折号;也是因为我们通常不关心它内部是如何工作的。
现在您的插入代码将变为

prepared_insert($db, 'tasks',[
    'task_name' => $taskName,
    'start_date' => $startDate,
    'completed_date' => $completedDate,
]);

去掉了这么多重复

一个婴儿ORM

然而我也不喜欢上面的解决方案,其中有一些怪癖。
为了满足自动化的需要,我们可以实现面向对象编程这样一个伟大的概念。这样,您必须为每个表精确地编写一次列列表。
我专门写了一个complete working example来演示这个概念。
首先,您需要创建一个原型类,它将创建用于任何表的所有公共方法。像

public function create($data): int
{
    $fields = $this->makeFieldList($data);
    $placeholders = str_repeat('?,', count($data) - 1) . '?';

    $sql = "INSERT INTO `$this->table` ($fields) VALUES ($placeholders)";
    $this->sql($sql,array_values($data));

    return $this->db->lastInsertId();
}

之后,您将为特定的表创建实际的类,您将在其中提供表名和列列表:

class UserGateway extends BasicTableGateway {
    protected $table = 'gw_users';
    protected $fields = ['email', 'password', 'name', 'birthday'];
}

然后-所有的魔法都在这里发生!- * 你根本不需要编写插入代码!* 相反,只需创建一个包含数据的数组,然后创建一个类的新示例,并调用该类的合适方法:

$data = [
    'email' => '[email protected]',
    'password' => 123,
    'name' => 'Fooster',
];

$userGateway = new UserGateway($pdo);

$id = $userGateway->create($data);
echo "Create: $id", PHP_EOL;

应该注意的是,现在可以使用mysqli或PDO,$db变量可以是任何一个。在将这个例子移植到mysqli时,只需要做一些小的调整

pqwbnv8z

pqwbnv8z2#

这不是多余的,它是最不复杂的例子。
当您必须使用不同的参数多次执行一个查询时,预处理语句的重复性会发挥更大的作用。
下面的示例将按value获取从1到10的所有行:

$value = 1;
$stmt = $pdo->prepare("INSER INTO (first, second, third) VALUES (?, ?, ?);");

$rowsToInsert = [["first", "second", "third"]];

foreach ($rowsToInsert as $row) {
  array_map($row, function($v, $i) use ($stmt) {
    $stmt->bindValue($i + 1, $v);
  });

  $stmt->execute();
}

你也可以自由使用其他php逻辑来绑定参数:

$params = [
 ":first" => $first,
 ":second" => $second,
 ":third" => $third
];

$sql = sprintf(
  "INSERT INTO (first, second, third) VALUES (%s);", 
  implode(" ", array_keys($params))
);

$stmt = $pdo->prepare($sql);

foreach ($params as $name => $value) {
  $stmt->bindValue($name, $value);
}

$stmt->execute();

相关问题