Skip to content

Commit 45bad64

Browse files
Mysqli bind in execute (#6271)
1 parent 44a35c9 commit 45bad64

File tree

6 files changed

+253
-10
lines changed

6 files changed

+253
-10
lines changed

UPGRADING

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ PHP 8.1 UPGRADE NOTES
9191
for details of behavior changes and how to explicitly set this attribute. To
9292
keep the old behavior, use mysqli_report(MYSQLI_REPORT_OFF);
9393
RFC: https://wiki.php.net/rfc/mysqli_default_errmode
94+
. Classes extending mysqli_stmt::execute() will be required to specify the
95+
additional parameter now.
96+
RFC: https://wiki.php.net/rfc/mysqli_bind_in_execute
9497

9598
- MySQLnd:
9699
. The mysqlnd.fetch_copy_data ini setting has been removed. However, this
@@ -218,6 +221,9 @@ PHP 8.1 UPGRADE NOTES
218221
used to specify a directory from which files are allowed to be loaded. It
219222
is only meaningful if mysqli.allow_local_infile is not enabled, as all
220223
directories are allowed in that case.
224+
. Binding in execute has been added to mysqli prepared statements.
225+
Parameters can now be passed to mysqli_stmt::execute as an array.
226+
RFC: https://wiki.php.net/rfc/mysqli_bind_in_execute
221227

222228
- PDO MySQL:
223229
. The PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY attribute has been added, which

ext/mysqli/mysqli.stub.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ public function data_seek(int $offset) {}
505505
* @return bool
506506
* @alias mysqli_stmt_execute
507507
*/
508-
public function execute() {}
508+
public function execute(?array $params = null) {}
509509

510510
/**
511511
* @return bool|null
@@ -642,10 +642,10 @@ function mysqli_error(mysqli $mysql): string {}
642642

643643
function mysqli_error_list(mysqli $mysql): array {}
644644

645-
function mysqli_stmt_execute(mysqli_stmt $statement): bool {}
645+
function mysqli_stmt_execute(mysqli_stmt $statement, ?array $params = null): bool {}
646646

647647
/** @alias mysqli_stmt_execute */
648-
function mysqli_execute(mysqli_stmt $statement): bool {}
648+
function mysqli_execute(mysqli_stmt $statement, ?array $params = null): bool {}
649649

650650
function mysqli_fetch_field(mysqli_result $result): object|false {}
651651

ext/mysqli/mysqli_api.c

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -811,15 +811,57 @@ PHP_FUNCTION(mysqli_stmt_execute)
811811
{
812812
MY_STMT *stmt;
813813
zval *mysql_stmt;
814+
HashTable *input_params = NULL;
814815
#ifndef MYSQLI_USE_MYSQLND
815816
unsigned int i;
816817
#endif
817818

818-
if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "O", &mysql_stmt, mysqli_stmt_class_entry) == FAILURE) {
819+
if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "O|h!", &mysql_stmt, mysqli_stmt_class_entry, &input_params) == FAILURE) {
819820
RETURN_THROWS();
820821
}
821822
MYSQLI_FETCH_RESOURCE_STMT(stmt, mysql_stmt, MYSQLI_STATUS_VALID);
822823

824+
// bind-in-execute
825+
if (input_params) {
826+
#if defined(MYSQLI_USE_MYSQLND)
827+
zval *tmp;
828+
unsigned int index;
829+
unsigned int hash_num_elements;
830+
unsigned int param_count;
831+
MYSQLND_PARAM_BIND *params;
832+
833+
if (!zend_array_is_list(input_params)) {
834+
zend_argument_value_error(ERROR_ARG_POS(2), "must be a list array");
835+
RETURN_THROWS();
836+
}
837+
838+
hash_num_elements = zend_hash_num_elements(input_params);
839+
param_count = mysql_stmt_param_count(stmt->stmt);
840+
if (hash_num_elements != param_count) {
841+
zend_argument_value_error(ERROR_ARG_POS(2), "must consist of exactly %d elements, %d present", param_count, hash_num_elements);
842+
RETURN_THROWS();
843+
}
844+
845+
params = mysqlnd_stmt_alloc_param_bind(stmt->stmt);
846+
ZEND_ASSERT(params);
847+
848+
index = 0;
849+
ZEND_HASH_FOREACH_VAL(input_params, tmp) {
850+
ZVAL_COPY_VALUE(&params[index].zv, tmp);
851+
params[index].type = MYSQL_TYPE_VAR_STRING;
852+
index++;
853+
} ZEND_HASH_FOREACH_END();
854+
855+
if (mysqlnd_stmt_bind_param(stmt->stmt, params)) {
856+
MYSQLI_REPORT_STMT_ERROR(stmt->stmt);
857+
RETVAL_FALSE;
858+
}
859+
#else
860+
zend_argument_count_error("Binding parameters in execute is not supported with libmysqlclient");
861+
RETURN_THROWS();
862+
#endif
863+
}
864+
823865
#ifndef MYSQLI_USE_MYSQLND
824866
if (stmt->param.var_cnt) {
825867
int j;

ext/mysqli/mysqli_arginfo.h

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: 1c01e60c65f87e4f59435c3712296137d265dfdc */
2+
* Stub hash: 3f3d19da5a2b7c8edc6dba0fde6215b93d10bb32 */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_mysqli_affected_rows, 0, 1, MAY_BE_LONG|MAY_BE_STRING)
55
ZEND_ARG_OBJ_INFO(0, mysql, mysqli, 0)
@@ -71,6 +71,7 @@ ZEND_END_ARG_INFO()
7171

7272
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_execute, 0, 1, _IS_BOOL, 0)
7373
ZEND_ARG_OBJ_INFO(0, statement, mysqli_stmt, 0)
74+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, params, IS_ARRAY, 1, "null")
7475
ZEND_END_ARG_INFO()
7576

7677
#define arginfo_mysqli_execute arginfo_mysqli_stmt_execute
@@ -300,7 +301,9 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_bind_result, 0, 1, _
300301
ZEND_ARG_VARIADIC_TYPE_INFO(1, vars, IS_MIXED, 0)
301302
ZEND_END_ARG_INFO()
302303

303-
#define arginfo_mysqli_stmt_close arginfo_mysqli_stmt_execute
304+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_close, 0, 1, _IS_BOOL, 0)
305+
ZEND_ARG_OBJ_INFO(0, statement, mysqli_stmt, 0)
306+
ZEND_END_ARG_INFO()
304307

305308
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_data_seek, 0, 2, IS_VOID, 0)
306309
ZEND_ARG_OBJ_INFO(0, statement, mysqli_stmt, 0)
@@ -351,7 +354,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_more_results, 0, 1,
351354
ZEND_END_ARG_INFO()
352355
#endif
353356

354-
#define arginfo_mysqli_stmt_next_result arginfo_mysqli_stmt_execute
357+
#define arginfo_mysqli_stmt_next_result arginfo_mysqli_stmt_close
355358

356359
#define arginfo_mysqli_stmt_num_rows arginfo_mysqli_stmt_affected_rows
357360

@@ -362,7 +365,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_prepare, 0, 2, _IS_B
362365
ZEND_ARG_TYPE_INFO(0, query, IS_STRING, 0)
363366
ZEND_END_ARG_INFO()
364367

365-
#define arginfo_mysqli_stmt_reset arginfo_mysqli_stmt_execute
368+
#define arginfo_mysqli_stmt_reset arginfo_mysqli_stmt_close
366369

367370
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_mysqli_stmt_result_metadata, 0, 1, mysqli_result, MAY_BE_FALSE)
368371
ZEND_ARG_OBJ_INFO(0, statement, mysqli_stmt, 0)
@@ -374,7 +377,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_send_long_data, 0, 3
374377
ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0)
375378
ZEND_END_ARG_INFO()
376379

377-
#define arginfo_mysqli_stmt_store_result arginfo_mysqli_stmt_execute
380+
#define arginfo_mysqli_stmt_store_result arginfo_mysqli_stmt_close
378381

379382
#define arginfo_mysqli_stmt_sqlstate arginfo_mysqli_stmt_error
380383

@@ -640,7 +643,9 @@ ZEND_END_ARG_INFO()
640643

641644
#define arginfo_class_mysqli_stmt_data_seek arginfo_class_mysqli_result_data_seek
642645

643-
#define arginfo_class_mysqli_stmt_execute arginfo_class_mysqli_character_set_name
646+
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_mysqli_stmt_execute, 0, 0, 0)
647+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, params, IS_ARRAY, 1, "null")
648+
ZEND_END_ARG_INFO()
644649

645650
#define arginfo_class_mysqli_stmt_fetch arginfo_class_mysqli_character_set_name
646651

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
--TEST--
2+
mysqli_stmt_execute() - bind in execute
3+
--SKIPIF--
4+
<?php
5+
require_once 'skipif.inc';
6+
require_once 'skipifconnectfailure.inc';
7+
if (!stristr(mysqli_get_client_info(), 'mysqlnd')) {
8+
die("skip: only available in mysqlnd");
9+
}
10+
?>
11+
--FILE--
12+
<?php
13+
require_once "connect.inc";
14+
15+
require 'table.inc';
16+
17+
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
18+
19+
// first, control case
20+
$id = 1;
21+
$abc = 'abc';
22+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
23+
$stmt->bind_param('sss', ...[&$abc, 42, $id]);
24+
$stmt->execute();
25+
assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']);
26+
$stmt = null;
27+
28+
// 1. same as the control case, but skipping the middle-man (bind_param)
29+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
30+
$stmt->execute([&$abc, 42, $id]);
31+
assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']);
32+
$stmt = null;
33+
34+
// 2. param number has to match - missing 1 parameter
35+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
36+
try {
37+
$stmt->execute([&$abc, 42]);
38+
} catch (ValueError $e) {
39+
echo '[001] '.$e->getMessage()."\n";
40+
}
41+
$stmt = null;
42+
43+
// 3. Too many parameters
44+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
45+
try {
46+
$stmt->execute([&$abc, null, $id, 24]);
47+
} catch (ValueError $e) {
48+
echo '[002] '.$e->getMessage()."\n";
49+
}
50+
$stmt = null;
51+
52+
// 4. param number has to match - missing all parameters
53+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
54+
try {
55+
$stmt->execute([]);
56+
} catch (ValueError $e) {
57+
echo '[003] '.$e->getMessage()."\n";
58+
}
59+
$stmt = null;
60+
61+
// 5. param number has to match - missing argument to execute()
62+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
63+
try {
64+
$stmt->execute();
65+
} catch (mysqli_sql_exception $e) {
66+
echo '[004] '.$e->getMessage()."\n";
67+
}
68+
$stmt = null;
69+
70+
// 6. wrong argument to execute()
71+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
72+
try {
73+
$stmt->execute(42);
74+
} catch (TypeError $e) {
75+
echo '[005] '.$e->getMessage()."\n";
76+
}
77+
$stmt = null;
78+
79+
// 7. objects are not arrays and are not accepted
80+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
81+
try {
82+
$stmt->execute((object)[&$abc, 42, $id]);
83+
} catch (TypeError $e) {
84+
echo '[006] '.$e->getMessage()."\n";
85+
}
86+
$stmt = null;
87+
88+
// 8. arrays by reference work too
89+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
90+
$arr = [&$abc, 42, $id];
91+
$arr2 = &$arr;
92+
$stmt->execute($arr2);
93+
assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']);
94+
$stmt = null;
95+
96+
// 9. no placeholders in statement. nothing to bind in an empty array
97+
$stmt = $link->prepare('SELECT label FROM test WHERE id=1');
98+
$stmt->execute([]);
99+
assert($stmt->get_result()->fetch_assoc() === ['label'=>'a']);
100+
$stmt = null;
101+
102+
// 10. once bound the values are persisted. Just like in PDO
103+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
104+
$stmt->execute(['abc', 42, $id]);
105+
assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']);
106+
$stmt->execute(); // no argument here. Values are already bound
107+
assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']);
108+
try {
109+
$stmt->execute([]); // no params here. PDO doesn't throw an error, but mysqli does
110+
} catch (ValueError $e) {
111+
echo '[007] '.$e->getMessage()."\n";
112+
}
113+
$stmt = null;
114+
115+
// 11. mixing binding styles not possible. Also, NULL should stay NULL when bound as string
116+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
117+
$stmt->bind_param('sss', ...['abc', 42, null]);
118+
$stmt->execute([null, null, $id]);
119+
assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>null, 'num' => null]);
120+
$stmt = null;
121+
122+
// 12. Only list arrays are allowed
123+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
124+
try {
125+
$stmt->execute(['A'=>'abc', 2=>42, null=>$id]);
126+
} catch (ValueError $e) {
127+
echo '[008] '.$e->getMessage()."\n";
128+
}
129+
$stmt = null;
130+
131+
132+
mysqli_close($link);
133+
?>
134+
--CLEAN--
135+
<?php
136+
require_once "clean_table.inc";
137+
?>
138+
--EXPECT--
139+
[001] mysqli_stmt::execute(): Argument #1 ($params) must consist of exactly 3 elements, 2 present
140+
[002] mysqli_stmt::execute(): Argument #1 ($params) must consist of exactly 3 elements, 4 present
141+
[003] mysqli_stmt::execute(): Argument #1 ($params) must consist of exactly 3 elements, 0 present
142+
[004] No data supplied for parameters in prepared statement
143+
[005] mysqli_stmt::execute(): Argument #1 ($params) must be of type ?array, int given
144+
[006] mysqli_stmt::execute(): Argument #1 ($params) must be of type ?array, stdClass given
145+
[007] mysqli_stmt::execute(): Argument #1 ($params) must consist of exactly 3 elements, 0 present
146+
[008] mysqli_stmt::execute(): Argument #1 ($params) must be a list array
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
mysqli_stmt_execute() - bind in execute - not supported with libmysql
3+
--SKIPIF--
4+
<?php
5+
require_once 'skipif.inc';
6+
require_once 'skipifconnectfailure.inc';
7+
if (stristr(mysqli_get_client_info(), 'mysqlnd')) {
8+
die("skip: only applicable for libmysqlclient");
9+
}
10+
?>
11+
--FILE--
12+
<?php
13+
require_once "connect.inc";
14+
15+
require 'table.inc';
16+
17+
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
18+
19+
// first, control case
20+
$id = 1;
21+
$abc = 'abc';
22+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
23+
$stmt->bind_param('sss', ...[&$abc, 42, $id]);
24+
$stmt->execute();
25+
assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']);
26+
$stmt = null;
27+
28+
// 1. same as the control case, but skipping the middle-man (bind_param)
29+
$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?');
30+
try {
31+
$stmt->execute([&$abc, 42, $id]);
32+
} catch (ArgumentCountError $e) {
33+
echo '[001] '.$e->getMessage()."\n";
34+
}
35+
$stmt = null;
36+
37+
mysqli_close($link);
38+
?>
39+
--CLEAN--
40+
<?php
41+
require_once "clean_table.inc";
42+
?>
43+
--EXPECT--
44+
[001] Binding parameters in execute is not supported with libmysqlclient

0 commit comments

Comments
 (0)