General

非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環境のサポートは、後ほど。それほど難しくないはずだが、テスト用のデータベース及びテーブルを作成しないといけないので、後回しにした。
<?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

コメント送信