平成30/ 2018-12-10 16:42
www/lib/db.php 2017.02.10.00


#############################
# 前回との違い

(1) connect() 削除した、conn() 作り直した。
(2) 利用していない関数を削除した。
(3) conn() の、pgsql でPort指定していないケースを追記した。
(4) Default のポートを削除した。
(5) conn() の、return を、true or false にした。
(6) query(), placeholder() を、mysql, postgreSql で処理を分けないようにした。
(7) 利用例を追記した。



#############################
# 利用例

Db::conn(array( "name" => DB_NAME, "usr"  => DB_USR, "pw" => DB_PW,));

$sql = 'select id, name from sample where name like ?';
$vals = array('test');
$res = array();
if( !Db::placeholder($sql, $vals)):
    print_r(array(Db::errs(), Db::placeholderSql($sql, $vals)));
else:
    while($r = Db::fetch()):
        $res[] = $r;
    endwhile;
endif;
print_r($res);

~~~~
$sql = 'select id, name from sample where name like '. Db::quote('test');
$res = array();
if( !Db::query($sql)):
    print_r(array(Db::errs(), $sql));
else:
    while($r = Db::fetch()):
        $res[] = $r;
    endwhile;
endif;



#############################

<?php
class Db{
    static public $type    = "mysql";
    static public $name    = "";
    static public $usr     = "";
    static public $pw      = "";
    static public $host          = "localhost";
    static public $charset       = "utf8";
    static public $charbyte      = "3";     # 3byteなら、4byte対応する。(MySqlのcharsetが utf8mb4 なら4byte)
    static public $res  = 0;
    static public $conn,$errs,$port; # Mysql Default Port is 3306, Postgres Default is 5432




    # 接続情報を返す
    #
    # self::$db = Db::conn(array('host'=>'','name'=>'','port'=>'','usr'=>'','pw'=>''));
    #
    public static function conn($_=null){

        # 接続情報を取得できるようにする(別の接続して元に戻したい場合を考慮)
        if(!isset($_)):
            return isset(self::$conn)?(self::$conn):false;
        endif;

        foreach(array('port','type','host','charset') as $k):
            if(empty($_[$k]))   : $_[$k] = self::${$k}; endif;
        endforeach;
        //self::$vals = $_;


        try{ # 例外処理しないとConnectに失敗したときにアカウントが表示されてしまう

            $dsn = 'host='. $_["host"].';dbname='. $_["name"].
                   (empty($_['port'])?'':";port=".$_["port"]);

            if( $_['type'] == 'pgsql' ):
                $dsn = 'pgsql:'.$dsn;
                $conn = new PDO( $dsn, $_["usr"], $_["pw"]);

                $sql = 'set names '. self::quote($_['charset']);

                if(self::pgsqlQuery($sql, $conn)):
                    self::$conn = $conn;
                    return true;
                endif;
                throw new Exception('');

            else:
                $opt = array(
                    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES '.$_["charset"],
                    PDO::MYSQL_ATTR_USE_BUFFERED_QUERY  => false,    // バッファしない命令。バッファしない分、速くなる。バッファしないので使い回しできなくなる。
                    PDO::ATTR_EMULATE_PREPARES  => true,             // これやらないとシングルクウォートでのエスケープがうまく動かない。なので必要
                );
                $dsn = 'mysql:'.$dsn;
                self::$conn = new PDO( $dsn, $_["usr"], $_["pw"], $opt);
                return true;
            endif;
        } catch (PDOException $e) {
            return false;
        }
    }


    // 更新し、実際に更新された件数を返す
    // 例えば、UPDATEの構文は間違っていなくても、更新前の同じデータの場合は実際には更新されないので「0」が返る
    // ERRORを取得したい場合は「DB::errs()」の有無で判断する。
    public static function exec($sql){
        self::$errs = null;
        if($re = DB::$conn->exec($sql)):
            return $re;
        endif;

        if( self::errs()):
            return false;
        endif;
        return true;
    }
/* 使い方
if( DB::exec($sql) === false ){
    Debug::mailto(array("body"=>array(DB::errs())));
} else{
    // OK.
}
*/


    public static function pgsqlQuery($_sql, $conn){
        # ATTR_CURSOR = CURSOR_FWDONLY は、1回のみ取得という意味でDefaultみたい。何度も取得したいときに、これとは違うパラメータを指定する必要あるとのこと。
        #
        # self::$res = $conn->prepare($_sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));

        self::$res = $conn->prepare($_sql);
        self::$res->execute();
        self::$errs = null;
        return (self::errs())?false:true;
    }



    public static function query($_sql, $conn=null){
        if(!isset($conn)): $conn = self::$conn; endif;

        // PDO を利用している場合
        if( isset($conn)):
            DB::$res = self::$conn->query($_sql);
        else:
            DB::$res = mysql_query($_sql);
        endif;
        self::$errs = null;
        return !(self::errs());

        // query()は、SQL文に誤りがなければ、エラーは返ってこない。
        // 例えば、存在しないデータに対してUPDATEしたとしても、SQL文に誤りがなければエラーではない。
    }


    // プレースホルダ
    public static function placeholder($_sql, $_execute=null){
        // PDO を利用している場合
        self::$errs = null;
        if( isset(self::$conn)):

            # ATTR_CURSOR = CURSOR_FWDONLY は、1回のみ取得という意味でDefaultみたい。何度も取得したいときに、これとは違うパラメータを指定する必要あるとのこと。
            #
            # self::$res = $conn->prepare($_sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
            #
            self::$res = DB::$conn->prepare($_sql);

            if(isset($_execute)):
                $_execute = self::adjustExecute($_sql, $_execute);
                self::$res->execute($_execute);
            else:
                self::$res->execute();
            endif;
            return !(self::errs());
        else:
            self::$errs = array('PDO Not Connect!');
            return false;
        endif;
    }
    # placeholder() のSQL文確認用
    public static function placeholderSql($_, $_execute){
        return self::sql($_, $_execute);
    }

    public static function adjustExecute($_sql, $_execute){
        $_sql = trim($_sql);

        # DB が 3byte のときは utf8 4byte を、3byte に置換する
        # utf8(4byte) 文字(絵文字など)を、? に変換する。(MYSQL TABLE is charset=utf8 の場合は変換しないとエラーでる)
        # SELECT文以外の場合のみ(utf8(4byte)の問題はSELECT文では発生しない)
        #
        if(!preg_match('/^select/ui', $_sql) and
            self::$charbyte==3 and
            self::$charset=='utf8' and
            method_exists('Fn', 'utf8423')
        ):
            foreach($_execute as $k => $v):
                $_execute[$k] = Fn::utf8423($v);
            endforeach;
        endif;
        return $_execute;
    }



    public static function errs(){
        if(isset(self::$errs)): return self::$errs; endif;
        self::$errs = array();
        if( !isset(self::$conn)):
            self::$errs = array( mysql_error());
        else:
            self::$errs = self::$res->errorInfo();
            if(empty(self::$errs[2])): self::$errs = array(); endif; # [2]に値が無ければエラー無し
        endif;
        return self::errs();
    }


    public static function fetch(){
        if( !isset(DB::$conn)):
            return ($tmp=mysql_fetch_assoc(DB::$res))?$tmp:false;
        endif;
        # TIMEOUTなどで、値取得できなかった場合に用途によっては、実際に事象は確認できなかったのだが、無限ループになったケースがあったので、条件分岐いれる
        #
        return ($tmp=DB::$res->fetch(PDO::FETCH_ASSOC))?$tmp:false;
    }
    public static function fetchAll(){
        $li = DB::$res->fetchAll(PDO::FETCH_ASSOC);
        return isset($li[0])?$li[0]:array();
    }



    public static function quote($str){
        if(!isset($str)): return 'null'; endif;

        if( !isset(self::$conn)):
            return "'". mysql_real_escape_string($str). "'";
        endif;
        return self::$conn->quote($str);
        //(quote() は、きっと、1バイトごとにエスケープ文字(円マーク、バックスラッシュ)をいれている。なので、UTF8の全角を、quote() すると文字化ける。しかし、DB側だと、うまく処理できるのだと思う。気を付けなければいけないのは、quote() した後に、preg_replace() などで文字を処理すると、UTF8文字が判別できず戻り値が取得できないケースが発生する)
    }

    // カラム名に危険な文字が存在する場合は空を返す
    public static function fieldQuote($_){
        return preg_match("/^[a-z0-9_]+$/ui", $_)?$_:'';
    }

    public static function insertId(){
        if( !isset(DB::$conn)):
            return mysql_insert_id();
        endif;
        // エラーは戻り値は無いみたい。
        return DB::$conn->lastInsertId();
    }


    # transaction() して、rollback() or commit() して、再び、rollback() などしたいときは、再び transaction() しないといけません。
    public static function transaction(){
        if( self::$conn->beginTransaction()):
            return true;
        endif;
        self::$errs = null;
        return false;
    }
    public static function commit(){
        if( DB::$conn->commit()):
            return true;
        endif;
        self::$errs = null;
        return false;
    }
    public static function rollback(){
        if( DB::$conn->rollback()):
            return true;
        endif;
        self::$errs = null;
        return false;
    }
    public static function fetchClose(){
        // バッファをOFFにしている場合は、fetch()を空にせず次のSQLを実行するとエラーがでる。その場合は、closeCursor()する。
        DB::$res->closeCursor();
    }

    public static function column($_){
        if(
        empty($_)
        ){
            return array();
        }
//echo DB::$dbname;
//exit;

        $table_schema = DB::quote(DB::$dbname);
        $table_name   = DB::quote($_);

        $sql = "select column_name from  information_schema.columns where table_schema={$table_schema} and table_name = {$table_name}";
        $li = array();

        if(!Db::query($sql)):
        else:
            while($r = DB::fetch()){
                $li[$r["column_name"]] = $r["column_name"];
            }
        endif;
        return $li;
    }



    # 利用方法
    # プレースホルダに切り替えられるよう、「?」で置換できるようにする
    # $sql = DB::sql("
    #         select * from sample
    #         where name = ? and name2 like ?
    #     ",
    #     シングルクォーテーションで囲まれる前提で記述すること
    #     array(
    #         "shimizu", "%株式%"
    #     ),
    # );
    public static function sql($_tmpl, $_execute = null){
        $tmpl = trim($_tmpl);

        $values = array();
        if(isset($_execute) and is_array($_execute)):
            $_execute = self::adjustExecute($_tmpl, $_execute);

            # vsprinf()で置換できるよう、「%s」に変換する
            #
            $tmpl = preg_replace('/(\(\s*|,\s*|=\s*|in\s*\(\s*|like\s*|>\s*|<\s*|\+\s*)\?/ui', '$1%s', $_tmpl);
            $tmpl = preg_replace('/(\slimit\s+)\?(\s*,\s*)\?(\s|$)/ui', '$1%s$2%s$3', $tmpl);
            #
            # 以下のケースを増やして行く方が安全。
            # -
            # =?
            # in(?)
            # like ?
            # limit ?, ?
            # (?,?,?)
            # > ?
            # + ?

            foreach($_execute as $k => $v){
                if(
                    is_array($v)
                ){
                    foreach($v as $kk => $vv){
                        $values[$k][$kk] = self::quote($vv);
                    }
                    $values[$k] = implode(",", $values[$k]);
                    continue;
                }
                $values[$k] = DB::quote($v);
            }
            $sql = vsprintf($tmpl, $values);
        else:
            $sql = $tmpl;
        endif;



        // LIMIT 指定の場合は値が数値でなければエラーとする
        if(
            preg_match("/\slimit\s+%(s|d)\s*,\s*%(s|d)/ui", $tmpl)
        ){

            // CGI/FastCGI の環境で(これが原因とは分からないが) quote 後の、preg_replace('//u') で戻り値が無くなってしまう現象があった為、修飾子の「u」を使わないようにする
            $tmp = explode("limit", $sql);
            $end = end($tmp);
            $end = preg_replace("/'([0-9]+)'(\s*,\s*)'([0-9]+)'/", "$1$2$3", $end); // この時点では、UTF8 文字は含まれていない
            $tmp[key($tmp)] = $end;
            $sql2 = implode("limit", $tmp);


            // preg_replace() が機能するならば上記より以下がシンプル
            // 置換できなければエラー
            // $sql2 = preg_replace("/(\slimit\s+)'([0-9]+)'(\s*,\s*)'([0-9]+)'/ui", "$1$2$3$4", $sql);


            if(
                $sql == $sql2
            ){
                Debug::mailto(array("body"=>$sql, "file"=>__FILE__, "line"=>__LINE__));
                return '';
            }
            $sql = $sql2;
        }



        # quote() 側で utf8(4byte)対応するようにした
        #
        #if(
        #    !preg_match("/^select /ui", $tmpl)
        #){
        #    // SELECT文以外の場合のみ(utf8(4byte)の問題はSELECT文では発生しない)
        #    // utf8(4byte) 文字(絵文字など)を、? に変換する。(MYSQL TABLE is charset=utf8 の場合は変換しないとエラーでる)
        #    $sql = utf8_4To3($sql);
        #}
        return $sql;
    }


    public static function foundRows(){
        $sql = DB::sql(array("tmpl"=>"select found_rows() as ct","values"=>array(),));
        $re = 0;
        if(
            !DB::query($sql)
        ){
        } else{
            while($r = DB::fetch()){
                $re = $r["ct"];
            }
        }
        return $re;
    }

    public static function type(){
        return DB::$type;
    }
}