General

汎用PDOエミュレータ

2008年11月17日

主に(な)さんへ(まだ、SkinabbleAdminでPDO使ってたらだけど)。

JeansでのPDOエミュレータを、バージョンアップした。SQLiteだけでなく、MySQLやpgSQLでの使用も視野に入れてある。

現在のJeans(v.2.1.0.4)では、次のように記述してある。まず、PDOクラスとPDOStatementクラスのエミュレーションから。それぞれ、sql_pdo及びsql_pdostatementクラスとして記述。

/*
 * General PDO emulator
 * Note that sql_bios must be defined for each SQL engines in sqlite_ord.php etc.
 * Note that sql_pdo class must be defined in sqlite.php, mysql.php etc 
 * if real PDO is used.
 */

class sql_pdo extends sql_bios {
    public function __construct($dsn,$user,$passwd){
        return $this->sql_open($dsn,$user,$passwd);
    }
    public function quote($data){
        return self::sql_quote($data);
    }
    public function __destruct(){
        core::shutdown($this);
    }
    public function beginTransaction(){
        return (bool)$this->sql_query('BEGIN;');
    }
    public function commit(){
        return (bool)$this->sql_query('COMMIT;');
    }
    /* Following methods are not impremented.
     * getAttribute,lastInsertId,rollBack,setAttribute
     */
    public function exec($query){
        return $this->sql_exec($query);
    }
    public function query($query, $fetchMode=false){
        $res= new sql_pdostatement($this, $this->db,$query);
        if ($fetchMode!==false) $res->setFetchMode($fetchMode);
        $res->execute();
        return $res;
    }
    public function prepare($query){
        return new sql_pdostatement($this, $this->db,$query);
    }
    public function errorCode($e=null){
        if ($e===null) $e=$this->sqlerror;
        switch($e){
            case 0:
                return '00000'; //Success
            default:
                return 'HY000'; //General error
        }
    }
    public function errorInfo($e=null){
        if ($e===null) $e=$this->sqlerror;
        return array($this->errorCode($e), $e, sql_error_string($e));
    }
}

class sql_pdostatement extends sql_bios {
    /*
     * Following methods are not implemented.
     * setAttributes, getAttribute, getColumnMeta, nextRowset
     */
    private $pdo, $query, $params=array(), $res=false;
    public function __construct(&$pdo, $db, $query){
        // Receive the database connection resource and prepared statement.
        // Note that "$query" is a prepared statement or a completed query.
        $this->pdo=&$pdo;
        $this->db=$db;
        $this->query=$query;
    }
    public function errorCode(){
        return $this->pdo->errorCode($this->sqlerror);
    }
    public function errorInfo(){
        return $this->pdo->errorInfo($this->sqlerror);
    }
    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->sql_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]="'".self::sql_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->sql_fetch($this->res,$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->sql_fetch($this->res,PDO::FETCH_BOTH)) return $row[$column_number];
        else return false;
    }
    public function columnCount(){
        return self::sql_columnCount($this->res);
    }
    public function rowCount(){
        return self::sql_rowCount($this->db);
    }
}

見てのとおり、SQLiteに特異的な関数のコールは排除してある。代わりに、sql_biosクラスを継承しすることで、そこで記述したsql_xxxメソッドを呼び出している。SQLiteの場合のsql_biosクラスは、次のように記述。
/*
 * Abstract class for PDO emulator
 * See also sql_pdo and sql_pdostatements classes in sql_pdo.php
 */

abstract class sql_bios {
    protected $db=false, $sqlerror=0;
    protected function sql_open($dsn,$user,$passwd){
        if ($this->db) return;
        $dsn=preg_replace('/^.*:/','',$dsn);
        if (!($this->db=sqlite_open($dsn))) core::exitWithError('SQLite connection error.');
    }
    protected function sql_quote($data){
        return sqlite_escape_string($data);
    }
    protected function sql_query($query){
        $ret=sqlite_query($this->db,$query);
        $this->sqlerror=sqlite_last_error($this->db);
        return $ret;
    }
    protected function sql_exec($query){
        $ret=sqlite_exec($this->db,$query);
        $this->sqlerror=sqlite_last_error($this->db);
        return $ret;
    }
    protected function sql_error_string($e_code){
        return sqlite_error_string($e_code);
    }
    protected function sql_columnCount($res){
        return sqlite_num_fields($res);
    }
    protected function sql_rowCount($db){
        return sqlite_changes($db);
    }
    protected function sql_fetch($res,$result_type=null){
        if ($result_type===null) $result_type=$this->fetchMode;
        switch($result_type){
            case PDO::FETCH_BOTH:
                return sqlite_fetch_array($res,SQLITE_BOTH);
            case PDO::FETCH_NUM:
                return sqlite_fetch_array($res,SQLITE_NUM);
            case PDO::FETCH_ASSOC:
                return sqlite_fetch_array($res,SQLITE_ASSOC);
            case PDO::FETCH_OBJ:
                return sqlite_fetch_object($this->res);
            default:
                return false;
        }
    }
}

使用上の注意点としては、文字列のクオートには必ずシングルクオート( ' )を利用すること。今回のバージョンでは、errorCode()とerrorInfo()に対応した。

コメント

佐藤(な) (2008年11月22日 08:18:31)

現状ではまだpdo_mysqlが入っていないサーバが結構あるようなので、pdoを使わない方向で進んでいましたが、これで心置きなくpdo書式でいけます。

MySQL版 sql_bios 作って試してみたいと思います。
ありがとうございます~。

Kat (2008年11月24日 18:08:38)

いえいえ。
よろしくお願いします。待ってますョ。

コメント送信