wraugh/pdoqt
Requires
- wraugh/defphp: 0.3.0
Requires (Dev)
- phpunit/phpunit: ^6.1
This package is not auto-updated.
Last update: 2024-09-24 22:48:03 UTC
README
Pdoqt 是一个围绕 PDO 的小型包装器,使得运行简单的查询变得简单,并可能运行复杂的查询。它提供两种类型的方法:一些执行查询,一些帮助构建查询。它不会阻止您构建或运行无效的 SQL;相反,它的设计是为了鼓励在 PHP 中编写查询的一致和清晰风格。
Pdoqt 以文学风格编写。感谢docco,相同的文本和代码可以在源代码、README 或 html 中阅读。
namespace Pdoqt;
class Pdoqt {
use \Defphp\Ex;
private $conn;
public function __construct (\PDO $connection) {
$this->conn = $connection;
}
public function getConn (): \PDO {
return $this->conn;
}
首先,我们需要创建一个围绕查询方法的错误检查包装器。根据我的经验,通常期望数据库语句成功执行。与它们的错误真的是一个异常,适合 try/catch 处理。
在这个过程中,我们可以将参数化语句和无参数语句组合在同一个方法中。
public function q (string $query, array $params = null): \PDOStatement {
if (isset($params)) {
$stmt = $this->conn->prepare($query);
$stmt->execute($params);
} else {
$stmt = $this->conn->query($query);
}
if ($stmt === false) {
$msg = "Error executing query $query";
if (isset($params)) {
$msg .= " w/ params " . json_encode($params);
}
$msg .= ": " . implode(" ", $this->conn->errorInfo());
static::ex('DbQueryError', $msg, self::ERR_QUERY);
}
return $stmt;
}
这样的简单包装器已经使得代码易于阅读,例如
try {
$db->q("CREATE TABLE foo (id INT NOT NULL, name TEXT NOT NULL)");
$db->q("INSERT INTO foo (id, name) VALUES (?, ?)", [1, "Foo One"]);
$foo = $db->q("SELECT * FROM foo WHERE id = :id", [":id" => 1])->fetch();
echo json_encode($foo) . "\n";
} catch (\Exception $e) {
error_log($e);
exit($e->getCode());
}
它还有一个非常通用的优点:您可以使用它来运行数据库服务器处理的任何语句。
尽管如此,我们通常想要运行选择查询。让我们编写一个只做这件事的方法
public function fetchAll (
string $query,
array $params = null,
string $indexBy = null
): array {
$stmt = $this->q($query, $params);
$rows = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
if (isset($indexBy)) {
$this->indexRowByCol($rows, $row, $indexBy);
} else {
$rows[] = $row;
}
}
return $rows;
}
注意,fetchAll
与q
接受相同的参数。这使得代码库更容易阅读(重复的模式更容易识别)。嗯,几乎是这样。fetchAll
有一个可选的第三个参数,允许您通过一列索引结果集,例如
$q = "INSERT INTO foo (name) VALUES ('Herp', 'Derp') RETURNING name";
$rows = $db->fetchAll($q, null, "name");
$derp = $rows["Derp"];
这仅在索引列没有空或重复值时才有效;通常您会为此使用 id 列。
private function indexRowByCol (array &$index, array $row, string $col) {
if (!isset($row[$col])) {
static::ex('DbIdx,DbIdxMissingCol',
"No value at column '$col' for row " . json_encode($row),
self::ERR_IDX_MISSING_COL);
}
if (isset($index[$row[$col]])) {
static::ex('DbIdx,DbIdxDuplicateCol',
"column $col isn't unique; found duplicate value " .
json_encode($row[$col]) . " in row " . json_encode($row),
self::ERR_IDX_DUPLICATE_COL);
}
$index[$row[$col]] = $row;
}
为每一行查找索引是不高效的,但同样,将整个结果集提取到数组中也是不高效的。fetchAll
仅适用于小型结果集。当预期大量数据时,可以直接调用 POD 的fetch
来逐行处理,例如
$stmt = $db->q("SELECT * FROM foobar");
while ($row = $stmt->fetch()) {
// do something with $row
}
当您期望获取单行(例如,通过 id 查询)时,您可以使用fetchOne
方法,这仅仅是fetchAll
加上合理性检查
public function fetchOne (string $query, array $params = null): array {
$rows = $this->fetchAll($query, $params);
if (count($rows) != 1) {
$msg = "Expected exactly one row but got " . count($rows) .
" from query $query";
if (isset($params)) {
$msg .= " w/ params " . json_encode($params);
}
if (count($rows)) {
static::ex('DbUnexpectedRows,DbTooManyRows',
$msg,
self::ERR_TOO_MANY_ROWS);
} else {
static::ex('DbUnexpectedRows,DbNoRows,NotFound',
$msg,
self::ERR_NO_ROWS);
}
}
return reset($rows);
}
对于插入、更新和删除查询,获取受影响行数通常很有用。
public function nAffected (string $query, array $params = null): int {
return $this->q($query, $params)->rowCount();
}
调用类似以下内容
$n = $db->nAffected("DELETE FROM foo WHERE id = ?", [1]);
与例如
$n = $db->q("DELETE FROM foo WHERE id = ?", [1])->rowCount();
相比,节省的打字不多,但可能更容易阅读。它遵循与fetchAll
和fetchOne
相同的模式。
围绕查询执行这些包装器并不能帮助构建查询,但它们确实鼓励与数据库交互的一致风格。因为我们希望直接编写 SQL,所以无法避免连接字符串。我们能做的就是提供方便的工具来做这件事,并依赖于编码风格/编写模式来保持事情整洁和清晰。
这样的工具之一是用于转义列名的辅助工具。这始终需要这样做;我们最好有统一的方式来做。
/**
* Escape column names for use in queries.
* For example:
*
* $cols = ['a', 'b', 'c'];
* $q = "INSERT INTO t (" . $this->cs($cols) . ") VALUES ...";
*
* If an element's key is not numeric, its key is used as the column name.
* That is, the following gives the same result as the above example:
*
* $cols = ['a', 'b' => 'foo', 'c'];
* $q = "INSERT INTO t (" . $this->cs($cols) . ") VALUES ...";
*/
public function cs (array $cols): string {
$cs = [];
foreach ($cols as $k => $v) {
$col = is_numeric($k) ? $v : $k;
$cs[] = '"' . strtr($col, ['"' => '""']) . '"';
}
return implode(', ', $cs);
}
下一个工具是帮助我们编写 INSERT 语句。通常,当您开始将记录插入数据库时,您会以键值数组的形式拥有它们。此方法将这些数组转换为 VALUES 文本
/**
* Generate parameters and their placeholders for use in VALUES commands.
* For example,
*
* $foobars = [
* ['foo' => 1, 'bar' => 2],
* ['foo' => 1, 'bar' => 3],
* ];
* $cols = ['foo', 'bar'];
* list ($vals, $params) = $this->vals($foobars, $cols);
* $q = "INSERT INTO foobars (" . $this->cs($cols) . ") VALUES " . $vals .
* " RETURNING id";
* $this->fetchAll($q, $params);
*
* Constant values can be set by using a key => value syntax for $cols.
* That is, the following gives the same result as the above example:
*
* $foobars = [
* ['bar' => 2],
* ['bar' => 3],
* ];
* $cols = ['foo' => 1, 'bar'];
* list ($vals, $params) = $this->vals($foobars, $cols);
* $q = "INSERT INTO foobars (" . $this->cs($cols) . ") VALUES " . $vals .
* " RETURNING id";
* $this->fetchAll($q, $params);
*/
public function vals (array $rows, array $cols): array {
$vals = [];
$params = [];
foreach ($rows as $row) {
$vs = [];
foreach ($cols as $k => $c) {
if (is_numeric($k)) {
$vs[] = '?';
$params[] = isset($row[$c]) ? $row[$c] : null;
} else {
$vs[] = $c;
}
}
$vals[] = '(' . implode(', ', $vs) . ')';
}
return [implode(",\n", $vals), $params];
}
我们现在需要的就这些。您可以根据遇到的重复模式添加更多工具以及风格示例。
const ERR_QUERY = 1969001;
const ERR_IDX_MISSING_COL = 1969002;
const ERR_IDX_DUPLICATE_COL = 1969003;
const ERR_TOO_MANY_ROWS = 1969004;
const ERR_NO_ROWS = 1969005;
}
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */