rawsrc / pdo-plus-plus

该包已被废弃,不再维护。作者建议使用rawsrc/pdoplusplus包。

PHP对象的PDO包装器,具有革命性的流体SQL语法

5.0.2 2023-01-16 14:02 UTC

This package is auto-updated.

Last update: 2023-06-27 06:29:35 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 BIGINT(或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')

请注意,binary 和 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); 
}

请注意,前面的语句有 "by ref" 变量,且在这种情况下禁用了自动重置。

更新记录

因此,为了能够重复使用同一个 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(有符号或无符号)数据类型。在内部,即使您必须在 PHP 世界中将它们作为字符串处理,引擎也会始终发送真正的 bigint 到 sql 引擎。这也适用于
使用 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,您通常需要使用以下所示的特殊值注入器。

一个数据集

让我们创建一个仅返回简单数据集的 SP

$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  

一次性返回两个数据集

让我们创建一个一次返回双数据集的 SP

// 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 参数的 SP

// 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 参数的 SP

// 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'];

带有两个输出参数的单输入输出参数

最后,让我们创建一个SP,它使用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。

好的,伙计们,这就是全部内容。享受吧!

原始源代码