如何处理从PHP Mysql API到PDO的转换中的数据类型


How to handle data types in conversion from PHP Mysql API to PDO

我正在将我们的网站从PHP Mysql API转换为PDO,并且遇到了数据类型问题。

以前,我们转义了所有变量,就好像它们是字符串一样。 例如,

SET varname = '$varvalue'

现在,对于PDO,当然,我这样做

SET varname = :varvalue

然后我们有一个处理 $varvalue 值绑定的类,根据变量的类型设置数据类型。

当varname应该是一个字符串时,我们的问题就来了,而由于某种原因,$varvalue是空的。 以前,当 $varvalue 为 null 时,"$varvalue"会变成"。 现在,我们"正确"地将$varvalue绑定为 null,但数据库字段不允许 null。

我知道解决此问题的最正确方法是确保$varvalue以正确的值进入函数,但我们有一个庞大的遗留代码库,这将需要大量工作来实现。 另一种解决方案是在将每个变量绑定到正确的类型时显式强制转换每个变量。如果可能的话,我们更喜欢一种解决方案,避免我们不得不显式转换模型中的每个变量。 有吗?

这可能不是您一直在等待的答案,但应该提及:使用例外。

您可以将 PDO 配置为引发类型 PDOException 的异常,而不是依赖于返回值:

$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
您可以

捕获这些异常并记录它们,通过电子邮件发送它们等,以便您可以准确识别哪段代码传入了错误的值并修复它。

这是一个有点痛苦的操作;当我们开始在我们的网站上报告所有未捕获的异常并且我们的收件箱中充斥着错误时,我们不得不自己忍受这一点。它持续了几天,但我们设法清除了所有非常糟糕的代码:)

既然以前您不介意使用空字符串,为什么不只检查 var 值是否为 null?

$stmt->bindParam(':varvalue', (is_null($varvalue) ? '' $varvalue), PDO::PARAM_STR);

您可能应该为应用程序使用抽象,而不是直接使用 PDO。在抽象层中,可以执行要执行的任何类型转换。

如果这绝对不是一个选项,并且您必须直接使用 PDO,那么您可以尝试子类化和委派来生成执行所需类型转换的类似 PDO 工作的对象。但是,如果有任何列确实需要 null,则会使抽象层大大复杂化。您可能需要对数据库进行内省或其他一些技巧。您可能无法完全保留 PDO API。

PDOStatement代表团

这对bindValue来说非常简单。但是,bindParam使用引用,如果不重写它们,我们就无法对其进行类型转换,因此我们需要一种解决方法,在调用execute时将这些调用转换为bindValue调用。

首先,我们对PDO进行子类化,以便返回新的包装PDOStatement

class PDO_nullcast extends PDO {
    public function prepare($statement, $driver_options=array()) {
        $prepared = parent::prepare($statement, $driver_options);
        $delegated_prepared = new PDOStatement_nullcast($prepared);
        return $delegated_prepared;
    }
}

然后,我们创建一个具有空转换语义的委托PDOStatement_nullcast。我们的第一次尝试只会覆盖bindValue

class PDOStatement_nullcast {
    protected $pstmt;
    protected $bindparams; // this is for later
    function __construct(PDOStatement $pstmt) {
        $this->pstmt = $pstmt;
        $this->bindparams = array();
    }
    function __get($k) {
        return $this->pstmt->{$k};
    }
    function __set($k, $v) {
        $this->pstmt->{$k} = $v;
    }
    function __call($k, $a) {
        return call_user_func_array(array($this->pstmt, $k), $a);
    }
    function bindValue($parameter, $value, $data_type=PDO::PARAM_STR) {
        $newvalue = $this->castValue($value, $data_type);
        return $this->pstmt->bindValue($parameter, $newvalue, $data_type);
    }
    static public function castValue($val, $typehint) {
        $newval = $val;
        if ($val===NULL) {
            if ($typehint===PDO::PARAM_STR) {
                $newval = '';
            } else if ($typehint===PDO::PARAM_INT) {
                $newval = 0;
            } else if ($typehint===PDO::PARAM_BOOL) {
                $newval = false;
            }
        } else {
            if ($typehint===PDO::PARAM_STR) {
                $newval = (string) $val;
            } else if ($typehint===PDO::PARAM_INT) {
                $newval = (int) $val;
            } else if ($typehint===PDO::PARAM_BOOL) {
                $newval = (bool) $val;
            }
        }
        return $newval;
    }
}

下面是一些演示代码。我们将使用下表作为示例:

CREATE TABLE `typetest` (
  `intcol` int(11) NOT NULL,
  `strcol` varchar(255) NOT NULL,
  `intnullcol` int(11) DEFAULT NULL,
  `intstrcol` varchar(255) DEFAULT NULL,
)

现在是PHP代码。假设您有一个分配给$dbPDO_nullcast对象:

$sql = 'INSERT INTO typetest (`intcol`, `strcol`, `intnullcol`, `intstrcol`) VALUES (?,?,?,?)';
$insert = $db->prepare($sql);
$insert->bindValue(1, null, PDO::PARAM_INT);
$insert->bindValue(2, null, PDO::PARAM_STR);
$insert->bindValue(3, null, PDO::PARAM_INT);
$insert->bindValue(4, null, PDO::PARAM_STR);
$insert->execute();
$insert->closeCursor();
$select = $d->prepare('SELECT * FROM typetest');
$select->execute();
$res = $select->fetchAll();
$select->closeCursor();
var_dump($res);

您可以将castValue函数更改为所需的语义。

但是,这不会处理bindParam情况。在这里,我们需要在内部保留一个引用,直到在我们的包装器上调用execute,然后将这些引用转换为bindValue调用。但是,我们无法以这种方式处理bindParam的所有用途!INOUT 参数没有解决方法,因为我们无法通过类型转换保留引用。

我们可以拦截bindParam并像这样execute调用来获得我们想要的东西(将以下方法添加到上面的 PDOStatement_nullcast 类中):

function bindParam($parameter, &$variable, $data_type=PDO::PARAM_STR, $length=null, $driver_options=null) {
    if (isset($length) || isset($driver_options) || ($data_type & PDO::PARAM_INPUT_OUTPUT)) {
        // in either of these cases, we cannot wrap!
        return $this->pstmt->bindParam($parameter, $variable, $data_type, $length, $driver_options);
    }
    // note we preserve a reference to the variable
    $this->bindparams[] = array($parameter, &$variable, $data_type);
    return true; // this is a bit of a lie--we can't know if we would have an error until later.
}
function execute($input_parameters=null) {
    if ($input_parameters!==null) {
        return $this->pstmt->execute($input_parameters);
    }
    // for-loop is to preserve references more clearly
    // foreach is trickier
    for ($i=0; $i < count($this->bindparams); $i++) {
        call_user_func_array(array($this,'bindValue'), $this->bindparams[$i]);
    }
    return $this->pstmt->execute();
}

以下是一些使用 bindParam 的测试代码:

$var = null;
$insert->bindParam(1, $var, PDO::PARAM_INT);
$insert->bindParam(2, $var, PDO::PARAM_STR);
$insert->bindParam(3, $var, PDO::PARAM_INT);
$insert->bindParam(4, $var, PDO::PARAM_STR);
error_log($var);
$insert->execute();
$var = 1;
$insert->execute();
$var = 2;
$insert->execute();