1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

ext/pdo: Refactor PDO::FETCH_CLASS to not rely on a FCI and use a HashTable for ctor_arg

To call the constructor we now only store the CE and a HashTable for the arguments.
This reduces the size of the _pdo_stmt_t struct from 320 bytes to 232 bytes.
Moreover, this now means that the constructor argument array follows the usual CUFA semantics.
This change is a BC break, as string keys now act like named arguments.
Moreover, the automatic wrapping of by-value arguments for by-ref parameters has been dropped, and the usual E_WARNING is now emitted in those cases.

The do_fetch() is heavily refactored to simplify the execution flow, which also makes it easier to understand.
Additionally we add a new bitflag in_fetch to prevent modification of the fetch flags by userland when PDO is fetching from the DB.
This commit is contained in:
Gina Peter Banyard
2025-01-09 13:26:50 +00:00
parent 229df24ae8
commit 3ff7758bcf
16 changed files with 344 additions and 428 deletions

View File

@@ -50,6 +50,18 @@ PHP 8.5 UPGRADE NOTES
. pcntl_exec() now throws ValueErrors when entries or keys of the
$env_vars parameter contain null bytes.
- PDO:
. The constructor arguments set in conjunction with PDO::FETCH_CLASS now
follow the usual CUFA (call_user_func_array) semantics.
This means string keys will act like a named argument.
Moreover, automatic wrapping for by-value arguments passed to a by-ref
parameter has been removed, and the usual E_WARNING about this is now
emitted.
To pass a variable by-ref to a constructor argument use the general
array value reference assignment: $ctor_args = [&$valByRef]
. Attempting to modify a PDOStatement during a call to PDO::fetch(),
PDO::fetchObject(), PDO::fetchAll() will now throw an Error.
- PDO_FIREBIRD:
. A ValueError is now thrown when trying to set a cursor name that is too
long on a PDOStatement resulting from the Firebird driver.

View File

@@ -610,60 +610,6 @@ static bool do_fetch_common(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, ze
}
/* }}} */
static bool do_fetch_class_prepare(pdo_stmt_t *stmt) /* {{{ */
{
zend_class_entry *ce = stmt->fetch.cls.ce;
zend_fcall_info *fci = &stmt->fetch.cls.fci;
zend_fcall_info_cache *fcc = &stmt->fetch.cls.fcc;
fci->size = sizeof(zend_fcall_info);
if (!ce) {
stmt->fetch.cls.ce = ZEND_STANDARD_CLASS_DEF_PTR;
ce = ZEND_STANDARD_CLASS_DEF_PTR;
}
if (ce->constructor) {
ZVAL_UNDEF(&fci->function_name);
fci->param_count = 0;
fci->params = NULL;
zend_fcall_info_args_ex(fci, ce->constructor, &stmt->fetch.cls.ctor_args);
fcc->function_handler = ce->constructor;
fcc->called_scope = ce;
return 1;
} else if (!Z_ISUNDEF(stmt->fetch.cls.ctor_args)) {
zend_throw_error(NULL, "User-supplied statement does not accept constructor arguments");
return 0;
} else {
return 1; /* no ctor no args is also ok */
}
}
/* }}} */
static void do_fetch_opt_finish(pdo_stmt_t *stmt, bool free_ctor_agrs) /* {{{ */
{
/* fci.size is used to check if it is valid */
if (stmt->fetch.cls.fci.size && stmt->fetch.cls.fci.params) {
if (!Z_ISUNDEF(stmt->fetch.cls.ctor_args)) {
/* Added to free constructor arguments */
zend_fcall_info_args_clear(&stmt->fetch.cls.fci, 1);
} else {
efree(stmt->fetch.cls.fci.params);
}
stmt->fetch.cls.fci.params = NULL;
}
stmt->fetch.cls.fci.size = 0;
if (!Z_ISUNDEF(stmt->fetch.cls.ctor_args) && free_ctor_agrs) {
zval_ptr_dtor(&stmt->fetch.cls.ctor_args);
ZVAL_UNDEF(&stmt->fetch.cls.ctor_args);
stmt->fetch.cls.fci.param_count = 0;
}
}
/* }}} */
static bool pdo_do_key_pair_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, zend_long offset, HashTable *container)
{
if (!do_fetch_common(stmt, ori, offset)) {
@@ -688,13 +634,41 @@ static bool pdo_do_key_pair_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation o
return true;
}
/* perform a fetch.
* Stores values into return_value according to HOW. */
/* Return value MUST be an initialized object */
static bool pdo_call_fetch_object_constructor(zend_function *constructor, HashTable *ctor_args, zval *return_value)
{
zval retval_constructor_call;
zend_fcall_info fci = {
.size = sizeof(zend_fcall_info),
.function_name = {},
.object = Z_OBJ_P(return_value),
.retval = &retval_constructor_call,
.param_count = 0,
.params = NULL,
.named_params = ctor_args,
};
zend_fcall_info_cache fcc = {
.function_handler = constructor,
.object = Z_OBJ_P(return_value),
.called_scope = Z_OBJCE_P(return_value),
.calling_scope = NULL,
.closure = NULL,
};
zend_call_function(&fci, &fcc);
bool failed = Z_ISUNDEF(retval_constructor_call);
zval_ptr_dtor(&retval_constructor_call);
return failed;
}
/* Performs a row fetch, the value is stored into return_value according to HOW.
* retun_value MUST be safely destroyable as it will be freed if an error occurs. */
static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type how, enum pdo_fetch_orientation ori, zend_long offset, zval *group_key) /* {{{ */
{
int flags, idx, old_arg_count = 0;
zend_class_entry *ce = NULL, *old_ce = NULL;
zval old_ctor_args = {{0}, {0}, {0}};
int flags;
zend_class_entry *ce = NULL;
HashTable *ctor_arguments = NULL;
int column_index_to_fetch = 0;
zval *fetch_function_params = NULL;
uint32_t fetch_function_param_num = 0;
@@ -756,8 +730,7 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h
return true;
}
RETVAL_FALSE;
stmt->in_fetch = true;
switch (how) {
case PDO_FETCH_USE_DEFAULT:
case PDO_FETCH_ASSOC:
@@ -772,90 +745,81 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h
break;
case PDO_FETCH_OBJ:
object_init_ex(return_value, ZEND_STANDARD_CLASS_DEF_PTR);
ce = zend_standard_class_def;
object_init(return_value);
break;
case PDO_FETCH_CLASS:
if (flags & PDO_FETCH_CLASSTYPE) {
zval val;
zend_class_entry *cep;
old_ce = stmt->fetch.cls.ce;
ZVAL_COPY_VALUE(&old_ctor_args, &stmt->fetch.cls.ctor_args);
old_arg_count = stmt->fetch.cls.fci.param_count;
do_fetch_opt_finish(stmt, 0);
fetch_value(stmt, &val, column_index_to_fetch++, NULL);
if (Z_TYPE(val) != IS_NULL) {
if (!try_convert_to_string(&val)) {
return 0;
}
if ((cep = zend_lookup_class(Z_STR(val))) == NULL) {
stmt->fetch.cls.ce = ZEND_STANDARD_CLASS_DEF_PTR;
} else {
stmt->fetch.cls.ce = cep;
}
}
do_fetch_class_prepare(stmt);
zval_ptr_dtor_str(&val);
}
ce = stmt->fetch.cls.ce;
/* TODO: Make this an assertion and ensure this is true higher up? */
if (!ce) {
/* TODO Error? */
pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "No fetch class specified");
return 0;
}
if ((flags & PDO_FETCH_SERIALIZE) == 0) {
if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) {
return 0;
if (flags & PDO_FETCH_CLASSTYPE) {
zval ce_name_from_column;
fetch_value(stmt, &ce_name_from_column, column_index_to_fetch++, NULL);
/* This used to use try_convert_to_string() which would silently support integers, floats, null
* even if any such value could not generate a valid class name, as no class was found it would
* then proceed to use stdClass */
// TODO Raise PDO implementation error when the column name is not a string
if (Z_TYPE(ce_name_from_column) == IS_STRING) {
ce = zend_lookup_class(Z_STR(ce_name_from_column));
}
if (!stmt->fetch.cls.fci.size) {
if (!do_fetch_class_prepare(stmt)) {
zval_ptr_dtor(return_value);
return 0;
}
/* Use default CE if present */
if (ce == NULL) {
ce = zend_standard_class_def;
}
zval_ptr_dtor(&ce_name_from_column);
} else {
/* This can happen if the fetch flags are set via PDO::setAttribute()
* $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_CLASS);
* See ext/pdo/tests/bug_38253.phpt */
if (UNEXPECTED(ce == NULL)) {
pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "No fetch class specified");
goto in_fetch_error;
}
ctor_arguments = stmt->fetch.cls.ctor_args;
}
ZEND_ASSERT(ce != NULL);
if (flags & PDO_FETCH_SERIALIZE) {
if (!ce->unserialize) {
/* As this option is deprecated we do not bother to mention the class name. */
pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "cannot unserialize class");
goto in_fetch_error;
}
} else {
if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) {
goto in_fetch_error;
}
if (ce->constructor && (flags & PDO_FETCH_PROPS_LATE)) {
zval retval_constructor_call;
stmt->fetch.cls.fci.retval = &retval_constructor_call;
stmt->fetch.cls.fci.object = Z_OBJ_P(return_value);
stmt->fetch.cls.fcc.object = Z_OBJ_P(return_value);
zend_call_function(&stmt->fetch.cls.fci, &stmt->fetch.cls.fcc);
if (Z_TYPE(retval_constructor_call) == IS_UNDEF) {
/* Exception has happened */
bool failed = pdo_call_fetch_object_constructor(ce->constructor, ctor_arguments, return_value);
if (UNEXPECTED(failed)) {
zval_ptr_dtor(return_value);
return false;
goto in_fetch_error;
}
zval_ptr_dtor(&retval_constructor_call);
ZVAL_UNDEF(stmt->fetch.cls.fci.retval);
}
}
break;
case PDO_FETCH_INTO:
/* TODO: Make this an assertion and ensure this is true higher up? */
/* This can happen if the fetch flags are set via PDO::setAttribute()
* $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_INTO);
* See ext/pdo/tests/bug_38253.phpt */
if (stmt->fetch.into == NULL) {
/* TODO ArgumentCountError? */
pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "No fetch-into object specified.");
return 0;
break;
goto in_fetch_error;
}
ZVAL_OBJ_COPY(return_value, stmt->fetch.into);
if (Z_OBJ_P(return_value)->ce == ZEND_STANDARD_CLASS_DEF_PTR) {
how = PDO_FETCH_OBJ;
}
/* We want the behaviour of fetching into an object to be called from the global scope rather
* than the object scope */
ce = NULL;
break;
case PDO_FETCH_FUNC:
/* TODO: Make this an assertion and ensure this is true higher up? */
if (!ZEND_FCC_INITIALIZED(stmt->fetch.func.fcc)) {
/* TODO ArgumentCountError? */
/* This can happen if the fetch flags are set via PDO::setAttribute()
* $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_FUNC);
* See ext/pdo/tests/bug_38253.phpt */
if (UNEXPECTED(!ZEND_FCC_INITIALIZED(stmt->fetch.func.fcc))) {
pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "No fetch function specified");
return false;
goto in_fetch_error;
}
/* There will be at most stmt->column_count parameters.
* However, if we fetch a group key we will have over allocated. */
@@ -870,7 +834,27 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h
column_index_to_fetch++;
}
for (idx = 0; column_index_to_fetch < stmt->column_count; column_index_to_fetch++, idx++) {
if (how == PDO_FETCH_CLASS && (flags & PDO_FETCH_SERIALIZE)) {
zval unserialization_string;
fetch_value(stmt, &unserialization_string, column_index_to_fetch, NULL);
const unsigned char *str = (const unsigned char*) "";
size_t str_len = 0;
if (Z_TYPE(unserialization_string) == IS_STRING) {
str = (unsigned char*) Z_STRVAL(unserialization_string);
str_len = Z_STRLEN(unserialization_string);
}
zend_result unserialize_res = ce->unserialize(return_value, ce, str, str_len, NULL);
zval_ptr_dtor(&unserialization_string);
if (unserialize_res == FAILURE) {
pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "cannot unserialize class");
zval_ptr_dtor(return_value);
goto in_fetch_error;
}
column_index_to_fetch++;
}
for (; column_index_to_fetch < stmt->column_count; column_index_to_fetch++) {
zval val;
fetch_value(stmt, &val, column_index_to_fetch, NULL);
zend_string *column_name = stmt->columns[column_index_to_fetch].name;
@@ -926,82 +910,41 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h
zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &val);
break;
case PDO_FETCH_CLASS:
case PDO_FETCH_OBJ:
case PDO_FETCH_INTO:
zend_update_property_ex(NULL, Z_OBJ_P(return_value), column_name, &val);
zend_update_property_ex(ce, Z_OBJ_P(return_value), column_name, &val);
zval_ptr_dtor(&val);
break;
case PDO_FETCH_CLASS:
if ((flags & PDO_FETCH_SERIALIZE) == 0 || idx) {
zend_update_property_ex(ce, Z_OBJ_P(return_value), column_name, &val);
zval_ptr_dtor(&val);
} else {
if (!ce->unserialize) {
zval_ptr_dtor(&val);
pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "cannot unserialize class");
return 0;
} else if (ce->unserialize(return_value, ce, (unsigned char *)(Z_TYPE(val) == IS_STRING ? Z_STRVAL(val) : ""), Z_TYPE(val) == IS_STRING ? Z_STRLEN(val) : 0, NULL) == FAILURE) {
zval_ptr_dtor(&val);
pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "cannot unserialize class");
zval_ptr_dtor(return_value);
ZVAL_NULL(return_value);
return 0;
} else {
zval_ptr_dtor(&val);
}
}
break;
case PDO_FETCH_FUNC:
ZVAL_COPY_VALUE(&fetch_function_params[fetch_function_param_num++], &val);
break;
default:
zval_ptr_dtor(&val);
zend_value_error("Fetch mode must be a bitmask of PDO::FETCH_* constants");
return 0;
EMPTY_SWITCH_DEFAULT_CASE();
}
}
switch (how) {
case PDO_FETCH_CLASS:
if (ce->constructor && !(flags & (PDO_FETCH_PROPS_LATE | PDO_FETCH_SERIALIZE))) {
zval retval_constructor_call;
stmt->fetch.cls.fci.retval = &retval_constructor_call;
stmt->fetch.cls.fci.object = Z_OBJ_P(return_value);
stmt->fetch.cls.fcc.object = Z_OBJ_P(return_value);
zend_call_function(&stmt->fetch.cls.fci, &stmt->fetch.cls.fcc);
if (Z_TYPE(retval_constructor_call) == IS_UNDEF) {
/* Exception has happened */
zval_ptr_dtor(return_value);
return false;
}
zval_ptr_dtor(&retval_constructor_call);
ZVAL_UNDEF(stmt->fetch.cls.fci.retval);
}
if (flags & PDO_FETCH_CLASSTYPE) {
do_fetch_opt_finish(stmt, 0);
stmt->fetch.cls.ce = old_ce;
ZVAL_COPY_VALUE(&stmt->fetch.cls.ctor_args, &old_ctor_args);
stmt->fetch.cls.fci.param_count = old_arg_count;
}
break;
case PDO_FETCH_FUNC:
zend_call_known_fcc(&stmt->fetch.func.fcc, return_value, fetch_function_param_num, fetch_function_params, NULL);
/* Free FCI parameters that were allocated in the previous loop */
for (uint32_t param_num = 0; param_num < fetch_function_param_num; param_num++) {
zval_ptr_dtor(&fetch_function_params[param_num]);
}
efree(fetch_function_params);
break;
default:
break;
/* Run constructor for objects if not already run and not unserialized */
if (how == PDO_FETCH_CLASS && ce->constructor && !(flags & (PDO_FETCH_PROPS_LATE | PDO_FETCH_SERIALIZE))) {
bool failed = pdo_call_fetch_object_constructor(ce->constructor, ctor_arguments, return_value);
if (UNEXPECTED(failed)) {
zval_ptr_dtor(return_value);
goto in_fetch_error;
}
} else if (how == PDO_FETCH_FUNC) {
zend_call_known_fcc(&stmt->fetch.func.fcc, return_value, fetch_function_param_num, fetch_function_params, NULL);
/* Free FCI parameters that were allocated in the previous loop */
for (uint32_t param_num = 0; param_num < fetch_function_param_num; param_num++) {
zval_ptr_dtor(&fetch_function_params[param_num]);
}
efree(fetch_function_params);
}
stmt->in_fetch = false;
return true;
return 1;
in_fetch_error:
stmt->in_fetch = false;
return false;
}
/* }}} */
@@ -1108,44 +1051,38 @@ PHP_METHOD(PDOStatement, fetchObject)
{
zend_class_entry *ce = NULL;
zend_class_entry *old_ce;
zval old_ctor_args, *ctor_args = NULL;
int old_arg_count;
HashTable *old_ctor_args, *ctor_args = NULL;
ZEND_PARSE_PARAMETERS_START(0, 2)
Z_PARAM_OPTIONAL
Z_PARAM_CLASS_OR_NULL(ce)
Z_PARAM_ARRAY(ctor_args)
Z_PARAM_ARRAY_HT(ctor_args)
ZEND_PARSE_PARAMETERS_END();
PHP_STMT_GET_OBJ;
PDO_STMT_CLEAR_ERR();
old_ce = stmt->fetch.cls.ce;
ZVAL_COPY_VALUE(&old_ctor_args, &stmt->fetch.cls.ctor_args);
old_arg_count = stmt->fetch.cls.fci.param_count;
old_ctor_args = stmt->fetch.cls.ctor_args;
do_fetch_opt_finish(stmt, 0);
if (ce == NULL) {
ce = zend_standard_class_def;
}
if (ctor_args && zend_hash_num_elements(Z_ARRVAL_P(ctor_args))) {
ZVAL_ARR(&stmt->fetch.cls.ctor_args, zend_array_dup(Z_ARRVAL_P(ctor_args)));
} else {
ZVAL_UNDEF(&stmt->fetch.cls.ctor_args);
}
if (ce) {
stmt->fetch.cls.ce = ce;
} else {
stmt->fetch.cls.ce = zend_standard_class_def;
if (ctor_args && zend_hash_num_elements(ctor_args) && ce->constructor == NULL) {
zend_argument_value_error(2, "must be empty when class provided in argument #1 ($class) does not have a constructor");
RETURN_THROWS();
}
stmt->fetch.cls.ce = ce;
stmt->fetch.cls.ctor_args = ctor_args;
if (!do_fetch(stmt, return_value, PDO_FETCH_CLASS, PDO_FETCH_ORI_NEXT, /* offset */ 0, NULL)) {
PDO_HANDLE_STMT_ERR();
RETVAL_FALSE;
}
do_fetch_opt_finish(stmt, 1);
stmt->fetch.cls.ce = old_ce;
ZVAL_COPY_VALUE(&stmt->fetch.cls.ctor_args, &old_ctor_args);
stmt->fetch.cls.fci.param_count = old_arg_count;
stmt->fetch.cls.ctor_args = old_ctor_args;
}
/* }}} */
@@ -1197,15 +1134,13 @@ PHP_METHOD(PDOStatement, fetchAll)
zend_long how = PDO_FETCH_USE_DEFAULT;
zval *arg2 = NULL;
zend_class_entry *old_ce;
zval old_ctor_args, *ctor_args = NULL;
uint32_t old_arg_count;
HashTable *current_ctor = NULL;
HashTable *old_ctor_args, *ctor_args = NULL;
ZEND_PARSE_PARAMETERS_START(0, 3)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(how)
Z_PARAM_ZVAL_OR_NULL(arg2)
Z_PARAM_ARRAY_OR_NULL(ctor_args)
Z_PARAM_ARRAY_HT_OR_NULL(ctor_args)
ZEND_PARSE_PARAMETERS_END();
PHP_STMT_GET_OBJ;
@@ -1217,48 +1152,38 @@ PHP_METHOD(PDOStatement, fetchAll)
int flags = how & PDO_FETCH_FLAGS;
old_ce = stmt->fetch.cls.ce;
ZVAL_COPY_VALUE(&old_ctor_args, &stmt->fetch.cls.ctor_args);
if (Z_TYPE(old_ctor_args) == IS_ARRAY) {
/* Protect against destruction by marking this as immutable: we consider this non-owned temporarily */
Z_TYPE_INFO(stmt->fetch.cls.ctor_args) = IS_ARRAY;
}
old_arg_count = stmt->fetch.cls.fci.param_count;
do_fetch_opt_finish(stmt, 0);
old_ctor_args = stmt->fetch.cls.ctor_args;
/* TODO Would be good to reuse part of pdo_stmt_setup_fetch_mode() in some way */
switch (fetch_mode) {
case PDO_FETCH_CLASS:
case PDO_FETCH_CLASS: {
/* Figure out correct class */
zend_class_entry *fetch_class = NULL;
if (arg2) {
if (Z_TYPE_P(arg2) != IS_STRING) {
zend_argument_type_error(2, "must be of type string, %s given", zend_zval_value_name(arg2));
RETURN_THROWS();
}
stmt->fetch.cls.ce = zend_lookup_class(Z_STR_P(arg2));
if (!stmt->fetch.cls.ce) {
fetch_class = zend_lookup_class(Z_STR_P(arg2));
if (fetch_class == NULL) {
zend_argument_type_error(2, "must be a valid class");
RETURN_THROWS();
}
} else {
stmt->fetch.cls.ce = zend_standard_class_def;
fetch_class = zend_standard_class_def;
}
if (ctor_args && zend_hash_num_elements(Z_ARRVAL_P(ctor_args)) > 0) {
/* We increase the refcount and store it in case usercode has been messing around with the ctor args.
* We need to store current_ctor separately as usercode may change the ctor_args which will cause a leak. */
current_ctor = Z_ARRVAL_P(ctor_args);
ZVAL_COPY(&stmt->fetch.cls.ctor_args, ctor_args);
/* Protect against destruction by marking this as immutable: we consider this non-owned
* as destruction is handled via current_ctor. */
Z_TYPE_INFO(stmt->fetch.cls.ctor_args) = IS_ARRAY;
} else {
ZVAL_UNDEF(&stmt->fetch.cls.ctor_args);
if (ctor_args && zend_hash_num_elements(ctor_args) > 0) {
if (fetch_class->constructor == NULL) {
zend_argument_value_error(3, "must be empty when class provided in argument #2 ($class) does not have a constructor");
RETURN_THROWS();
}
stmt->fetch.cls.ctor_args = ctor_args;
}
stmt->fetch.cls.ce = fetch_class;
do_fetch_class_prepare(stmt);
break;
}
case PDO_FETCH_FUNC: /* Cannot be a default fetch mode */
if (ZEND_NUM_ARGS() != 2) {
@@ -1309,7 +1234,6 @@ PHP_METHOD(PDOStatement, fetchAll)
}
}
if (fetch_mode == PDO_FETCH_USE_DEFAULT) {
flags |= stmt->default_fetch_type & PDO_FETCH_FLAGS;
fetch_mode = stmt->default_fetch_type & ~PDO_FETCH_FLAGS;
@@ -1320,6 +1244,7 @@ PHP_METHOD(PDOStatement, fetchAll)
zval data, group_key;
ZVAL_UNDEF(&data);
array_init(return_value);
if (fetch_mode == PDO_FETCH_KEY_PAIR) {
@@ -1353,20 +1278,11 @@ PHP_METHOD(PDOStatement, fetchAll)
}
}
do_fetch_opt_finish(stmt, 0);
if (current_ctor) {
zend_array_release(current_ctor);
}
/* Restore defaults which were changed by PDO_FETCH_CLASS mode */
stmt->fetch.cls.ce = old_ce;
/* ctor_args may have been changed to an owned object in the meantime, so destroy it.
* If it was not, then the type flags update will have protected us against destruction. */
zval_ptr_dtor(&stmt->fetch.cls.ctor_args);
ZVAL_COPY_VALUE(&stmt->fetch.cls.ctor_args, &old_ctor_args);
stmt->fetch.cls.fci.param_count = old_arg_count;
stmt->fetch.cls.ctor_args = old_ctor_args;
PDO_HANDLE_STMT_ERR();
PDO_HANDLE_STMT_ERR_EX(zval_ptr_dtor(return_value); RETVAL_EMPTY_ARRAY(););
}
/* }}} */
@@ -1660,6 +1576,24 @@ PHP_METHOD(PDOStatement, getColumnMeta)
}
/* }}} */
void pdo_stmt_free_default_fetch_mode(pdo_stmt_t *stmt)
{
enum pdo_fetch_type default_fetch_mode = stmt->default_fetch_type & ~PDO_FETCH_FLAGS;
if (default_fetch_mode == PDO_FETCH_INTO) {
/* This can happen if the fetch flags are set via PDO::setAttribute()
* $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_INTO);
* See ext/pdo/tests/bug_38253.phpt */
if (EXPECTED(stmt->fetch.into != NULL)) {
OBJ_RELEASE(stmt->fetch.into);
}
} else if (default_fetch_mode == PDO_FETCH_CLASS) {
if (stmt->fetch.cls.ctor_args != NULL) {
zend_array_release(stmt->fetch.cls.ctor_args);
}
}
memset(&stmt->fetch, 0, sizeof(stmt->fetch));
}
/* {{{ Changes the default fetch mode for subsequent fetches (params have different meaning for different fetch modes) */
bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_arg_num,
@@ -1670,16 +1604,7 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a
uint32_t constructor_arg_num = mode_arg_num + 2;
uint32_t total_num_args = mode_arg_num + variadic_num_args;
switch (stmt->default_fetch_type) {
case PDO_FETCH_INTO:
if (stmt->fetch.into) {
OBJ_RELEASE(stmt->fetch.into);
stmt->fetch.into = NULL;
}
break;
default:
;
}
pdo_stmt_free_default_fetch_mode(stmt);
stmt->default_fetch_type = PDO_FETCH_BOTH;
@@ -1728,9 +1653,6 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a
break;
case PDO_FETCH_CLASS: {
HashTable *constructor_args = NULL;
/* Undef constructor arguments */
ZVAL_UNDEF(&stmt->fetch.cls.ctor_args);
/* Gets its class name from 1st column */
if ((flags & PDO_FETCH_CLASSTYPE) == PDO_FETCH_CLASSTYPE) {
if (variadic_num_args != 0) {
@@ -1740,7 +1662,6 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a
zend_string_release(func);
return false;
}
stmt->fetch.cls.ce = NULL;
} else {
zend_class_entry *cep;
if (variadic_num_args == 0) {
@@ -1776,18 +1697,16 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a
return false;
}
if (Z_TYPE(args[1]) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL(args[1]))) {
constructor_args = Z_ARRVAL(args[1]);
if (cep->constructor == NULL) {
zend_argument_value_error(3, "must be empty when class provided in argument #2 ($class) does not have a constructor");
return false;
}
GC_TRY_ADDREF(Z_ARRVAL(args[1]));
stmt->fetch.cls.ctor_args = Z_ARRVAL(args[1]);
}
}
stmt->fetch.cls.ce = cep;
/* If constructor arguments are present and not empty */
if (constructor_args) {
ZVAL_ARR(&stmt->fetch.cls.ctor_args, zend_array_dup(constructor_args));
}
}
do_fetch_class_prepare(stmt);
break;
}
case PDO_FETCH_INTO:
@@ -1828,8 +1747,10 @@ PHP_METHOD(PDOStatement, setFetchMode)
PHP_STMT_GET_OBJ;
do_fetch_opt_finish(stmt, 1);
if (stmt->in_fetch) {
zend_throw_error(NULL, "Cannot change default fetch mode while fetching");
RETURN_THROWS();
}
if (!pdo_stmt_setup_fetch_mode(stmt, fetch_mode, 1, args, num_args)) {
RETURN_THROWS();
}
@@ -2051,8 +1972,8 @@ static HashTable *dbstmt_get_gc(zend_object *object, zval **gc_data, int *gc_cou
zend_get_gc_buffer_add_obj(gc_buffer, stmt->database_object_handle);
if (default_fetch_mode == PDO_FETCH_INTO) {
zend_get_gc_buffer_add_obj(gc_buffer, stmt->fetch.into);
} else if (default_fetch_mode == PDO_FETCH_CLASS) {
zend_get_gc_buffer_add_zval(gc_buffer, &stmt->fetch.cls.ctor_args);
} else if (default_fetch_mode == PDO_FETCH_CLASS && stmt->fetch.cls.ctor_args != NULL) {
zend_get_gc_buffer_add_ht(gc_buffer, stmt->fetch.cls.ctor_args);
}
zend_get_gc_buffer_use(gc_buffer, gc_data, gc_count);
@@ -2099,13 +2020,7 @@ PDO_API void php_pdo_free_statement(pdo_stmt_t *stmt)
}
pdo_stmt_reset_columns(stmt);
if (stmt->fetch.into && stmt->default_fetch_type == PDO_FETCH_INTO) {
OBJ_RELEASE(stmt->fetch.into);
stmt->fetch.into = NULL;
}
do_fetch_opt_finish(stmt, 1);
pdo_stmt_free_default_fetch_mode(stmt);
if (stmt->database_object_handle != NULL) {
OBJ_RELEASE(stmt->database_object_handle);
@@ -2148,6 +2063,7 @@ static void pdo_stmt_iter_dtor(zend_object_iterator *iter)
if (!Z_ISUNDEF(I->fetch_ahead)) {
zval_ptr_dtor(&I->fetch_ahead);
ZVAL_UNDEF(&I->fetch_ahead);
}
}
@@ -2188,6 +2104,7 @@ static void pdo_stmt_iter_move_forwards(zend_object_iterator *iter)
if (!Z_ISUNDEF(I->fetch_ahead)) {
zval_ptr_dtor(&I->fetch_ahead);
ZVAL_UNDEF(&I->fetch_ahead);
}
if (!do_fetch(stmt, &I->fetch_ahead, PDO_FETCH_USE_DEFAULT,

View File

@@ -568,7 +568,9 @@ struct _pdo_stmt_t {
* emulate prepare and bind on its behalf */
unsigned supports_placeholders:2;
unsigned _reserved:29;
/* If true we are in a do_fetch() call, and modification to the statement must be prevented */
unsigned in_fetch:1;
unsigned _reserved:28;
/* the number of columns in the result set; not valid until after
* the statement has been executed at least once. In some cases, might
@@ -611,13 +613,10 @@ struct _pdo_stmt_t {
union {
int column;
struct {
zval ctor_args; /* freed */
zend_fcall_info_cache fcc;
zend_fcall_info fci;
HashTable *ctor_args;
zend_class_entry *ce;
} cls;
struct {
zval dummy; /* This exists due to alignment reasons with fetch.into and fetch.cls.ctor_args */
zend_fcall_info_cache fcc;
} func;
zend_object *into;

View File

@@ -34,6 +34,7 @@ PDO_API void pdo_handle_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt);
memcpy(stmt->error_code, PDO_ERR_NONE, sizeof(PDO_ERR_NONE)); \
} while (0)
#define PDO_HANDLE_DBH_ERR() if (strcmp(dbh->error_code, PDO_ERR_NONE)) { pdo_handle_error(dbh, NULL); }
#define PDO_HANDLE_STMT_ERR() if (strcmp(stmt->error_code, PDO_ERR_NONE)) { pdo_handle_error(stmt->dbh, stmt); }
#define PDO_HANDLE_STMT_ERR_EX(cleanup_instruction) if (strcmp(stmt->error_code, PDO_ERR_NONE) != 0) { cleanup_instruction pdo_handle_error(stmt->dbh, stmt); }
#define PDO_HANDLE_STMT_ERR() PDO_HANDLE_STMT_ERR_EX(;)
#endif /* PHP_PDO_ERROR_H */

View File

@@ -48,7 +48,8 @@ $stmt->execute();
var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'TestBase'));
$stmt->execute();
var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'TestDerived', array(0)));
$rowCounter = 0;
var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'TestDerived', [&$rowCounter]));
?>
--CLEAN--

View File

@@ -47,9 +47,14 @@ $db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_by_ref_ctor");
?>
--EXPECTF--
Warning: TestByRefCtor::__construct(): Argument #1 ($str) must be passed by reference, value given in %s on line %d
TestByRefCtor::__construct(aaaaaaaaaa, 1)
TestByRefCtor::__construct(aaaaaaaaaaA, 2)
TestByRefCtor::__construct(aaaaaaaaaaAB, 3)
Warning: TestByRefCtor::__construct(): Argument #1 ($str) must be passed by reference, value given in %s on line %d
TestByRefCtor::__construct(aaaaaaaaaa, 2)
Warning: TestByRefCtor::__construct(): Argument #1 ($str) must be passed by reference, value given in %s on line %d
TestByRefCtor::__construct(aaaaaaaaaa, 3)
array(3) {
[0]=>
object(TestByRefCtor)#%d (3) {
@@ -67,7 +72,7 @@ array(3) {
["val"]=>
string(1) "B"
["str":"TestByRefCtor":private]=>
string(12) "aaaaaaaaaaAB"
string(11) "aaaaaaaaaaB"
}
[2]=>
object(TestByRefCtor)#%d (3) {
@@ -76,6 +81,6 @@ array(3) {
["val"]=>
string(1) "C"
["str":"TestByRefCtor":private]=>
string(13) "aaaaaaaaaaABC"
string(11) "aaaaaaaaaaC"
}
}

View File

@@ -37,7 +37,12 @@ $stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_one');
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Test', [$stmt]);
$stmt->execute();
var_dump($stmt->fetch());
try {
var_dump($stmt->fetch());
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}
?>
--CLEAN--
@@ -51,9 +56,4 @@ object(PDOStatement)#%d (1) {
["queryString"]=>
string(54) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_one"
}
object(Test)#%d (2) {
["val1"]=>
string(1) "A"
["val2"]=>
string(5) "alpha"
}
Error: Cannot change default fetch mode while fetching

View File

@@ -37,7 +37,12 @@ $db->exec("INSERT INTO pdo_fetch_class_change_ctor_two VALUES(4, 'D', 'delta')")
$stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_two');
$stmt->execute();
var_dump($stmt->fetchObject('Test', [$stmt]));
try {
var_dump($stmt->fetchObject('Test', [$stmt]));
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}
?>
--CLEAN--
@@ -51,9 +56,4 @@ object(PDOStatement)#%s (1) {
["queryString"]=>
string(54) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_two"
}
object(Test)#%s (2) {
["val1"]=>
string(1) "A"
["val2"]=>
string(5) "alpha"
}
Error: Cannot change default fetch mode while fetching

View File

@@ -37,7 +37,12 @@ $stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_three')
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Test', [$stmt]);
$stmt->execute();
var_dump($stmt->fetchAll());
try {
var_dump($stmt->fetchAll());
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}
?>
--CLEAN--
@@ -51,36 +56,4 @@ object(PDOStatement)#%d (1) {
["queryString"]=>
string(56) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_three"
}
string(5) "alpha"
string(5) "alpha"
string(5) "alpha"
array(4) {
[0]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "A"
["val2"]=>
string(5) "alpha"
}
[1]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "B"
["val2"]=>
string(4) "beta"
}
[2]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "C"
["val2"]=>
string(5) "gamma"
}
[3]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "D"
["val2"]=>
string(5) "delta"
}
}
Error: Cannot change default fetch mode while fetching

View File

@@ -36,7 +36,12 @@ $db->exec("INSERT INTO pdo_fetch_class_change_ctor_four VALUES(4, 'D', 'delta')"
$stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_four');
$stmt->execute();
var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'Test', [$stmt]));
try {
var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'Test', [$stmt]));
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}
?>
--CLEAN--
@@ -50,36 +55,4 @@ object(PDOStatement)#%d (1) {
["queryString"]=>
string(55) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_four"
}
string(5) "alpha"
string(5) "alpha"
string(5) "alpha"
array(4) {
[0]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "A"
["val2"]=>
string(5) "alpha"
}
[1]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "B"
["val2"]=>
string(4) "beta"
}
[2]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "C"
["val2"]=>
string(5) "gamma"
}
[3]=>
object(Test)#%d (2) {
["val1"]=>
string(1) "D"
["val2"]=>
string(5) "delta"
}
}
Error: Cannot change default fetch mode while fetching

View File

@@ -38,7 +38,11 @@ function stuffingErrorHandler(int $errno, string $errstr, string $errfile, int $
}
set_error_handler(stuffingErrorHandler(...));
var_dump($stmt->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_SERIALIZE, 'B', [$stmt]));
try {
var_dump($stmt->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_SERIALIZE, 'B', [$stmt]));
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), \PHP_EOL;
}
?>
--CLEAN--
@@ -47,9 +51,6 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_change_ctor_five");
?>
--EXPECTF--
--EXPECT--
PDOStatement::fetchAll(): The PDO::FETCH_SERIALIZE mode is deprecated
PDOStatement::fetchAll(): SQLSTATE[HY000]: General error: cannot unserialize class
PDOStatement::fetchAll(): SQLSTATE[HY000]: General error%S
array(0) {
}
Error: Cannot change default fetch mode while fetching

View File

@@ -44,12 +44,12 @@ $db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_ctor_named");
?>
--EXPECTF--
Value of $a: My key is B
Value of $b: My key is A
Value of $a: My key is B
Value of $b: My key is A
Value of $a: My key is B
Value of $b: My key is A
Value of $a: My key is A
Value of $b: My key is B
Value of $a: My key is A
Value of $b: My key is B
Value of $a: My key is A
Value of $b: My key is B
array(3) {
[0]=>
object(TestBase)#%d (3) {

View File

@@ -48,39 +48,5 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_class_ctor_named_and_positional");
?>
--EXPECTF--
Value of $a: My key is B
Value of $b: No key
Value of $a: My key is B
Value of $b: No key
Value of $a: My key is B
Value of $b: No key
array(3) {
[0]=>
object(TestBase)#%d (3) {
["id"]=>
string(1) "1"
["val":protected]=>
string(1) "A"
["val2":"TestBase":private]=>
string(2) "AA"
}
[1]=>
object(TestBase)#%d (3) {
["id"]=>
string(1) "2"
["val":protected]=>
string(1) "B"
["val2":"TestBase":private]=>
string(2) "BB"
}
[2]=>
object(TestBase)#%d (3) {
["id"]=>
string(1) "3"
["val":protected]=>
string(1) "C"
["val2":"TestBase":private]=>
string(2) "CC"
}
}
--EXPECT--
Error: Cannot use positional argument after named argument

View File

@@ -47,5 +47,5 @@ $db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_fetch_all_class_ctor_error");
?>
--EXPECT--
Error: User-supplied statement does not accept constructor arguments
Error: User-supplied statement does not accept constructor arguments
ValueError: PDOStatement::fetchAll(): Argument #3 must be empty when class provided in argument #2 ($class) does not have a constructor
ValueError: PDOStatement::setFetchMode(): Argument #3 must be empty when class provided in argument #2 ($class) does not have a constructor

View File

@@ -40,15 +40,15 @@ class Bar {
$stmt->execute();
try {
$obj = $stmt->fetchObject(Foo::class);
} catch (ArgumentCountError $exception) {
echo $exception->getMessage() . "\n";
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
$stmt->execute();
try {
$obj = $stmt->fetchObject(Foo::class, []);
} catch (ArgumentCountError $exception) {
echo $exception->getMessage() . "\n";
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
$stmt->execute();
@@ -66,8 +66,8 @@ var_dump($obj);
try {
$stmt->execute();
$obj = $stmt->fetchObject(Bar::class, ["a" => 123]);
} catch (Error $exception) {
echo $exception->getMessage() . "\n";
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
?>
@@ -78,8 +78,8 @@ $db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_stmt_fetchobject_ctor_args");
?>
--EXPECTF--
Too few arguments to function Foo::__construct(), 0 passed and exactly 1 expected
Too few arguments to function Foo::__construct(), 0 passed and exactly 1 expected
ArgumentCountError: Too few arguments to function Foo::__construct(), 0 passed and exactly 1 expected
ArgumentCountError: Too few arguments to function Foo::__construct(), 0 passed and exactly 1 expected
object(Foo)#%d (2) {
["a"]=>
int(123)
@@ -94,4 +94,4 @@ object(Bar)#%d (1) {
["id"]=>
int(1)
}
User-supplied statement does not accept constructor arguments
ValueError: PDOStatement::fetchObject(): Argument #2 ($constructorArgs) must be empty when class provided in argument #1 ($class) does not have a constructor

View File

@@ -0,0 +1,68 @@
--TEST--
PDO Common: PDOStatement->fetchObject() with $constructorArgs when default CTORs have been set-up
--EXTENSIONS--
pdo
--SKIPIF--
<?php
$dir = getenv('REDIR_TEST_DIR');
if (false == $dir) die('skip no driver');
require_once $dir . 'pdo_test.inc';
PDOTest::skip();
?>
--FILE--
<?php
if (getenv('REDIR_TEST_DIR') === false) putenv('REDIR_TEST_DIR='.__DIR__ . '/../../pdo/tests/');
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
class Foo {
public int $id;
public function __construct(public string $v) { }
}
class Bar {
public int $id;
}
$table = 'pdo_stmt_fetchobject_ctor_args_after_default';
$db->exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))");
$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')");
$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')");
$db->exec("INSERT INTO {$table} (id, label) VALUES (3, 'c')");
$query = "SELECT id FROM {$table} ORDER BY id ASC";
$stmt = $db->prepare($query);
$stmt->setFetchMode(PDO::FETCH_CLASS, Foo::class, ['Hello']);
$stmt->execute();
var_dump($stmt->fetch());
try {
$obj = $stmt->fetchObject(Bar::class, ['no-args']);
var_dump($obj);
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
var_dump($stmt->fetch());
?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_stmt_fetchobject_ctor_args_after_default");
?>
--EXPECTF--
object(Foo)#%d (2) {
["id"]=>
int(1)
["v"]=>
string(5) "Hello"
}
ValueError: PDOStatement::fetchObject(): Argument #2 ($constructorArgs) must be empty when class provided in argument #1 ($class) does not have a constructor
object(Foo)#%d (2) {
["id"]=>
int(2)
["v"]=>
string(5) "Hello"
}