如何在 PHP 8 中修复这个动态 SQL 查询功能?

Posted

技术标签:

【中文标题】如何在 PHP 8 中修复这个动态 SQL 查询功能?【英文标题】:How to fix this dynamic SQL query function in PHP 8? 【发布时间】:2021-07-03 06:43:34 【问题描述】:

在我的旧项目中,我在进行查询时使用了一个函数来“缩短”我的代码。

而不是使用通常的方法

$conn = [...]
$stmt = $conn->prepare(...)
$stmt->bind_param(...)
$stmt->execute();
$stmt->close();
$conn->close();

我得到了一个函数来做这件事,叫做dynamic_db_reader($mysqli, $param, $qry)

它返回一个数组(或 null),如:$array[index]['column_name'] = value

或者至少,它在以前的版本中是这样做的。 (在 php 7.4.16 中工作)

这是我的函数的代码:

/**
 * Dynamically executes a given sql statement as prepared statement (?-placeholder).
 * Expects correct parameters as an array to replace ?.
 * Returns an array with ($arr[index]['column_name'] = value), or null.
 *
 * @param $ms       mysqli
 * @param $params   array
 * @param $qry      string
 * @return array|null
 */
function dynamic_db_reader($ms, $params, $qry)

    $fields = array();
    $results = array();

    // Replace prefix (DBPREF in: inc/config.php)
    if (strpos($qry, 'prefix_') !== false)
        $qry = str_replace('prefix', DBPREF, $qry);
    

    // Set charset
    mysqli_set_charset($ms, 'utf8mb4');

    if ($stmt = $ms->prepare($qry))

        // Dynamically bind parameters from $params
        if (!isset($params) || !empty($params))
            // Parameters are set
            $types = '';

            foreach($params as $param)
                // Set parameter data type
                if (is_string($param))
                    $types .= 's';              // Strings
                 else if (is_int($param))
                    $types .= 'i';              // Integer
                 else if (is_float($param))
                    $types .= 'd';              // Double/Float
                 else 
                    $types .= 'b';              // Default: Blob and unknown types
                
            

            $bind_names[] = $types;
            for ($i = 0; $i < count($params); $i++)
                $bind_name = 'bind' . $i;
                $$bind_name = $params[$i];
                $bind_names[] = &$$bind_name;
            

            call_user_func_array(array($stmt, 'bind_param'), $bind_names);
        

        $stmt->execute();

        $meta = $stmt->result_metadata();

        // Dynamically create an array to bind the results to
        while ($field = $meta->fetch_field())
            $var = $field->name;
            $$var = null;
            $fields[$var] = &$$var;
        

        // Bind results
        call_user_func_array(array($stmt, 'bind_result'), $fields); // --> Error :(

        // Fetch results
        $i = 0;
        while ($stmt->fetch())
            $results[$i] = array();
            foreach($fields as $k => $v)
                $results[$i][$k] = $v;
            
            $i++;
        

        // Close statement
        $stmt->close();

        if (sizeof($results) > 0)
            return $results;
        
    
    return NULL;

错误:

Fatal error:  Uncaught ArgumentCountError: mysqli_stmt::bind_result() does not accept unknown named parameters in [...]\inc\db.php:87
Stack trace:
#0 [...]\root\inc\db.php(87): mysqli_stmt->bind_result(data_key: NULL, data_value: NULL)
#1 [...]\root\inc\func\common.php(76): dynamic_db_reader(Object(mysqli), Array, 'SELECT * FROM v...')
#2 [...]\root\www\index.php(22): getTestArray()
#3 main
  thrown in [...]\root\inc\db.php on line 87

如何修复此代码,使其在 PHP 8 中也能正常工作?

【问题讨论】:

【参考方案1】:

对于如此简单的事情来说,这是一个非常长的方法。 PHP 8 添加了命名参数。当您解压缩要用作参数的数组时,其键将用作参数名称。 mysqli_stmt::bind_result() 不接受您传递的命名参数。

如果我们简化这段代码,那么它应该看起来像这样:

/**
 * Dynamically executes a given sql statement as prepared statement (?-placeholder).
 * Expects correct parameters as an array to replace ?.
 * Returns an array with ($arr[index]['column_name'] = value), or null.
 */
function dynamic_db_reader(mysqli $ms, array $params, string $qry): ?array

    // Replace prefix (DBPREF in: inc/config.php)
    if (strpos($qry, 'prefix_') !== false) 
        $qry = str_replace('prefix', DBPREF, $qry);
    

    $stmt = $ms->prepare($qry);

    // Dynamically bind parameters from $params
    if ($params) 
        $stmt->bind_param(str_repeat('s', count($params)), ...$params);
    

    $stmt->execute();

    return $stmt->get_result()->fetch_all(MYSQLI_ASSOC) ?: null;



mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli('localhost', 'user', 'password', 'test');
$mysqli->set_charset('utf8mb4');

$results = dynamic_db_reader($mysqli, ['foo'], 'SELECT ?');

如果由于某种原因您使用的是从 libmysql 客户端编译的 mysqli,那么……是时候弄清楚如何启用 mysqlnd 或切换到 PDO。

附:请确保您已启用 mysqli 错误报告。 How to get the error message in MySQLi?。此外,每次都设置字符集是没有意义的。建立连接后立即设置。

【讨论】:

【参考方案2】:

我看到的情况是 call_user_func_array 被传递 $fields 作为 ['array_key_txt'=&gt;'array_value'] 导致错误。

尝试将您的 $fields 数组包装在“array_values”中

call_user_func_array(array($stmt, 'bind_result'), array_values($fields));


更新:here 网站解释了 PHP8 中新的“命名参数”

它指出:

str_replace(time_limit: "mi");

会产生错误:

致命错误:未捕获错误:未知命名参数 $time_limit in...

https:php.watch 说的:

所有 call_user_func_array() 函数调用都必须注意 PHP 8.0 将关联数组和数值数组解释不同。

作为预防措施, call_user_func_array() 调用可以使用 如果参数数组可能包含非数字键,则为 array_values。

$params = [
      'replace' => 'Iron Man',
      'search' => 'Inevitable',
      'subject' => 'I am Inevitable', ];  

echo call_user_func_array('str_replace', array_values($params));

使用 array_values 调用,PHP 将始终使用位置调用 模式,确保 PHP 8.0 及更高版本中的结果相同。

【讨论】:

以上是关于如何在 PHP 8 中修复这个动态 SQL 查询功能?的主要内容,如果未能解决你的问题,请参考以下文章

如何修复这个内连接 SQL 查询?而不是 1 count() 返回 3

如何在 PHP 中动态绑定 mysqli bind_param 参数?

如何在 PHP 中动态绑定 mysqli bind_param 参数?

如何将 PHP 逗号分隔的字符串集成到 SQL 查询中?

如何防止使用动态表名进行 SQL 注入?

如何修复 laravel 5.5 中的查询生成器错误 sql 注入 *