paragonie/easydb

易于使用的数据库抽象层

v3.0.2 2022-06-08 05:23 UTC

README

Build Status Latest Stable Version Latest Unstable Version License Downloads

PDO 缺乏简洁性;EasyDB 使分离数据和指令变得容易(且美观)。

EasyDB 是由 Paragon Initiative Enterprises 创建的,作为我们鼓励更好的 应用安全 实践的一部分。

也请查看我们的其他 开源项目

如果你在寻找一个功能齐全的查询构建器,请查看 LatitudeAura.SqlQuery,它们可以与 EasyDB 一起使用。

如果你想使用 EasyDB 但要在内存中缓存预编译语句以进行多次查询(即减少数据库往返),请查看我们的 EasyDB-Cache 包装类。

安装 EasyDB

首先,如果你还没有使用,请 安装 Composer

然后,运行以下命令

/path/to/your/local/composer.phar require paragonie/easydb:^3

如果你已将 Composer 安装在 /usr/bin,你可以将 /path/to/your/local/composer.phar 替换为 composer

为什么使用 EasyDB?因为它更简洁!

让我们重构一个危险的 PHP 片段,该片段之前使用字符串连接来传递用户输入而不是预编译语句。例如,想象一下将 {$_GET['blogpostid']} 直接放入 mysql_query() 语句中间的情况。让我们使其变得安全。

PDO 方法

$db = new \PDO(
    'mysql:host=localhost;dbname=something',
    'username',
    'putastrongpasswordhere'
);

$statement = $db->prepare('SELECT * FROM comments WHERE blogpostid = ? ORDER BY created ASC');
$exec = $statement->execute([$_GET['blogpostid']]);
$rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
foreach ($rows as $row) {
    $template_engine->render('comment', $row);
}

对于如此简单的任务来说,这有点罗嗦。如果我们这样做的地方很多,我们就重复很多次。

EasyDB 解决方案

$db = \ParagonIE\EasyDB\Factory::fromArray([
    'mysql:host=localhost;dbname=something',
    'username',
    'putastrongpasswordhere'
]);

$rows = $db->run('SELECT * FROM comments WHERE blogpostid = ? ORDER BY created ASC', $_GET['blogpostid']);
foreach ($rows as $row) {
    $template_engine->render('comment', $row);
}

我们将其简化为一行。

EasyDB 还能快速做什么?

将一行插入到数据库表中

$db->insert('comments', [
    'blogpostid' => $_POST['blogpost'],
    'userid' => $_SESSION['user'],
    'comment' => $_POST['body'],
    'parent' => isset($_POST['replyTo']) ? $_POST['replyTo'] : null
]);

这相当于以下 SQL 查询(假设 $_POST['blogpostid'] 等于 123$_SESSION['user'] 等于 234$_POST['body'] 等于 test,以及 $_POST['replyTo'] 等于 3456

INSERT INTO comments (blogpostid, userid, comment, parent) VALUES (
    123,
    234,
    'test',
    3456
);

构建不执行的插入

$sql = $db->buildInsertQuery('comments', [
    'blogpostid',
    'userid',
    'comment'
]);

// INSERT INTO comments (blogpostid, userid, comment) VALUES (?, ?, ?)

$result = $db->q(
    $sql,
    $values,
    \PDO::FETCH_BOTH,
    true
);

从数据库表中更新一行

$db->update('comments', [
    'column' => 'foo',
    'otherColumn' => 123456,
    'approved' => true
], [
    'commentid' => $_POST['comment']
]);

这相当于以下 SQL 查询(假设 $_POST['comment'] 等于 789

UPDATE comments
SET 
  column = 'foo',
  otherColumn = 123456,
  approved = TRUE
WHERE commentid = 789

从数据库表中删除一行

// Delete all of this user's comments
$db->delete('comments', [
    'userid' => 3
]);

这相当于以下 SQL 查询

DELETE FROM comments WHERE userid = 3

从表中获取单行

$userData = $db->row(
    "SELECT * FROM users WHERE userid = ?",
    $_GET['userid']
);

注意:这期望一个可变数量的参数列表,而不是数组。如果你有多个参数,请像这样堆叠它们

$userData = $db->row(
    "SELECT * FROM users WHERE userid = ? AND other = ?",
    $_GET['userid'],
    $_GET['other']
);

这是 错误的

$userData = $db->row(
    "SELECT * FROM users WHERE userid = ? AND other = ?",
    array($userid, $other) // WRONG, should not be in an array
);

从表中获取单行的单列

$exists = $db->cell(
    "SELECT count(id) FROM users WHERE email = ?",
    $_POST['email']
);

/* OR YOU CAN CALL IT THIS WAY: */
$exists = $db->single(
    "SELECT count(id) FROM users WHERE email = ?",
    array(
        $_POST['email']
    )
);

注意:cell() 期望一个可变数量的参数列表,而不是数组。如果你有多个参数,请像这样堆叠它们

$exists = $db->cell(
    "SELECT count(id) FROM users WHERE email = ? AND username = ?",
    $_POST['email'],
    $_POST['usenrame']
);

这是 错误的

$exists = $db->cell(
    "SELECT count(id) FROM users WHERE email = ? AND username = ?",
    array($email, $username) // WRONG, should not be in an array
);

或者,如果你确实想传递一个数组,你可以使用 single() 代替 cell()

尝试执行事务

$save = function (EasyDB $db) use ($userData, $query) : int {
    $db->safeQuery($query, [$userData['userId']]);
    return \Some\Other\Package::CleanUpTable($db);
};
// auto starts, commits and rolls back a transaction as necessary
$returnedInt = $db->tryFlatTransaction($save);

生成动态查询条件

$statement = EasyStatement::open()
    ->with('last_login IS NOT NULL');

if (strpos($_POST['search'], '@') !== false) {
    // Perform a username search
    $statement->orWith('username LIKE ?', '%' . $db->escapeLikeValue($_POST['search']) . '%');
} else {
    // Perform an email search
    $statement->orWith('email = ?', $_POST['search']);
}

// The statement can compile itself to a string with placeholders:
echo $statement; /* last_login IS NOT NULL OR username LIKE ? */

// All the values passed to the statement are captured and can be used for querying:
$user = $db->single("SELECT * FROM users WHERE $statement", $statement->values());

注意:带有条件的传递值完全是可选的,但推荐。

"IN" 参数的可变数量

// Statements also handle translation for IN conditions with variable arguments,
// using a special ?* placeholder:
$roles = [1];
if ($_GET['with_managers']) {
    $roles[] = 2;
}

$statement = EasyStatement::open()->in('role IN (?*)', $roles);

// The ?* placeholder is replaced by the correct number of ? placeholders:
echo $statement; /* role IN (?, ?) */

// And the values will be unpacked accordingly:
print_r($statement->values()); /* [1, 2] */

条件分组

// Statements can also be grouped when necessary:
$statement = EasyStatement::open()
    ->group()
        ->with('subtotal > ?')
        ->andWith('taxes > ?')
    ->end()
    ->orGroup()
        ->with('cost > ?')
        ->andWith('cancelled = 1')
    ->end();

echo $statement; /* (subtotal > ? AND taxes > ?) OR (cost > ? AND cancelled = 1) */

使用自定义占位符进行插入和更新

从版本 2.12.0 开始,EasyDB 支持在插入或更新数据时调用存储过程和 SQL 函数的占位符。

EasyPlaceholder 类的构建方式与其他 EasyDB 方法相同:第一个参数,即“掩码”,必须是一个字符串。掩码中可以包含 ? 占位符,而后续的参数将在查询执行时填充这些 ? 占位符。

$db->insert('user_auth', [
    'user_id' => 1,
    'timestamp' => new EasyPlaceholder('NOW()'),
    'expired' => new EasyPlaceholder('TIMESTAMPADD(HOUR, 2, NOW())'),
    'location' => new EasyPlaceholder(
        "ST_GeomFromText(CONCAT('POINT(', ?, ' ', ?, ')'))",
        50.4019514,
        30.3926105
    )
]);

$db->update(
    'user_auth', 
    [
        'last_update' => new EasyPlaceholder('NOW()'),
    ], 
    [
        'user_id' => 1
    ]
);

安全警告:不要将用户输入连接到第一个参数。

EasyPlaceholder 可用于 insert()insertIgnore()insertOnDuplicateKeyUpdate()update()

如果我需要针对特定内容使用 PDO 呢?

$pdo = $db->getPdo();

我可以为现有的 PDO 实例创建 EasyDB 包装器吗?

可以! 它就像这样做

$easy = new \ParagonIE\EasyDB\EasyDB($pdo, 'mysql');

我该如何运行测试?

vendor/bin/phpunit

使用 Psalm 的安全分析功能与 EasyDB

首先,确保你已经 阅读了 Psalm 文档

EasyDB 的 API 暴露了几个污染点。接下来,在你的使用 EasyDB 的代码库上运行以下命令,以识别 SQL 注入风险的来源。

vendor/bin/psalm --taint-analysis

这将暴露你以潜在不安全的方式向 EasyDB 传递污染数据的位置。

常见问题故障排除

仅允许一维数组

当尝试将参数数组传递给 run() 时,这种情况经常出现。

EasyDB::run() 期望一个查询字符串,然后是任意数量的可选参数。它 期望所有参数的数组。

如果你想要使用类似 $obj->method($string, $array) 的 API,请使用 safeQuery() 而不是 run()

<?php
/**
 * @var EasyDB $db
 * @var string $query
 * @var array $params 
 */
- $rows = $db->run($query, $params);
+ $rows = $db->safeQuery($query, $params);

或者,你可以使用 展开运算符 展平你的数组

<?php
/**
 * @var EasyDB $db
 * @var string $query
 * @var array $params 
 */
- $rows = $db->run($query, $params);
+ $rows = $db->run($query, ...$params);

EasyDB 的 run() 方法是 safeQuery() 的可变参数包装器,所以任何一种解决方案都是正确的。

支持合同

如果你的公司在产品或服务中使用这个库,你可能对从 Paragon Initiative Enterprises 购买 支持合同 感兴趣。