rawsrc/pdoplusplus

PHP 的一个全面对象 PDO 包装器,具有革命性的流畅 SQL 语法

5.0.2 2023-01-16 14:02 UTC

This package is auto-updated.

Last update: 2024-09-27 09:21:33 UTC


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)有符号/无符号支持

对于存储过程,您可以使用任何 INOUTINOUT 参数。
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 数据类型的别名:doublenumnumeric,仅保留 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的 BIGINTINT8(有符号或无符号)数据类型。在内部,引擎始终会将一个真正的bigint发送到SQL引擎,即使您必须在PHP世界中将其作为字符串进行操作。这也适用于
使用PDO绑定机制的注入器。引擎为这些特定用例实现了绕过方案,因此对于只需要声明类型 bigint 的开发人员来说,它是透明的。

由于整数核心限制(PHP_INT_MINPHP_INT_MAX),您不能定义像 $int = 18446744073709551600; 这样的变量,PHP核心会自动将该值转换为浮点数 $int = 1.844674407371E+19。在 PDOPlusPlus 之前,除非将它们视为字符串,否则在PHP环境中很难轻松使用,而在SQL世界中则可以。

记住,当您从数据库中选择一个无符号bigint列时,如果值严格大于 PHP_INT_MAX,则将检索一个字符串,否则为一个真正的整数。通常,对于有符号bigint列,SQL限制与PHP核心限制相匹配,因为通常两者都在运行x64架构。

存储过程

由于可以一次性提取多个数据集或/和传递多个参数 INOUTINOUT,大多数情况下您将不得不使用以下所示的特殊值注入器。

一个数据集

让我们创建一个仅返回简单数据集的存储过程。

$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参数

最后,让我们创建一个使用 INOUTOUT 参数混合的存储过程。

// 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。

好了,伙计们,这就是全部了。享受吧!

原始源代码