rawsrc/pdoplusplus 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

rawsrc/pdoplusplus

Composer 安装命令:

composer require rawsrc/pdoplusplus

包简介

A full object PDO Wrapper for PHP with a new revolutionary fluid SQL syntax

README 文档

README

2022-11-04 PHP 8.0+ v.5.0.1

A PHP full object PDO Wrapper in one class

PDOPlusPlus (alias PPP) is a single class PDO Wrapper for PHP with a revolutionary fluid SQL syntax. You do not have anymore to use PDO in classical way, you can completely omit the notions of prepare(), bindValue(), bindParam(). The usage of these mechanisms is now hidden by PDOPlusPlus. All you have to do is to write directly a clean SQL query and inject directly your values.

The engine, will automatically escape the values and will let you concentrate only on the SQL syntax.

PDOPlusPlus is totally compliant with:

  • INSERT
  • UPDATE
  • DELETE
  • SELECT
  • STORED PROCEDURE
  • TRANSACTIONS (EVEN NESTED ONES)
  • NATIVE SQL BIGINT (OR INT8) SIGNED/UNSIGNED SUPPORT

For stored procedures, you'll be able to use any IN, OUT or INOUT params.
PDOPlusPlus is also fully compatible with those returning multiple dataset at once.

BE CAREFUL: PDOPlusPlus DOESN'T VALIDATE ANY VALUE

A true Swiss knife for PDO.

INSTALLATION

composer require rawsrc/pdoplusplus

THE CONCEPT

The power of PDOPlusPlus is directly linked to the way the instance is called as a function using the PHP magic function __invoke()
All you have to choose is the right injector that will take care, in a secure way, of the values to be injected into the SQL.

To cover all use cases, there's 6 different injectors:

  • getInjectorIn(): injected values are directly escaped (plain sql). THIS IS THE DEFAULT INJECTOR
  • getInjectorInByVal(): injected values are escaped using the PDOStatement->bindValue() mechanism
  • getInjectorInByRef(): injected values are escaped using the PDOStatement->bindParam() mechanism
  • getInjectorInAsRef(): values are passed by ref and directly escaped (plain sql)
  • getInjectorOut(): for stored procedure with only OUT param
  • getInjectorInOut(): for stored procedure with INOUT param, IN param is directly escaped (plain sql)

Please note that by default, PDOPlusPlus will escape your values in plain sql. If you want to have another behavior, like using a PDOStatement or calling a stored procedure, then you must use a specific injector.

CHANGELOG FROM VERSION 4.0

This version 5.0.x is a major update and may slightly break the compatibility with the code based on version 4.x

NEW FEATURES:

  • Full support of BIGINT/INT8 data type (SIGNED/UNSIGNED)
  • New injector: getInjectorInAsRef(): values are passed by ref and directly escaped (plain sql)
  • Remove some aliases for float data types: double, num, numeric, only float remain available

REMOVED:

  • Defining the final data type when creating an injector
  • AbstractInjector class as its code was so simple that it was meaningful to directly implement it in each injector. So now PDOPlusPlus is truly a standalone class with no other dependency

The test code is now available. All tests are written for another of my projects: Exacodis, a minimalist testing engine for PHP

AUTO-RESET FEATURE

Previously, you had to create a new instance of PDOPlusPlus for each statement you wanted to execute. With the auto-reset feature (enabled by default) you can reuse the same instance of PDOPlusPlus as many times as necessary.

The auto-reset is automatically disabled just in 2 cases:

  • if the statement fails
  • if there's any by ref variable

In those cases, the instance keeps the data and the parameters that were defined.
You must force the reset of the instance using: $ppp->reset();

Everything is cleaned except save points in transactions which are reset with $ppp->releaseAll();

You can activate/deactivate this feature using:

  • $ppp->setAutoResetOn()
  • $ppp->setAutoResetOff()

ABOUT INJECTORS

The different allowed data types are : int str float bool binary bigint

Every injector is invocable with its own parameters.

  • 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')

Note that binary and bigint data are types like others. Just internally the engine, the process is different.

Please have a look below how to use them in a SQL context.

CONNECTION TO THE DATABASE

As written, PDOPlusPlus is as PDO Wrapper, so it will have to connect to your database using PDO of course. You can declare as many connections profiles as necessary. Each connection has a unique 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
);

You can define the connection for the SQL you have to execute on the server when initializing a new instance $ppp = new PDOPlusPlus('user_root'); or $ppp = new PDOPlusPlus('user_test');,
If the id is omitted then the connection by default will be used. It is also possible to change the default connection's id once defined, see: $ppp->setDefaultConnection()

LET'S PLAY A LITTLE

For the course, I will use a very simple database of one table :

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)
);

SAMPLE DATASET

$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,
]];

ADD A RECORD

Let's add the first movie into the database using PDOPlusPlus:
I will use the SQL DIRECT mode omitting the PDOStatement step.

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'

Let's add the second movie into the database using PDOPlusPlus:
I will use a PDOStatement based on values (->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' 

Let's truncate the table and then add the whole list of films at once.
This time, I will use a PDOStatement based on references (->bindParam()) as there are many iterations to do. I will use the injector returned by ->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); 
}

Please note that the previous statement has "by ref" variables and the auto-reset is disabled in that case.

UPDATE A RECORD

So, to be able to reuse the same instance of PDOPlusPlus, we must clean it first.

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

DELETE A RECORD

$id = 1;
$sql = "DELETE FROM t_video WHERE video_id = {$ppp($id, 'int')}";
$nb = $ppp->delete($sql); // nb of affected rows

SELECT A RECORD

$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);

If you need a more powerful way of extracting data from a query, there's a specific method selectStmt() that gives you access to the PDOStatement generated by the engine.

$sql  = "SELECT * FROM t_video WHERE video_support LIKE {$ppp('%RAY%')}";
$stmt = $ppp->selectStmt($sql);
$data = $stmt->fetchAll(PDO::FETCH_OBJ);

It is also possible to have a scrollable cursor (here you also have access to the PDOStatement created by the engine):

$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)) {
    // ... // 
}

BOUND COLUMNS

Since v.4.0.0, it is possible to define bound columns as you'd do using PDOStatement->bindColumn(...). This is useful when you work especially with binary data.

This feature only works with $ppp->selectStmt() and $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 OR INT8 COLUMN

Since v.5.0.0, the engine is fully compliant with the SQL BIGINT or INT8 (signed or unsigned) data type. Internally, the engine will always send a true bigint to the sql engine even if you have to manipulate them as strings in the PHP world. This is also true for
injectors using the PDO binding mechanism. The engine implements a workaround for these specific use cases, so it's transparent for the developer who has just to declare the type bigint for any injected value.

Because of integer core limits (PHP_INT_MIN and PHP_INT_MAX), you can't define a variable like $int = 18446744073709551600;, the PHP core will automatically cast the value to a float $int = 1.844674407371E+19. Before PDOPlusPlus, unless you consider them as string, it was quite impossible to use them easily in a PHP context whereas it was possible in the SQL world.

Remember, when you select a unsigned bigint column from the database, if the value is strictly greater than PHP_INT_MAX, then you will retrieve a string, otherwise a true integer. Generally, for signed bigint columns, the SQL limits match the PHP Core limits as usually both are running a x64 architecture.

STORED PROCEDURE

Because of having the possibility to extract many datasets at once or/and also passing multiple parameters IN, OUT or INOUT, most of the time you will have to use a specific value injector as shown below.

ONE DATASET

Let's create a SP that just return a simple dataset:

$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
);

And now, call it:

$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 DATASET AT ONCE

Let's create a SP that just return a double dataset at once:

// 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
);

And now, call it:

$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)

ONE IN PARAM

Let's create a SP with one IN Param:

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

Chain directly the variables within the SQL as many as IN params you have to pass to the stored procedure.

ONE OUT PARAM

Let's create a SP with an OUT Param:

// 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
);

And call it using the specific injector for the OUT param:

$out = $ppp->getInjectorOut();
$exec = $ppp->call("CALL sp_nb_films_one_out_param({$out('@nb')})", false);
$nb = $exec['out']['@nb'];

Please note that all OUT values are always stored in the result array with the key out

ONE DATASET AND TWO OUT PARAMS

It is also possible to mix dataset and OUT param:

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

ONE INOUT PARAM WITH TWO OUT PARAMS

Finally, let's create a SP that use a mix between INOUT and OUT params:

// 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
);

And call it using the specific injectors: one for INOUT and another one for OUT params.
Please be careful with the syntax for the INOUT injector.

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

TRANSACTIONS

PDO++ is fully compatible with the RDBS transaction mechanism.
You have several methods that will help you to manage your SQL code flow:

  • setTransaction() to define the execution context of the transaction to come
  • startTransaction()
  • commit()
  • rollback() that will just rollback to the last save point
  • rollbackTo() that will just rollback to the given save point
  • rollbackAll() that will rollback to the beginning
  • savePoint() to create a new save point (a marker inside a flow of SQL code)
  • release() to remove a save point
  • releaseAll() to remove all save points

If you're familiar with the SQL transactions theory, the functions are well named and easy to understand.

Please note, that when you start a transaction, the engine disable the database AUTOCOMMIT parameter, that way, all sql statements will be saved at once on $ppp->commit();.

ERRORS

To avoid plenty of try { } catch { } blocks, I introduced a mechanism that will factorize this part of code.
As PDOPlusPlus can throw an Exception when a statement fails, you should always intercept that possible issue and use everywhere in your code a try { } catch { } block. It's pretty heavy, isn't it ?

Now you can define a closure that will embed the treatment of the exception. At the beginning, you just have to define once a unique closure that will receive and treat the thrown Exception by PDOPlusPlus

// 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';
});

Then you can activate/deactivate this feature using:

  • $ppp->setThrowOn();
  • $ppp->setThrowOff();

In case of problem and if the throwing is deactivated, PDOPlusPlus will intercept as usual the Exception and will pass it to your closure. In taht case, the method will return null.

Suppose this code produces an error:

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
}

using the mechanism of exception wrapper, you can simply do:

$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'
}

CONCLUSION

Hope this will help you to produce in a more comfortable way a better SQL code and use PDO natively in your PHP code.

Ok guys, that's all folks. Enjoy !

rawsrc

rawsrc/pdoplusplus 适用场景与选型建议

rawsrc/pdoplusplus 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 43 次下载、GitHub Stars 达 8, 最近一次更新时间为 2020 年 08 月 24 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「PHP PDO Wrapper PPP SQL fluid syntax」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 rawsrc/pdoplusplus 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 rawsrc/pdoplusplus 我们能提供哪些服务?
定制开发 / 二次开发

基于 rawsrc/pdoplusplus 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 43
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 8
  • 点击次数: 9
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 8
  • Watchers: 3
  • Forks: 2
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2020-08-24