<?php
/* =================================================================================
 * License: GPL-2.0 license
 * Author: 众产®  https://ciy.cn/code
 * Version: 0.6.11
 * =================================================================================*/

/**
 * 应用数据层类库（单服版）
 * 注：单数据库服务器请用单服版，效率较高。多数据库服务器使用多服版。
 * 1、type: mysql     使用MySQLi引擎 (不再支持)
 *    type: pdo       使用PDO引擎
 *    type: http      使用http透传（不再支持）
 * 
 * 2、mode: db.php    [单服版]
 *          dbns.php  [多服版] 读写分离，一主多从模式。
 *          dbms.php  [多服版] 多写多读模式。从注册中心获取一台空闲读服务器。
 * 
 * getone       获取一条数据。          无数据返回null      出错返回false   判断is_array，确认数据有效
 * get1         获取第一行第一列数据    无数据返回null      出错返回false
 * get          获取数据集合。          无数据返回array()   出错返回false
 * getraw       原始方法获取数据集合。  无数据返回array()   出错返回false
 * insert       新增数据。              成功返回影响行数    出错返回false
 * update       更新数据。              成功返回影响行数    出错返回false
 * delete       删除及备份数据。        成功返回影响行数    出错返回false
 * execute      执行SQL语句。           成功返回影响行数    出错返回false
 * begin        开始事务                成功返回true        出错返回false
 * commit       事务提交                成功返回true        出错返回false
 * rollback     事务回滚                成功返回true        出错返回false
 * tran         事务回调                成功返回true        出错返回false
 * getfield     获取表字段注解          返回字段名称字符串，引用返回注解
 * getdbcodes   获取表内某字段代码数组  无数据返回array()
 * insert_id    获取刚新增数据的id
 * errdata      错误集中接收后返回
 * 
 * 变量名取名尽量用s尾缀区分，建议getone函数使用$xxrow，get函数使用$xxrows。
 */

namespace ciy;

class db { //单数据库版
    public $linkdb; //写服务器驱动层类实例。
    public $dbpst;
    public $error;
    public $pf;
    public $lock;

    function __construct($dbpst = '') {
        $this->linkdb = false;
        $this->dbpst = $dbpst;
        $this->error = '';
        $this->lock = false;
    }
    /**
     * 连接到服务器。
     */
    function connect() {
        if ($this->linkdb !== false)
            return $this->linkdb;
        $cfg = webini('db' . $this->dbpst);
        if (is_string($cfg))
            die($cfg);
        $this->linkdb = new pdo($cfg, $this->pf);
        $this->linkdb->connect();
        return $this->linkdb;
    }
    /**
     * 获取一条数据。
     * 返回array()，无数据返回null，出错返回false
     * csql         SQL拼接类
     * 例：
     * $csql = new \ciy\sql();
     * $csql->...
     * $row = $mydata->getone($csql);
     * if(is_array($row))
     *      $row['username']...
     */
    function getone($csql) {
        $limit = $csql->limit;
        if (empty($limit))
            $limit = ' limit 1';
        if ($this->lock)
            $limit .= ' for update';
        $ret = $this->connect()->get(2, $csql->buildsql() . $limit, $csql->tsmt);
        if ($ret === false || $ret === null)
            return $this->errdata($ret);
        return $ret;
    }

    /**
     * 获取一条数据的单个字段。
     * 返回变量，无数据返回null，出错返回false
     * csql         SQL拼接类
     * 例：
     * $csql = new \ciy\sql();
     * $usercount = toint($mydata->get1($csql));
     * $csql->column('name');
     * $username = $mydata->get1($csql);
     */
    function get1($csql) {
        if ($csql->column == '*')
            $csql->column = 'count(*)';
        $limit = $csql->limit;
        if (empty($limit))
            $limit = ' limit 1';
        if ($this->lock)
            $limit .= ' for update';
        $ret = $this->connect()->get(3, $csql->buildsql() . $limit, $csql->tsmt);
        if ($ret === false || $ret === null)
            return $this->errdata($ret);
        return $ret;
    }
    /**
     * 获取数据集合。
     * 返回array()，出错返回false
     * 判断is_array，确认数据有效
     * csql         SQL拼接类
     * pageno       第pageno页，不分页传null，默认null
     * pagecount    每页pagecount条，默认20
     * rowcount     返回总数据条数(数据量大，查询耗时较高)，不填不查询。
     * 例：
     * $rows = $mydata->get($csql);//查询全部数据
     * $rows = $mydata->get($csql,1,$pagecount,$rowcount);
     * if(is_array($rows)){
     * foreach($rows as $row)
     * $row['username']...
     * }
     */
    function get($csql, &$rowcount = 0) { //$pageno=null 查询全部
        $sql = $csql->buildsql();
        $limit = $csql->limit;
        if ($this->lock)
            $limit .= ' for update';
        $ret = $this->connect()->get(1, $sql . $limit, $csql->tsmt);
        if ($ret === false)
            return $this->errdata($ret);
        if ($rowcount == -1) {
            $column = $csql->column;
            $csql->column = 'count(*)';
            $rowcount = toint($this->connect()->get(3, $csql->buildsql(), $csql->tsmt));
            $csql->column = $column;
        }
        return $ret;
    }
    /**
     * 原始参数获取数据集合。
     * 返回array()，出错返回false
     * sql          SQL prepare字符串
     * tsmt         array ?对应数据集
     * 例：
     * $row = $mydata->getraw('select * from xxx where id=?',[3]);
     */
    function getraw($sql, $tsmt = array()) {
        $ret = $this->connect()->get(1, $sql, $tsmt);
        if ($ret === false)
            return $this->errdata($ret);
        return $ret;
    }
    function getfield(&$field, $table) {
        $fieldrows = $this->connect()->get(1, 'show full fields from ' . $table, array());
        if ($fieldrows === false)
            return $this->errdata($fieldrows);
        $sorts = array();
        foreach ($fieldrows as $fr) {
            if (!empty($fr['Comment']))
                $sorts[] = $fr['Field'];
            $field[$fr['Field']] = array('c' => $fr['Comment']);
        }
        return implode(',', $sorts);
    }
    function getdbcodes($table, $field) {
        $fieldrow = $this->connect()->get(1, 'show full fields from ' . $table . ' where Field=?', array($field));
        if (count($fieldrow) == 0)
            return array();
        $comment = $fieldrow[0]['Comment'];
        $ret = array();
        $ind = strpos($comment, ',TINT');
        if ($ind !== false) {
            $exts = explode('|', substr($comment, $ind + 6));
            $exts = explode('.', $exts[0]);
            for ($i = 0; $i < count($exts); $i++) {
                $excos = explode(':', $exts[$i]);
                if (count($excos) > 1) {
                    $ret[] = array('id' => $excos[0], 'name' => $excos[1]);
                } else if (!empty($excos[0])) {
                    $ret[] = array('id' => $i, 'name' => $excos[0]);
                }
            }
            return $ret;
        }
        $ind = strpos($comment, ',TBIN');
        if ($ind !== false) {
            $exts = explode('|', substr($comment, $ind + 6));
            $exts = explode('.', $exts[0]);
            for ($i = 0; $i < count($exts); $i++) {
                $excos = explode('.', $exts[$i]);
                $ret[] = array('id' => $i + 1, 'name' => $exts[$i]);
            }
            return $ret;
        }
        $ind = strpos($comment, ',BOOL');
        if ($ind !== false) {
            $exts = explode('.', substr($comment, $ind + 6));
            if (count($exts) == 1)
                $exts[1] = '';
            $ret[] = array('id' => 1, 'name' => empty($exts[0]) ? '✔' : $exts[0]);
            $ret[] = array('id' => 2, 'name' => empty($exts[1]) ? '✘' : $exts[1]);
            return $ret;
        }
        return $ret;
    }
    function insert_id() {
        return $this->connect(true)->insert_id();
    }
    function insert($csql, $updata) {
        $ret = $this->connect(true)->insert($csql, $updata);
        if ($ret === false)
            return $this->errdata($ret);
        return $ret;
    }
    function update($csql, $updata) {
        $ret = $this->connect(true)->update($csql, $updata);
        if ($ret === false)
            return $this->errdata($ret);
        return $ret;
    }
    /**
     * 删除表数据，支持备份到_bak。（注意_bak建表时，应去掉id的自增属性）
     * 成功返回影响行数，失败返回false
     * csql         SQL拼接类
     * backup       false:直接删除；true：先备份到table_bak，再删除。
     * 例：
     * $affected = $mydata->delete($csql,true);//备份后删除
     */
    function delete($csql, $backup = false) {
        if ($backup) {
            $fields = $this->connect(true)->get(1, 'show full fields from ' . $csql->table, array());
            $fieldlts = array();
            if ($fields === false)
                return $this->errdata($fields);
            foreach ($fields as $row)
                $fieldlts[] = $row['Field'];
            $fieldlt = implode(',', $fieldlts);
            $sql = "insert into {$csql->table}_bak ({$fieldlt}) select {$fieldlt} from {$csql->table}" . $csql->buildwhere();
            $ret = $this->connect(true)->execute($sql, $csql->tsmt);
            if ($ret === false)
                return $this->errdata($ret);
        }
        $ret = $this->connect(true)->execute("delete from {$csql->table}" . $csql->buildwhere(), $csql->tsmt);
        if ($ret === false)
            return $this->errdata($ret);
        return $ret;
    }
    /**
     * 执行SQL语句。
     * 成功返回影响行数，失败返回false
     * sql      SQL语句。
     * 例：
     * $affected = $mydata->execute('update users set username=? where id=?',['aaa',12]);
     */
    function execute($sql, $tsmt = array()) {
        $ret = $this->connect(true)->execute($sql, $tsmt);
        if ($ret === false)
            return $this->errdata($ret);
        return $ret;
    }
    function begin() {
        $ret = $this->connect(true)->begin();
        if ($ret === false)
            return $this->errdata($ret);
        $this->lock = true;
        return $ret;
    }
    function commit() {
        $ret = $this->connect(true)->commit();
        if ($ret === false)
            return $this->errdata($ret);
        $this->lock = false;
        _setmemvar($this, 'func_commit', array('params+1'));
        return $ret;
    }
    function rollback() {
        $ret = $this->connect(true)->rollback();
        if ($ret === false)
            return $this->errdata($ret);
        $this->lock = false;
        _setmemvar($this, 'func_rollback', array('params+1'));
        return $ret;
    }
    function tran($func) {
        if (!($func instanceof \Closure)) {
            $this->error = '没有传递正确的闭包函数';
            return false;
        }
        $this->begin();
        $ret = false;
        try {
            $ret = $func();
        } catch (\Exception $ex) {
            $ret = false;
            $this->error = $ex->getMessage();
        }
        if ($ret === false)
            $this->rollback();
        else
            $this->commit();
        return $ret;
    }
    function errdata($ret) {
        if ($this->linkdb !== false)
            $this->error = $this->linkdb->error;
        return $ret;
    }
    function trace($pf, $rsuser = null) { //traceid全链路跟踪
        if (!preg_match('/^[A-Z]{2}\d{13}_\d{8}$/', $pf)) //防止前端攻击
            $pf = 'ER' . time() . '000_' . rand(10000000, 99999999);
        $time = substr($pf, 2, 10);
        if (abs(time() - $time) > 10) //跟随服务器时间
            $pf .= ',svrt=' . time();
        if (is_array($rsuser))
            $this->pf = $pf . ',ip=' . getip() . ',uid=' . $rsuser['id'];
        else
            $this->pf = $pf . ',ip=' . getip() . ',uid=no';
    }
    function __destruct() {
    }
}
