非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