非PDO環境のサポート
2008年10月26日
2ヵ月半ぶりの更新。ここのところ、コードをガリガリ書くよりも、基本部分の仕様を決めるための思考実験が続いている。また後日記事を書くが、どうやらスキンに関して、Nucleusに対する上位互換性の維持は切り捨てることになりそう。ただし、完全に切り捨てるのではなく、スキン変数の文法をNucleusと同じにはしないということ。Nucleusのスキンを取り込むときには、専用のプラグインを利用し、このプラグインがNucleusのスキンをJeansのスキンに翻訳して取り込むことで、互換性を確保する予定。
さて、本題。PDOは、PHP 5.1 以降でサポートされている。Jeansは、PHP 5.2以降で動作するように作成するつもり(もしかしたら、5.3以降になる可能性もあり)だから、当然PDOをサポートしているPHPが対象になる。
で、表題の『非PDO環境のサポート』であるが、なぜこんなことが必要かというと、PDOはサポートしていても、PDO-SQLiteのみでPDO-MySQLなどがサポートされていない環境が結構あるようだというのが理由。そういう環境でもJeansでPDOをエミュレートして、通常のMySQL/SQLite/pgSQLなどで使えるようにしようというのが、目的である。
とりあえず、PDOでないSQLiteに対応するように書いてみた。jeans/misc/ディレクトリに、sqlite_ord.phpの名でPHPファイルを用意し、ここにすべてを記述してある。config.phpで、『define('_CONF_DB_TYPE', 'sqlite_ord')』とすれば使えるようになる。『ord』は、『ordinary』の略。ならPDOが、extraordinaryなのかどうかは、置いておいて…。
PDOのヘルプを見ながら作ったが、すべての機能を実装するのは骨が折れるし、自分自身すべてを使うわけではない。なので、機能を絞り込んで実装した。結果として、約200行の長さに収まったから、セキュリティー対策やデバッグもしやすいだろうと思っている。
以下、設けた制約をここにピックアップしてみる。ここに述べたことは、イコール、Jeansの規約にもなる(もしコアやプラグインのコードが、すべてのJeans動作環境に対応しようとするのであればだが)。
1)以下のPDOメソッドは、実装しない
・PDO::getAttribute
・PDO::lastInsertId
・PDO::rollBack
・PDO::setAttribute
2)以下のPDOメソッドは、未実装
・PDO::errorCode
・PDO::errorInfo
3)以下のPDOStatementメソッドは、実装しない
・PDOStatement::setAttributes
・PDOStatement::getAttribute
・PDOStatement::getColumnMeta
・PDOStatement::nextRowset
4)以下のPDOStatementメソッドは、未実装
・PDOStatement::errorCode
・PDOStatement::errorInfo
5)result_typeは、次のものだけが有効
・PDO::FETCH_BOTH
・PDO::FETCH_NUM
・PDO::FETCH_ASSOC
・PDO::FETCH_OBJ
2)及び4)については、後のバージョンで実装予定。ただし、SQLSTATEエラーコードについては、実装しないか、すべて同じエラーコードを返す簡易実装になる可能性が高い。
それと、これは非PDO環境に限った話ではないが、文字列は必ずシングルクオート『'』ではさむのが、Jeansの規約である。これは、MySQL/SQLite/pgSQLの3つの環境で動作するようにするため。
以下、sqlite_ord.phpのソースコード。このコードの後半部分、『General functions follow』の部分は、SQLite/MySQL/pgSQL共通のコードなので、後のバージョンでは外に出して、継承もとの親クラスにする予定。非PDOのMySQL環境のサポートは、後ほど。それほど難しくないはずだが、テスト用のデータベース及びテーブルを作成しないといけないので、後回しにした。
さて、本題。PDOは、PHP 5.1 以降でサポートされている。Jeansは、PHP 5.2以降で動作するように作成するつもり(もしかしたら、5.3以降になる可能性もあり)だから、当然PDOをサポートしているPHPが対象になる。
で、表題の『非PDO環境のサポート』であるが、なぜこんなことが必要かというと、PDOはサポートしていても、PDO-SQLiteのみでPDO-MySQLなどがサポートされていない環境が結構あるようだというのが理由。そういう環境でもJeansでPDOをエミュレートして、通常のMySQL/SQLite/pgSQLなどで使えるようにしようというのが、目的である。
とりあえず、PDOでないSQLiteに対応するように書いてみた。jeans/misc/ディレクトリに、sqlite_ord.phpの名でPHPファイルを用意し、ここにすべてを記述してある。config.phpで、『define('_CONF_DB_TYPE', 'sqlite_ord')』とすれば使えるようになる。『ord』は、『ordinary』の略。ならPDOが、extraordinaryなのかどうかは、置いておいて…。
PDOのヘルプを見ながら作ったが、すべての機能を実装するのは骨が折れるし、自分自身すべてを使うわけではない。なので、機能を絞り込んで実装した。結果として、約200行の長さに収まったから、セキュリティー対策やデバッグもしやすいだろうと思っている。
以下、設けた制約をここにピックアップしてみる。ここに述べたことは、イコール、Jeansの規約にもなる(もしコアやプラグインのコードが、すべてのJeans動作環境に対応しようとするのであればだが)。
1)以下のPDOメソッドは、実装しない
・PDO::getAttribute
・PDO::lastInsertId
・PDO::rollBack
・PDO::setAttribute
2)以下のPDOメソッドは、未実装
・PDO::errorCode
・PDO::errorInfo
3)以下のPDOStatementメソッドは、実装しない
・PDOStatement::setAttributes
・PDOStatement::getAttribute
・PDOStatement::getColumnMeta
・PDOStatement::nextRowset
4)以下のPDOStatementメソッドは、未実装
・PDOStatement::errorCode
・PDOStatement::errorInfo
5)result_typeは、次のものだけが有効
・PDO::FETCH_BOTH
・PDO::FETCH_NUM
・PDO::FETCH_ASSOC
・PDO::FETCH_OBJ
2)及び4)については、後のバージョンで実装予定。ただし、SQLSTATEエラーコードについては、実装しないか、すべて同じエラーコードを返す簡易実装になる可能性が高い。
それと、これは非PDO環境に限った話ではないが、文字列は必ずシングルクオート『'』ではさむのが、Jeansの規約である。これは、MySQL/SQLite/pgSQLの3つの環境で動作するようにするため。
以下、sqlite_ord.phpのソースコード。このコードの後半部分、『General functions follow』の部分は、SQLite/MySQL/pgSQL共通のコードなので、後のバージョンでは外に出して、継承もとの親クラスにする予定。非PDOのMySQL環境のサポートは、後ほど。それほど難しくないはずだが、テスト用のデータベース及びテーブルを作成しないといけないので、後回しにした。
<?php
/*
* The basic class for SQLite.
* Supports the non-PDO environment
*/
abstract class sql_base {
public final function initPDO(){
if ($this->cfok=method_exists($this->pdo,'sqliteCreateFunction')) {
// The callback function in SQL query, php is disabled.
// When a callback function is needed, it must be registered by sqlite_create_function().
// This way isn't supported by sqlite2.
$this->pdo->sqliteCreateFunction('php','pi');
}
}
public final function _vacuum(){
foreach(array(
'PRAGMA default_synchronous = FULL',
'PRAGMA short_column_names = 1') as $query) {
$this->pdo->query($query);
}
return $this->pdo->query('VACUUM');
}
public final function checkQuery(&$query){
// Modification of query is required to use the user function for sqlite2
if (!$this->cfok && count($this->funcsearch)) $this->modifyQuery($query);
// Cannot use ';' (the last one is OK).
$q=preg_replace('/;[\s]*$/','',$query);
if (strpos(';',preg_replace("/'[^']*'/",'',$q))===false) return $query;
exit('Cannot use more than two queries at once.');
}
// User function support stuffs follow.
private $cfok=false;
private $funcsearch=array();
private $funcreplace=array();
public final function createFunction($funcname, $userfunc){
if (!is_array($userfunc)) return false; // Cannot use global function.
list($class,$func)=$userfunc;
if (is_object($class)) return false; // Can only use static method.
foreach (array($funcname,$class,$func) as $value) {
if (preg_match('/[^a-zA-Z0-9_]/',$value)) return false;
}
if ($this->cfok) {
// sqlite3
$this->pdo->sqliteCreateFunction($funcname,$userfunc);
} else{
// sqlite2
$search="/^(([^']|'[^']*')*)([^a-z0-9_])".$funcname.'[\s]*\(/i';
if (array_key_exists($search,$this->funcsearch)) return true;
$this->funcsearch[]=$search;
$this->funcreplace[]="$1$3php('$class::$func'";
}
return true;
}
private function modifyQuery(&$query){
$search=$this->funcsearch;
$replace=$this->funcreplace;
$org=-1;
while($org!=$query) {
$org=$query;
$query=preg_replace($search,$fplace,$query);
}
}
}
class sql_pdo {
private $db=false;
public function __construct($dsn,$user,$passwd){
if ($this->db) return;
$dsn=preg_replace('/^.*:/','',$dsn);
if (!($this->db=sqlite_open($dsn))) core::exitWithError('SQLite connection error.');
}
public function __destruct(){
core::shutdown($this);
}
public function beginTransaction(){
return (bool)sqlite_query($this->db,'BEGIN;');
}
public function commit(){
return (bool)sqlite_query($this->db,'COMMIT;');
}
/* Following methods are not impremented.
* errorCode,errorInfo,getAttribute,lastInsertId,rollBack,setAttribute
*/
public function quote($data){
return sqlite_escape_string($data);
}
public function exec($query){
sqlite_exec($this->db,$query);
return sqlite_changes($this->db);
}
public function query($query){
return new sql_pdostatement($this->db,$query);
}
public function prepare($query){
return new sql_pdostatement($this->db,$query);
}
}
class sql_pdostatement {
/*
* SQLite specific functions follow
*/
private function _query($query){
return sqlite_query($this->db,$query);
}
private function _fetch($result_type=null){
if ($result_type===null) $result_type=$this->fetchMode;
switch($result_type){
case PDO::FETCH_BOTH:
return sqlite_fetch_array($this->res,SQLITE_BOTH);
case PDO::FETCH_NUM:
return sqlite_fetch_array($this->res,SQLITE_NUM);
case PDO::FETCH_ASSOC:
return sqlite_fetch_array($this->res,SQLITE_ASSOC);
case PDO::FETCH_OBJ:
return sqlite_fetch_object($this->res);
default:
return false;
}
}
private function columnCount(){
return sqlite_num_fields($this->res);
}
private function rowCount(){
return sqlite_changes($this->db);
}
/*
* Following methods are not implemented.
* setAttributes, getAttribute, getColumnMeta, errorCode, errorInfo, nextRowset
*/
/*
* General functions follow
*/
private $db, $query, $params=array(), $res=false;
public function __construct($db,$query){
// Receive the database connection resource and prepared statement.
// Note that "$query" is a prepared statement or a completed query.
$this->db=$db;
$this->query=$query;
}
public function execute($params=false){
if ($params) {
foreach($params as $key=>$value){
if (is_int($key)) $this->params[(string)($key+1)]=$value;
else $this->params[$key]=$value;
}
}
// Construct the query from prepared statement and parameters.
if (0<$this->execute_cb()) {// Initialize the static values
$query=preg_replace_callback("/([^'\?]*|\?|'[^']*')/",array($this,'execute_cb'),$this->query);
} else $query=$this->query;
return (bool)($this->res=$this->_query($query));
}
private function execute_cb($matches=false){
static $search,$replace,$repbynum,$num;
if (!$matches) {
krsort($this->params); // To avoid maching longer name by shorter name, for example, :abc vs :abcde
// Initialize static values.
$search=$replace=$repbynum=array();
foreach($this->params as $key=>$value) {
$search[]=$key;
$replace[]=$repbynum[$key]="'".sql_pdo::quote($value)."'";
}
$num=0;
return count($search);
}
switch($matches[0]{0}){
case "'": // Inside the string.
return $matches[0];
case '?': // '?' used.
$num++;
return $repbynum[(string)$num];
default: // ':xxx' may be used.
return str_replace($search,$replace,$matches[0]);
}
}
public function bindParam($key, &$var){
$this->params[$key]=&$var;
}
public function bindValue($key, $value){
$this->params[$key]=$value;
}
private $fetchMode=PDO::FETCH_BOTH;// Either both, num, assoc, or obj
public function setFetchMode($fetchMode){
switch($fetchMode){
case PDO::FETCH_BOTH:
case PDO::FETCH_NUM:
case PDO::FETCH_ASSOC:
case PDO::FETCH_OBJ:
break;
default:
return false;
}
$this->fetchMode=$fetchMode;
return 1;
}
private $bindColumnData=array();
public function bindColumn($column, &$param){
$this->bindColumnData=&$param;
return true;
}
public function closeCursor(){
return true;
}
/*
* fetch methods follow
*/
public function fetch($fetch_style=false){
if ($fetch_style===false) $fetch_style=$this->fetchMode;
$ret=$this->_fetch($fetch_style);
foreach($this->bindColumnData as $key=>&$value){
if (is_object($ret)) $value=$ret->$key;
else $value=$ret[$key];
}
return $ret;
}
public function fetchObject(){
return $this->fetch(PDO::FETCH_OBJ);
}
public function fetchAll($fetch_style=false){
$ret=array();
while($row=$this->fetch($fetch_style)) $ret[]=$row;
return $ret;
}
public function fetchColumn($column_number=0){
if ($row=$this->_fetch(PDO::FETCH_BOTH)) return $row[$column_number];
else return false;
}
}コメント
Kat (2008年10月26日 15:23:21)
SQLSTQTEは、次の3つだけで処理するのが手か?
00000 Success
01000 General warning
HY000 General error
http://msdn.microsoft.com/ja-jp/library/aa937531(SQL.80).aspx
00000 Success
01000 General warning
HY000 General error
http://msdn.microsoft.com/ja-jp/library/aa937531(SQL.80).aspx