paragonie / easydb
易于使用的数据库抽象层
Requires
- php: ^8
- ext-pdo: *
- paragonie/corner: ^2
Requires (Dev)
- dev-master
- v3.0.2
- v3.0.1
- v3.0.0
- v2.x-dev
- v2.12.0
- v2.11.0
- v2.10.0
- v2.9.0
- v2.8.0
- v2.7.0
- v2.6.2
- v2.6.1
- v2.6.0
- v2.5.1
- v2.5.0
- v2.4.0
- v2.3.1
- v2.3.0
- v2.2.1
- v2.2.0
- v2.1.1
- v2.1.0
- v2.0.1
- v2.0.0
- v1.7.0
- v1.6.2
- v1.6.1
- v1.3.1
- v1.3.0
- v1.2.1
- v1.2.0
- v1.1.1
- v1.1.0
- v1.0.0
- v0.2.4
- v0.2.3
- v0.2.2
- 0.1.0
- dev-closecursor
- dev-v1.x-legacy
- dev-feature/version-3-0
This package is auto-updated.
Last update: 2024-08-29 04:53:15 UTC
README
PDO 缺乏简洁性;EasyDB 使分离数据和指令变得容易(且美观)。
EasyDB 是由 Paragon Initiative Enterprises 创建的,作为我们鼓励更好的 应用安全 实践的一部分。
也请查看我们的其他 开源项目。
如果你在寻找一个功能齐全的查询构建器,请查看 Latitude 和 Aura.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 购买 支持合同 感兴趣。