rawsrc / pdoplusplus
PHP 的一个全面对象 PDO 包装器,具有革命性的流畅 SQL 语法
Requires
- php: >=8.0
- ext-pdo: *
README
2022-11-04
PHP 8.0+
v.5.0.1
一个类中的 PHP 完全对象 PDO 包装器
PDOPlusPlus
(别名 PPP
)是 PHP 的单一类 PDO 包装器,具有革命性的流畅 SQL 语法。您不再需要以传统方式使用 PDO,可以完全省略 prepare()
、bindValue()
、bindParam()
的概念。这些机制的用法现在被 PDOPlusPlus
隐藏。您所要做的就是直接编写干净的 SQL 查询,并直接注入您的值。
引擎将自动转义值,让您只需关注 SQL 语法。
PDOPlusPlus
完全符合
- INSERT
- UPDATE
- DELETE
- SELECT
- 存储过程
- 事务(即使嵌套的)
- 本地 SQL 大整数(或 INT8)有符号/无符号支持
对于存储过程,您可以使用任何 IN
、OUT
或 INOUT
参数。
PDOPlusPlus
也完全兼容那些一次返回多个数据集的情况。
请注意:PDOPlusPlus
不验证任何值
PDO 的瑞士军刀。
安装
composer require rawsrc/pdoplusplus
概念
PDOPlusPlus
的强大功能直接与其作为函数使用 PHP 魔术函数 __invoke()
调用的方式相关
您只需选择正确的 注入器,它将以安全的方式处理要注入 SQL 的值。
为了涵盖所有用例,有 6 种不同的注入器
getInjectorIn()
:注入的值直接转义(纯 SQL)。这是默认注入器getInjectorInByVal()
:注入的值使用PDOStatement->bindValue()
机制转义getInjectorInByRef()
:注入的值使用PDOStatement->bindParam()
机制转义getInjectorInAsRef()
:值通过引用传递并直接转义(纯 SQL)getInjectorOut()
:仅用于具有仅 OUT 参数的存储过程getInjectorInOut()
:用于具有 INOUT 参数的存储过程,IN 参数直接转义(纯 SQL)
请注意,默认情况下,PDOPlusPlus
将在纯 SQL 中转义您的值。如果您想有另一种行为,例如使用 PDOStatement
或调用存储过程,则必须使用特定的注入器。
从版本 4.0 的变更日志
此 5.0.x 版本是一个重大更新,可能与基于 4.x 版本的代码略有兼容性问题
新功能
- 完全支持
BIGINT/INT8
数据类型(SIGNED/UNSIGNED
) - 新注入器:
getInjectorInAsRef()
:值通过引用传递并直接转义(纯 SQL) - 删除一些
float
数据类型的别名:double
、num
、numeric
,仅保留float
可用
已删除
- 在创建注入器时定义最终数据类型
AbstractInjector
类,因为其代码如此简单,以至于可以直接在每个注入器中实现。因此现在PDOPlusPlus
是一个真正独立的类,没有其他依赖项
测试代码现在可用。所有测试都是为我的另一个项目编写的:Exacodis,PHP 的最小化测试引擎
自动重置功能
以前,您需要为要执行的每个语句创建一个新的 PDOPlusPlus
实例。有了自动重置功能(默认启用),您可以多次重用相同的 PDOPlusPlus
实例。
自动重置在以下两种情况下会自动禁用
- 如果语句执行失败
- 如果存在任何引用变量
在这些情况下,实例会保留定义的数据和参数。
您必须使用: $ppp->reset();
强制重置实例
除了事务中的保存点(使用 $ppp->releaseAll();
重置)之外,所有内容都会被清理
您可以使用以下方式激活/禁用此功能
$ppp->setAutoResetOn()
$ppp->setAutoResetOff()
关于注入器
允许的不同数据类型有:int str float bool binary bigint
每个注入器都可以使用自己的参数调用。
getInjectorIn(mixed $value, string $type = 'str')
getInjectorInAsRef(mixed &$value, string $type = 'str')
getInjectorInByVal(mixed $value, string $type = 'str')
getInjectorInByRef(mixed &$value, string $type = 'str')
getInjectorOut(string $out_tag)
getInjectorInOut(mixed $value, string $inout_tag, string $type = 'str')
请注意,二进制和bigint数据类型与其他数据类型类似。只是在内部引擎中,处理过程不同。
请参阅以下如何在SQL上下文中使用它们的示例。
数据库连接
如前所述,PDOPlusPlus
作为 PDO 包装器,因此当然需要使用 PDO 连接到您的数据库。您可以声明必要的连接配置文件。每个连接都有一个唯一的ID。
// first profile: power user PDOPlusPlus::addCnxParams( cnx_id: 'user_root', params: [ 'scheme' => 'mysql', 'host' => 'localhost', 'database' => '', 'user' => 'root', 'pwd' => '**********', 'port' => '3306', 'timeout' => '5', 'pdo_params' => [], 'dsn_params' => [] ], is_default: true ); // second profile: basic user PDOPlusPlus::addCnxParams( cnx_id: 'user_test', params: [ 'scheme' => 'mysql', 'host' => 'localhost', 'database' => 'db_pdo_plus_plus', 'user' => 'user_test', 'pwd' => '**********', 'port' => '3306', 'timeout' => '5', 'pdo_params' => [], 'dsn_params' => [] ], is_default: false );
您可以在初始化新实例时定义要在服务器上执行的SQL的连接: $ppp = new PDOPlusPlus('user_root');
或 $ppp = new PDOPlusPlus('user_test');
,
如果省略了ID,则默认将使用连接。一旦定义了默认连接的ID,也可以更改它,请参阅: $ppp->setDefaultConnection();
让我们玩一个小游戏
为了课程,我将使用一个非常简单的数据库,只有一个表
DROP DATABASE IF EXISTS db_pdo_plus_plus; CREATE DATABASE db_pdo_plus_plus; USE db_pdo_plus_plus; CREATE TABLE t_video ( video_id int auto_increment primary key, video_title varchar(255) not null, video_support varchar(30) not null comment 'DVD DIVX BLU-RAY', video_multilingual tinyint(1) default 0 not null, video_chapter int null, video_year int not null, video_summary text null, video_stock int default 0 not null, video_img mediumblob null, video_bigint_unsigned bigint unsigned null, video_bigint bigint null, constraint t_video_video_titre_index unique (video_title) );
示例数据集
$data = [[ 'title' => "The Lord of the Rings - The Fellowship of the Ring", 'support' => 'BLU-RAY', 'multilingual' => true, 'chapter' => 1, 'year' => 2001, 'summary' => null, 'stock' => 10, 'bigint_unsigned' => '18446744073709551600', 'bigint_signed' => -9223372036854775000, ], [ 'title' => "The Lord of the Rings - The two towers", 'support' => 'BLU-RAY', 'multilingual' => true, 'chapter' => 2, 'year' => 2002, 'summary' => null, 'stock' => 0, 'bigint_unsigned' => '18446744073709551600', 'bigint_signed' => -9223372036854775000, ], [ 'title' => "The Lord of the Rings - The return of the King", 'support' => 'DVD', 'multilingual' => true, 'chapter' => 3, 'year' => 2003, 'summary' => null, 'stock' => 1, 'bigint_unsigned' => '18446744073709551600', 'bigint_signed' => -9223372036854775000, ]];
添加记录
让我们使用 PDOPlusPlus
将第一部电影添加到数据库中
我将使用SQL直接模式,省略了 PDOStatement
步骤。
include 'PDOPlusPlus.php'; $ppp = new PDOPlusPlus(); // here the default connection wil be used and the auto-reset is enabled $film = $data[0]; $sql = <<<sql INSERT INTO t_video ( video_title, video_support, video_multilingual, video_chapter, video_year, video_summary, video_stock, video_bigint_unsigned, video_bigint_signed ) VALUES ( {$ppp($film['title'])}, {$ppp($film['support'])}, {$ppp($film['multilingual'], 'bool')}, {$ppp($film['chapter'], 'int')}, {$ppp($film['year'], 'int')}, {$ppp($film['summary'])}, {$ppp($film['stock'], 'int')}, {$ppp($film['bigint_unsigned'], 'bigint')}, {$ppp($film['bigint_signed'], 'bigint')} ) sql; $new_id = $ppp->insert($sql); // $new_id = '1'
让我们使用 PDOPlusPlus
将第二部电影添加到数据库中
我将使用基于值的 PDOStatement
(使用 ->bindValue()
)。
$in = $ppp->getInjectorInByVal(); $film = $data[1]; $sql = <<<sql INSERT INTO t_video ( video_title, video_support, video_multilingual, video_chapter, video_year, video_summary, video_stock, video_bigint_unsigned, video_bigint_signed ) VALUES ( {$in($film['title'])}, {$in($film['support'])}, {$in($film['multilingual'], 'bool')}, {$in($film['chapter'], 'int')}, {$in($film['year'], 'int')}, {$in($film['summary'])}, {$in($film['stock'], 'int')}, {$in($film['bigint_unsigned'], 'bigint')}, {$in($film['bigint_signed'], 'bigint')} ) sql; $new_id = $ppp->insert($sql); // $new_id = '2'
让我们截断表,然后一次性添加整部电影列表。
这次,我将使用基于引用的 PDOStatement
(使用 ->bindParam()
),因为有多次迭代要做。我将使用由 ->injectorInByRef();
返回的注入器。
$ppp->execute('TRUNCATE TABLE t_video'); $in = $ppp->getInjectorInByRef(); $sql = <<<sql INSERT INTO t_video ( video_title, video_support, video_multilingual, video_chapter, video_year, video_summary, video_stock, video_bigint_unsigned, video_bigint_signed ) VALUES ( {$in($title)}, {$in($support)}, {$in($multilingual, 'bool')}, {$in($chapter, 'int')}, {$in($year, 'int')}, {$in($summary)}, {$in($stock, 'int')}, {$in($bigint_unsigned, 'bigint')}, {$in($bigint_signed, 'bigint')} ) sql; foreach ($data as $film) { extract($film); // destructuring the array into components used to populate the references declared just above $ppp->insert($sql); }
请注意,前面的语句有“引用”变量,并且在那种情况下自动重置被禁用。
更新记录
因此,为了能够重用相同的 PDOPlusPlus
实例,我们首先必须清理它。
// we clean the instance $ppp->reset(); $id = 1; $support = 'DVD'; $sql = "UPDATE t_video SET video_support = {$ppp($support)} WHERE video_id = {$ppp($id, 'int')}"; $nb = $ppp->update($sql); // nb of affected rows
删除记录
$id = 1; $sql = "DELETE FROM t_video WHERE video_id = {$ppp($id, 'int')}"; $nb = $ppp->delete($sql); // nb of affected rows
选择记录
$id = 1; $sql = "SELECT * FROM t_video WHERE video_id = {$ppp($id, 'int')}"; $data = $ppp->select($sql);
$sql = "SELECT * FROM t_video WHERE video_support LIKE {$ppp('%RAY%')}"; $data = $ppp->select($sql);
如果您需要一个更强大的方法从查询中提取数据,则有一个特定的方法 selectStmt()
,它为您提供了访问由引擎生成的 PDOStatement
的权限。
$sql = "SELECT * FROM t_video WHERE video_support LIKE {$ppp('%RAY%')}"; $stmt = $ppp->selectStmt($sql); $data = $stmt->fetchAll(PDO::FETCH_OBJ);
您还可以有一个可滚动的游标(您也可以访问由引擎创建的 PDOStatement
)
$sql = "SELECT * FROM t_video WHERE video_support LIKE {$ppp('%RAY%')}"; $stmt = $ppp->selectStmtAsScrollableCursor($sql); while ($row = $stmt->fetch(PDO::FETCH_NUM, PDO::FETCH_ORI_NEXT)) { // ... // }
绑定列
从v.4.0.0版本开始,您可以使用类似于 PDOStatement->bindColumn(...)
的方式定义绑定列。这对于特别处理二进制数据时非常有用。
此功能仅适用于 $ppp->selectStmt()
和 $ppp->selectStmtAsScrollableCursor()
。
// First, you have to prepare the bound variables. $columns = [ 'video_title' => [&$video_title, 'str'], // watch carefully the & before the var 'video_img' => [&$video_img, 'binary'], // watch carefully the & before the var ]; // you have to declare into the instance the bound columns $ppp->setBoundColumns($columns); // then call the selectStmt() $ppp->selectStmt("SELECT video_title, video_img FROM t_video WHERE video_id = {$ppp(1, 'int')}"); // then read the result while ($row = $stmt->fetch(PDO::FETCH_BOUND)) { // here $video_title and $video_img are available and well defined }
BIGINT 或 INT8 列
从v.5.0.0版本开始,引擎完全符合SQL的 BIGINT
或 INT8
(有符号或无符号)数据类型。在内部,引擎始终会将一个真正的bigint发送到SQL引擎,即使您必须在PHP世界中将其作为字符串进行操作。这也适用于
使用PDO绑定机制的注入器。引擎为这些特定用例实现了绕过方案,因此对于只需要声明类型 bigint
的开发人员来说,它是透明的。
由于整数核心限制(PHP_INT_MIN
和 PHP_INT_MAX
),您不能定义像 $int = 18446744073709551600;
这样的变量,PHP核心会自动将该值转换为浮点数 $int = 1.844674407371E+19
。在 PDOPlusPlus
之前,除非将它们视为字符串,否则在PHP环境中很难轻松使用,而在SQL世界中则可以。
记住,当您从数据库中选择一个无符号bigint列时,如果值严格大于 PHP_INT_MAX
,则将检索一个字符串,否则为一个真正的整数。通常,对于有符号bigint列,SQL限制与PHP核心限制相匹配,因为通常两者都在运行x64架构。
存储过程
由于可以一次性提取多个数据集或/和传递多个参数 IN
、OUT
或 INOUT
,大多数情况下您将不得不使用以下所示的特殊值注入器。
一个数据集
让我们创建一个仅返回简单数据集的存储过程。
$ppp = new PPP(); $exec = $ppp->execute(<<<'sql' CREATE OR REPLACE DEFINER = root@localhost PROCEDURE db_pdo_plus_plus.sp_list_films() BEGIN SELECT * FROM t_video; END; sql );
现在,调用它
$rows = $ppp->call('CALL sp_list_films()', true); // the true tells PPP that SP is a query // $rows is a multidimensional array: // $rows[0] => for the first dataset which is an array of all films
一次性返回两个数据集
让我们创建一个存储过程,它一次性返回双倍数据集。
// TWO ROWSET $exec = $ppp->execute(<<<'sql' CREATE OR REPLACE DEFINER = root@localhost PROCEDURE db_pdo_plus_plus.sp_list_films_group_by_support() BEGIN SELECT * FROM t_video WHERE video_support = 'BLU-RAY'; SELECT * FROM t_video WHERE video_support = 'DVD'; END; sql );
现在,调用它
$rows = $ppp->call('CALL sp_list_films_group_by_support()', true); // the true tells PPP that SP is a query // $rows is a multidimensional array: // $rows[0] => for the first dataset which is an array of films (BLU-RAY) // $rows[1] => for the second dataset which is an array of films (DVD)
一个IN参数
让我们创建一个具有一个IN参数的存储过程。
// WITH ONE IN PARAM $exec = $ppp->execute(<<<'sql' CREATE OR REPLACE DEFINER = root@localhost PROCEDURE db_pdo_plus_plus.sp_list_films_one_in_param( p_support VARCHAR(30) ) BEGIN SELECT * FROM t_video WHERE video_support = p_support; END; sql ); // AND CALL IT // FIRST METHOD : plain sql $rows = $ppp->call("CALL sp_list_films_one_in_param({$ppp('DVD')})", true); // $rows is a multidimensional array: // $rows[0] => for the first dataset which is an array of films (DVD) // EXACTLY THE SAME USING ->bindValue() $in = $ppp->getInjectorInByVal(); $rows = $ppp->call("CALL sp_list_films_one_in_param({$in('DVD')})", true); // AND IF YOU WANT TO USE A REFERENCE INSTEAD $in = $ppp->getInjectorInByRef(); $sup = 'DVD'; $rows = $ppp->call("CALL sp_list_films_one_in_param({$in($sup)})", true); $ppp->reset(); // do not forget to reset the instance to be able to reuse it
将变量直接链入SQL中,与您要传递到存储过程的IN参数数量一样多。
一个OUT参数
让我们创建一个具有 OUT
参数的存储过程。
// WITH ONE OUT PARAM $exec = $ppp->execute(<<<'sql' CREATE OR REPLACE DEFINER = root@localhost PROCEDURE db_pdo_plus_plus.sp_nb_films_one_out_param( OUT p_nb INT ) BEGIN SELECT COUNT(video_id) INTO p_nb FROM t_video; END; sql );
并使用特定的 OUT
参数注入器来调用它
$out = $ppp->getInjectorOut(); $exec = $ppp->call("CALL sp_nb_films_one_out_param({$out('@nb')})", false); $nb = $exec['out']['@nb'];
请注意,所有 OUT
值都始终存储在结果数组中,键为 out
一个数据集和两个OUT参数
也可以混合数据集和 OUT
参数。
// WITH ROWSET AND TWO OUT PARAM $exec = $ppp->execute(<<<'sql' CREATE OR REPLACE DEFINER = root@localhost PROCEDURE db_pdo_plus_plus.sp_nb_films_rowset_two_out_param( OUT p_nb_blu_ray INT, OUT p_nb_dvd INT ) BEGIN SELECT * FROM t_video ORDER BY video_year DESC; SELECT COUNT(video_id) INTO p_nb_blu_ray FROM t_video WHERE video_support = 'BLU-RAY'; SELECT COUNT(video_id) INTO p_nb_dvd FROM t_video WHERE video_support = 'DVD'; END; sql ); $out = $ppp->getInjectorOut(); $exec = $ppp->call("CALL sp_nb_films_rowset_two_out_param({$out('@nb_blu_ray')}, {$out('@nb_dvd')})", true); $rows = $exec[0]; // $exec[0] => for the first dataset which is an array of all films ordered by year DESC $nb_br = $exec['out']['@nb_blu_ray']; // note the key 'out' $nb_dv = $exec['out']['@nb_dvd'];
一个INOUT参数和两个OUT参数
最后,让我们创建一个使用 INOUT
和 OUT
参数混合的存储过程。
// WITH ONE INOUT PARAM AND TWO OUT PARAM $exec = $ppp->execute(<<<'sql' CREATE OR REPLACE DEFINER = root@localhost PROCEDURE db_pdo_plus_plus.sp_nb_films_one_inout_two_out_param( INOUT p_qty INT, OUT p_nb_blu_ray INT, OUT p_nb_dvd INT ) BEGIN DECLARE v_nb INT; SELECT SUM(video_stock) INTO v_nb FROM t_video; SET p_qty = v_nb - p_qty; SELECT COUNT(video_id) INTO p_nb_blu_ray FROM t_video WHERE video_support = 'BLU-RAY'; SELECT COUNT(video_id) INTO p_nb_dvd FROM t_video WHERE video_support = 'DVD'; END; sql );
并使用特定的注入器来调用它:一个用于 INOUT
参数,另一个用于 OUT
参数。
请注意,INOUT
注入器的语法。
$io = $ppp->getInjectorInOut(); // io => input/output $out = $ppp->getInjectorOut(); $exec = $ppp->call("CALL sp_nb_films_one_inout_two_out_param({$io('25', '@stock', 'int')}, {$out('@nb_blu_ray')}, {$out('@nb_dvd')})", false); $stock = $exec['out']['@stock']; $nb_br = $exec['out']['@nb_blu_ray']; $nb_dv = $exec['out']['@nb_dvd'];
事务
PDO++ 完全兼容 RDBS 事务机制。
您有几种方法可以帮助您管理您的SQL代码流程。
setTransaction()
用于定义即将到来的事务的执行上下文startTransaction()
commit()
rollback()
将仅回滚到最后一个保存点rollbackTo()
将仅回滚到给定的保存点rollbackAll()
将仅回滚到开始位置savePoint()
用于创建一个新的保存点(SQL代码流中的一个标记)release()
用于删除保存点releaseAll()
用于删除所有保存点
如果您熟悉SQL事务理论,这些函数的命名都很清晰,易于理解。
请注意,当您开始一个事务时,引擎将禁用数据库 AUTOCOMMIT
参数,这样,所有SQL语句都将一次在 $ppp->commit();
上保存。
错误
为了避免大量的 try { } catch { }
块,我引入了一种机制来简化这部分代码。
由于 PDOPlusPlus
在语句失败时可以抛出 Exception
,您应该始终拦截可能的问题,并在代码的任何地方使用 try { } catch { }
块。这不是很重吗?
现在,您可以定义一个闭包来封装异常处理。最初,您只需定义一次唯一的闭包,该闭包将接收并处理 PDOPlusPlus
抛出的 Exception
// Exception wrapper for PDO PDOPlusPlus::setExceptionWrapper(function(Exception $e, PDOPlusPlus $ppp, string $sql, string $func_name, ...$args) { // here you code whatever you want // ... // then you must return a result return 'DB Error, unable to execute the query'; });
然后您可以使用以下方法激活/禁用此功能:
$ppp->setThrowOn();
$ppp->setThrowOff();
如果出现问题时并且抛出功能被禁用,PDOPlusPlus
将会像往常一样拦截 Exception
并将其传递给您的闭包。在这种情况下,该方法将返回 null
。
假设这段代码产生了错误
try { $ppp = new PDOPlusPlus(); $sql = "INSERT INTO t_table (field_a, field_b) VALUES ({$ppp('value_a')}, {$ppp('value_b')})"; $id = $ppp->insert($sql); } catch (Exception $e) { // bla bla }
使用异常包装器机制,您可以简单地做
$ppp = new PDOPlusPlus(); $ppp->setThrowOff(); $sql = "INSERT INTO t_table (field_a, field_b) VALUES ({$ppp('value_a')}, {$ppp('value_b')})"; $id = $ppp->insert($sql); if ($id === null) { $error = $ppp->getErrorFromWrapper(); // $error = 'DB Error, unable to execute the query' }
结论
希望这能帮助您更舒适地生成更好的 SQL 代码,并在 PHP 代码中原生使用 PDO。
好了,伙计们,这就是全部了。享受吧!
原始源代码