3

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

以前,我们将所有变量都转义为字符串。例如,

SET varname = '$varvalue'

现在,有了 PDO,我当然愿意

SET varname = :varvalue

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

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

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

4

3 回答 3

2

这可能不是您正在等待的答案,但应该提到:使用异常。

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

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

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

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

于 2012-08-21T09:07:53.647 回答
1

既然以前你不介意有空字符串,为什么不检查 var 值是否为空呢?

$stmt->bindParam(':varvalue', (is_null($varvalue) ? '' $varvalue), PDO::PARAM_STR);
于 2012-08-17T21:43:21.277 回答
1

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

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

PDOStatement代表团

这对于bindValue. 但是,bindParam使用引用并且我们不能在不重写它们的情况下对它们进行类型转换,因此我们需要一种解决方法,在bindValue调用时将它们转换execute为调用。

首先我们子类化PDO,所以它返回我们新的 Wrapped 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 代码。假设您有一个PDO_nullcast对象分配给$db

$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 参数没有解决方法,因为我们无法通过类型转换保留引用。

我们可以像这样拦截bindParamexecute调用来得到我们想要的(在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();
于 2012-08-18T00:42:59.087 回答