cakephp1.2からcakephp2系にあげて、エラーになったことと対処した内容です。
postgres関数からPDOに変わっていたこともあり、実装がかなり変わっています。
プリペアドステートメントに変わった影響
cakephp1.2は、変数を自前で変換してエスケープしてSQLを実行している実装になっていた。
cakephp2.10ではPDOのプリペアドステートメントを利用しているので、実装はDBのエンジンごとにまとまった実装になっていると思います。
いまのところエラーになった部分をいくつか対処した内容です。
queryの変数のパラメータで余計な内容はNG
このプログラムが1.2ではOKで、2系ではエラーになります。
test.php
$sql = "SELECT * FROM users WHERE id = :id ";
$params = [
'id' => 1
, 'company_id' => 2
];
$this->query($sql, $params);
理由としてはPDOの利用に代わり、PDO自体が余計なパラメータを許容していないからです。
むしろ便利にしていたのは1.2では自前でパラメータの部分を作成していたため、動作する仕様だったみたいです。
cake/libs/model/datasources/dbo_source.php
return $this->fetchAll(String::insert($args[0], $args[1]), $cache);
cake/libs/string.php
function insert($str, $data, $options = array()) {
$defaults = array(
'before' => ':', 'after' => null, 'escape' => '\\', 'format' => null, 'clean' => false
);
$options += $defaults;
$format = $options['format'];
if (!isset($format)) {
$format = sprintf(
'/(?<!%s)%s%%s%s/',
preg_quote($options['escape'], '/'),
str_replace('%', '%%', preg_quote($options['before'], '/')),
str_replace('%', '%%', preg_quote($options['after'], '/'))
);
}
if (!is_array($data)) {
$data = array($data);
}
if (array_keys($data) === array_keys(array_values($data))) {
$offset = 0;
while (($pos = strpos($str, '?', $offset)) !== false) {
$val = array_shift($data);
$offset = $pos + strlen($val);
$str = substr_replace($str, $val, $pos, 1);
}
} else {
asort($data);
$hashKeys = array_map('md5', array_keys($data));
$tempData = array_combine(array_keys($data), array_values($hashKeys));
foreach ($tempData as $key => $hashVal) {
$key = sprintf($format, preg_quote($key, '/'));
$str = preg_replace($key, $hashVal, $str);
}
$dataReplacements = array_combine($hashKeys, array_values($data));
foreach ($dataReplacements as $tmpHash => $data) {
$str = str_replace($tmpHash, $data, $str);
}
}
if (!isset($options['format']) && isset($options['before'])) {
$str = str_replace($options['escape'].$options['before'], $options['before'], $str);
}
if (!$options['clean']) {
return $str;
}
return String::cleanInsert($str, $options);
}
対処
パラメータを条件関係なくいれれたから楽だったんですが、変わったのは仕方がないです。
対処としては、呼び出しているところでパラメータを必要な時だけパラメータにする。
ただ呼び出し箇所が多かったので、ソースで対処してみました。
lib/Cake/Model/DataSource/DboSource.php
protected function _execute($sql, $params = array(), $prepareOptions = array()) {
$sql = trim($sql);
if (preg_match('/^(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX)/i', $sql)) {
$statements = array_filter(explode(';', $sql));
if (count($statements) > 1) {
$result = array_map(array($this, '_execute'), $statements);
return array_search(false, $result) === false;
}
}
try {
$query = $this->_connection->prepare($sql, $prepareOptions);
$query->setFetchMode(PDO::FETCH_LAZY);
if (!$query->execute($this->_executeParam($sql, $params))) {
$this->_result = $query;
$query->closeCursor();
return false;
}
if (!$query->columnCount()) {
$query->closeCursor();
if (!$query->rowCount()) {
return true;
}
}
return $query;
} catch (PDOException $e) {
if (isset($query->queryString)) {
$e->queryString = $query->queryString;
} else {
$e->queryString = $sql;
}
throw $e;
}
}
protected function _executeParam( $sql, $params ){
if(empty($params)) return $params;
if(!is_array($params)) return $params;
// ソート
uksort($params, function($a, $b){
$al = mb_strlen($a);
$bl = mb_strlen($b);
return $al >= $bl ? ($al == $bl ? 0 : -1 ) : 1;
});
$ret = [];
foreach($params as $key => $param){
if( is_numeric($key) ) {
$ret[$key] = $param;
continue;
}
if( strpos ($sql, ":".$key ) !== false) {
$ret[$key] = $param;
}
}
if(empty($ret)) return[];
return $ret;
}
単純に文字列からパラメータがある場合にのみ、パラメータとして渡すように変更してみました。
一応動作しています。