如何使用 PHP SQL 准备语句避免代码重复?

How to avoid code repetition with PHP SQL prepared statements?

在我看到的大多数 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,
    ]);

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

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

如何更好地准备语句避免 PHP 中的过多冗余?

这不是多余的,它是最不复杂的例子。
当您必须使用不同的参数多次执行查询时,准备好的语句的优势会发挥更大的作用。

以下示例将按 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();

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

原始 PHP

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

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

我称它们为技巧,因为它们并不总是适用。

请注意,您必须为 table 中的所有列提供一个值。它可以只是一个 null 值,或者,要使其与省略的字段 100% 等效,您可以将其设置为 DEFAULT(field_name) 这样它将插入一个在 table 定义中定义的默认值.

辅助函数

下一个级别是为插入创建辅助函数。这样做时,必须敏锐地意识到SQL Injection through field names

因此,这样的辅助函数必须有自己的辅助函数:

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

有了这样的函数,我们可以创建一个辅助函数,它接受一个 table 名称和一个包含 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));
} 

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

现在您的插入代码将变为

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

删除了太多重复项

一个婴儿 ORM

不过我也不喜欢上面的解决方案,里面有一些怪癖。

为了满足自动化的需要,我宁愿创建一个简单的 ORM。不要被这个词吓到,它并不像某些人想象的那样可怕。我最近发布了一篇 ,因此您也可以将其用于您的案例,特别是考虑到您已经在使用 OOP。

只需输入一个insert()方法

public function insert()
{
    $fields = '`'.implode("`,`", $this->_fields).'`';
    $placeholders = str_repeat('?,', count($this->_fields) - 1) . '?';

    $data = [];
    foreach($this->_fields as $key)
    {
        $data[]  = $this->{$key};
    }
    $sql = "INSERT INTO `{$this->_table}` ($fields) VALUES ($placeholders)";
    $this->_db->prepare($sql)->execute($data);
}

之后你将不得不准备你的 class,

class Task extends BaseAcctiveRecord
{
    protected $_table = "tasks";
    protected $_fields = ['task_name', 'start_date', 'completed_date'];
}

然后 - 所有的魔法都在这里发生! - 您根本不需要编写插入代码! 只需创建 class 的新实例,为其属性赋值,然后调用 insert()方法:

include 'pdo.php';
$task = new Task($pdo);
$task->task_name = $taskName;
$task->start_date = $startDate;
$task->completed_date = $completedDate;
$user->insert();