thinkphp5 伺服器 鏈接

代碼裡原有的註釋已經非常完善瞭。不需要我在做什麼瞭。。。。。

namespace think\db;

use PDO;
use PDOStatement;
use think\Collection;
use think\Db;
use think\db\exception\BindParamException;
use think\db\Query;
use think\Debug;
use think\Exception;
use think\exception\PDOException;
use think\Log;

/** 伺服器 連接 抽象類
 * Class Connection
 * @package think
 * @method Query table(string $table) 指定數據表(含前綴)
 * @method Query name(string $name) 指定數據表(不含前綴)
 *
 */
abstract class Connection
{

    /** @var PDOStatement PDO操作實例 */
    protected $PDOStatement;

    /** @var string 當前SQL指令 */
    protected $queryStr = '';
    // 返回或者影響記錄數
    protected $numRows = 0;
    // 事務指令數
    protected $transTimes = 0;
    // 錯誤信息
    protected $error = '';

    /** @var PDO[] 伺服器連接ID 支持多個連接 */
    protected $links = [];

    /** @var PDO 當前連接ID */
    protected $linkID;
    protected $linkRead;
    protected $linkWrite;

    // 查詢結果類型
    protected $resultSetType = 'array';
    // 查詢結果類型
    protected $fetchType = PDO::FETCH_ASSOC;
    // 字段屬性大小寫
    protected $attrCase = PDO::CASE_LOWER;
    // 監聽回調
    protected static $event = [];
    // 查詢對象
    protected $query = [];
    // 伺服器連接參數配置
    protected $config = [
        // 伺服器類型
        'type'           => '',
        // 伺服器地址
        'hostname'       => '',
        // 伺服器名
        'database'       => '',
        // 用戶名
        'username'       => '',
        // 密碼
        'password'       => '',
        // 端口
        'hostport'       => '',
        // 連接dsn
        'dsn'            => '',
        // 伺服器連接參數
        'params'         => [],
        // 伺服器編碼默認采用utf8
        'charset'        => 'utf8',
        // 伺服器表前綴
        'prefix'         => '',
        // 伺服器調試模式
        'debug'          => false,
        // 伺服器部署方式:0 集中式(單一伺服器),1 分佈式(主從伺服器)
        'deploy'         => 0,
        // 伺服器讀寫是否分離 主從式有效
        'rw_separate'    => false,
        // 讀寫分離後 主伺服器數量
        'master_num'     => 1,
        // 指定從伺服器序號
        'slave_no'       => '',
        // 是否嚴格檢查字段是否存在
        'fields_strict'  => true,
        // 數據集返回類型
        'resultset_type' => 'array',
        // 自動寫入時間戳字段
        'auto_timestamp' => false,
        // 是否需要進行SQL性能分析
        'sql_explain'    => false,
        // Builder類
        'builder'        => '',
        // Query類
        'query'          => '\\think\\db\\Query',
    ];

    // PDO連接參數
    protected $params = [
        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_EMULATE_PREPARES  => false,
    ];

    /**
     * 架構函數 讀取伺服器配置信息
     * @access public
     * @param array $config 伺服器配置數組
     */
    public function __construct(array $config = [])
    {
        if (!empty($config)) {
            $this->config = array_merge($this->config, $config);
        }
    }

    /**
     * 創建指定模型的查詢對象
     * @access public
     * @param string $model 模型類名稱
     * @param string $queryClass 查詢對象類名
     * @return Query
     */
    public function model($model, $queryClass = '')
    {
        // 查詢對象 query 中 $model 不存在時 執行
        if (!isset($this->query[$model])) {
            // 查詢類 命名空間地址
            $class               = $queryClass ?: $this->config['query'];
            //實例化該類 並賦值
            //$this 當前對象,$model 模型類名稱
            $this->query[$model] = new $class($this, $model);
        }
        return $this->query[$model];
    }

    /**
     * 調用Query類的查詢方法
     * @access public
     * @param string    $method 方法名稱
     * @param array     $args 調用參數
     * @return mixed
     */
    public function __call($method, $args)
    {
        if (!isset($this->query['database'])) {
            // 查詢類 命名空間地址
            $class                   = $this->config['query'];
            // 實例化並賦值
            //$this 當前對象
            $this->query['database'] = new $class($this);
        }
        return call_user_func_array([$this->query['database'], $method], $args);
    }

    /**
     * 解析pdo連接的dsn信息
     * @access protected
     * @param array $config 連接信息
     * @return string
     */
    abstract protected function parseDsn($config);

    /**
     * 取得數據表的字段信息
     * @access public
     * @param string $tableName
     * @return array
     */
    abstract public function getFields($tableName);

    /**
     * 取得伺服器的表信息
     * @access public
     * @param string $dbName
     * @return array
     */
    abstract public function getTables($dbName);

    /**
     * SQL性能分析
     * @access protected
     * @param string $sql
     * @return array
     */
    abstract protected function getExplain($sql);

    /**
     * 對返數據表字段信息進行大小寫轉換出來
     * @access public
     * @param array $info 字段信息
     * @return array
     */
    public function fieldCase($info)
    {
        // 字段大小寫轉換
        switch ($this->attrCase) {
            case PDO::CASE_LOWER:
                $info = array_change_key_case($info);
                break;
            case PDO::CASE_UPPER:
                $info = array_change_key_case($info, CASE_UPPER);
                break;
            case PDO::CASE_NATURAL:
            default:
                // 不做轉換
        }
        return $info;
    }

    /**
     * 獲取伺服器的配置參數
     * @access public
     * @param string $config 配置名稱
     * @return mixed
     */
    public function getConfig($config = '')
    {
        return $config ? $this->config[$config] : $this->config;
    }

    /**
     * 設置伺服器的配置參數
     * @access public
     * @param string|array      $config 配置名稱
     * @param mixed             $value 配置值
     * @return void
     */
    public function setConfig($config, $value = '')
    {
        if (is_array($config)) {
            $this->config = array_merge($this->config, $config);
        } else {
            $this->config[$config] = $value;
        }
    }

    /**
     * 連接伺服器方法
     * @access public
     * @param array         $config 連接參數
     * @param integer       $linkNum 連接序號
     * @param array|bool    $autoConnection 是否自動連接主伺服器(用於分佈式)
     * @return PDO
     * @throws Exception
     */
    public function connect(array $config = [], $linkNum = 0, $autoConnection = false)
    {
        // 伺服器鏈接links中序號為$linkNum是否已存在,如果不存在時執行
        if (!isset($this->links[$linkNum])) {
            // $config 連接參數不存在時,自動賦值
            if (!$config) {
                $config = $this->config;
            } else {
                // 合並鏈接參數
                $config = array_merge($this->config, $config);
            }
            // 連接參數
            if (isset($config['params']) && is_array($config['params'])) {
                $params = $config['params'] + $this->params;
            } else {
                $params = $this->params;
            }
            // 記錄當前字段屬性大小寫設置
            $this->attrCase = $params[PDO::ATTR_CASE];
            // 記錄數據集返回類型
            if (isset($config['resultset_type'])) {
                $this->resultSetType = $config['resultset_type'];
            }
            try {
                // $config 連接參數 dsn 不存在時
                if (empty($config['dsn'])) {
                    // 解析$config變量,生成 dsn 格式
                    $config['dsn'] = $this->parseDsn($config);
                }
                if ($config['debug']) {
                    $startTime = microtime(true);
                }
                // PDO 鏈接實例化
                $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
                // 伺服器配置 是否開啟 debug 模式
                if ($config['debug']) {
                    // 記錄伺服器連接信息
                    Log::record('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn'], 'sql');
                }
            } catch (\PDOException $e) {
                // 分佈式伺服器 是否開啟
                if ($autoConnection) {
                    Log::record($e->getMessage(), 'error');
                    // 重新鏈接
                    return $this->connect($autoConnection, $linkNum);
                } else {
                    throw $e;
                }
            }
        }
        return $this->links[$linkNum];
    }

    /**
     * 釋放查詢結果
     * @access public
     */
    public function free()
    {
        $this->PDOStatement = null;
    }

    /**
     * 獲取PDO對象
     * @access public
     * @return \PDO|false
     */
    public function getPdo()
    {
        if (!$this->linkID) {
            return false;
        } else {
            return $this->linkID;
        }
    }

    /**
     * 執行查詢 返回數據集
     * @access public
     * @param string        $sql sql指令
     * @param array         $bind 參數綁定
     * @param boolean       $master 是否在主伺服器讀操作
     * @param bool|string   $class 指定返回的數據集對象
     * @return mixed
     * @throws BindParamException
     * @throws PDOException
     */
    public function query($sql, $bind = [], $master = false, $class = false)
    {
        $this->initConnect($master);
        if (!$this->linkID) {
            return false;
        }
        // 根據參數綁定組裝最終的SQL語句
        $this->queryStr = $this->getRealSql($sql, $bind);

        //釋放前次的查詢結果
        if (!empty($this->PDOStatement)) {
            $this->free();
        }

        Db::$queryTimes++;
        try {
            // 調試開始
            $this->debug(true);
            // 預處理
            $this->PDOStatement = $this->linkID->prepare($sql);
            // 參數綁定
            $this->bindValue($bind);
            // 執行查詢
            $result = $this->PDOStatement->execute();
            // 調試結束
            $this->debug(false);
            $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
            return $this->getResult($class, $procedure);
        } catch (\PDOException $e) {
            throw new PDOException($e, $this->config, $this->queryStr);
        }
    }

    /**
     * 執行語句
     * @access public
     * @param string        $sql sql指令
     * @param array         $bind 參數綁定
     * @return int
     * @throws BindParamException
     * @throws PDOException
     */
    public function execute($sql, $bind = [])
    {
        $this->initConnect(true);
        if (!$this->linkID) {
            return false;
        }
        // 根據參數綁定組裝最終的SQL語句
        $this->queryStr = $this->getRealSql($sql, $bind);

        //釋放前次的查詢結果
        if (!empty($this->PDOStatement)) {
            $this->free();
        }

        Db::$executeTimes++;
        try {
            // 調試開始
            $this->debug(true);
            // 預處理
            $this->PDOStatement = $this->linkID->prepare($sql);
            // 參數綁定操作
            $this->bindValue($bind);
            // 執行語句
            $result = $this->PDOStatement->execute();
            // 調試結束
            $this->debug(false);

            $this->numRows = $this->PDOStatement->rowCount();
            return $this->numRows;
        } catch (\PDOException $e) {
            throw new PDOException($e, $this->config, $this->queryStr);
        }
    }

    /**
     * 根據參數綁定組裝最終的SQL語句 便於調試
     * @access public
     * @param string    $sql 帶參數綁定的sql語句
     * @param array     $bind 參數綁定列表
     * @return string
     */
    public function getRealSql($sql, array $bind = [])
    {
        if ($bind) {
            foreach ($bind as $key => $val) {
                $value = is_array($val) ? $val[0] : $val;
                $type  = is_array($val) ? $val[1] : PDO::PARAM_STR;
                if (PDO::PARAM_STR == $type) {
                    $value = $this->quote($value);
                }
                // 判斷占位符
                $sql = is_numeric($key) ?
                    substr_replace($sql, $value, strpos($sql, '?'), 1) :
                    str_replace(
                        [':' . $key . ')', ':' . $key . ',', ':' . $key . ' '],
                        [$value . ')', $value . ',', $value . ' '],
                        $sql . ' ');
            }
        }
        return $sql;
    }

    /**
     * 參數綁定
     * 支持 ['name'=>'value','id'=>123] 對應命名占位符
     * 或者 ['value',123] 對應問號占位符
     * @access public
     * @param array $bind 要綁定的參數列表
     * @return void
     * @throws \think\Exception
     */
    protected function bindValue(array $bind = [])
    {
        foreach ($bind as $key => $val) {
            // 占位符
            $param = is_numeric($key) ? $key + 1 : ':' . $key;
            if (is_array($val)) {
                $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
            } else {
                $result = $this->PDOStatement->bindValue($param, $val);
            }
            if (!$result) {
                throw new BindParamException(
                    "Error occurred  when binding parameters '{$param}'",
                    $this->config,
                    $this->queryStr,
                    $bind
                );
            }
        }
    }

    /**
     * 獲得數據集
     * @access protected
     * @param bool|string   $class true 返回PDOStatement 字符串用於指定返回的類名
     * @param bool          $procedure 是否存儲過程
     * @return mixed
     */
    protected function getResult($class = '', $procedure = false)
    {
        if (true === $class) {
            // 返回PDOStatement對象處理
            return $this->PDOStatement;
        }
        if ($procedure) {
            // 存儲過程返回結果
            return $this->procedure($class);
        }
        $result        = $this->PDOStatement->fetchAll($this->fetchType);
        $this->numRows = count($result);

        if (!empty($class)) {
            // 返回指定數據集對象類
            $result = new $class($result);
        } elseif ('collection' == $this->resultSetType) {
            // 返回數據集Collection對象
            $result = new Collection($result);
        }
        return $result;
    }

    /**
     * 獲得存儲過程數據集
     * @access protected
     * @param bool|string $class true 返回PDOStatement 字符串用於指定返回的類名
     * @return array
     */
    protected function procedure($class)
    {
        $item = [];
        do {
            $result = $this->getResult($class);
            if ($result) {
                $item[] = $result;
            }
        } while ($this->PDOStatement->nextRowset());
        $this->numRows = count($item);
        return $item;
    }

    /**
     * 執行伺服器事務
     * @access public
     * @param callable $callback 數據操作方法回調
     * @return mixed
     * @throws PDOException
     * @throws \Exception
     * @throws \Throwable
     */
    public function transaction($callback)
    {
        $this->startTrans();
        try {
            $result = null;
            if (is_callable($callback)) {
                $result = call_user_func_array($callback, [$this]);
            }
            $this->commit();
            return $result;
        } catch (\Exception $e) {
            $this->rollback();
            throw $e;
        } catch (\Throwable $e) {
            $this->rollback();
            throw $e;
        }
    }

    /**
     * 啟動事務
     * @access public
     * @return void
     */
    public function startTrans()
    {
        // 初始化伺服器連接
        $this->initConnect(true);
        if (!$this->linkID) {
            return false;
        }

        ++$this->transTimes;

        if (1 == $this->transTimes) {
            $this->linkID->beginTransaction();
        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
            $this->linkID->exec(
                $this->parseSavepoint('trans' . $this->transTimes)
            );
        }
    }

    /**
     * 用於非自動提交狀態下面的查詢提交
     * @access public
     * @return void
     * @throws PDOException
     */
    public function commit()
    {
        $this->initConnect(true);

        if (1 == $this->transTimes) {
            $this->linkID->commit();
        }

        --$this->transTimes;
    }

    /**
     * 事務回滾
     * @access public
     * @return void
     * @throws PDOException
     */
    public function rollback()
    {
        $this->initConnect(true);

        if (1 == $this->transTimes) {
            $this->linkID->rollBack();
        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
            $this->linkID->exec(
                $this->parseSavepointRollBack('trans' . $this->transTimes)
            );
        }

        $this->transTimes = max(0, $this->transTimes - 1);
    }

    /**
     * 是否支持事務嵌套
     * @return bool
     */
    protected function supportSavepoint()
    {
        return false;
    }

    /**
     * 生成定義保存點的SQL
     * @param $name
     * @return string
     */
    protected function parseSavepoint($name)
    {
        return 'SAVEPOINT ' . $name;
    }

    /**
     * 生成回滾到保存點的SQL
     * @param $name
     * @return string
     */
    protected function parseSavepointRollBack($name)
    {
        return 'ROLLBACK TO SAVEPOINT ' . $name;
    }

    /**
     * 批處理執行SQL語句
     * 批處理的指令都認為是execute操作
     * @access public
     * @param array $sqlArray SQL批處理指令
     * @return boolean
     */
    public function batchQuery($sqlArray = [])
    {
        if (!is_array($sqlArray)) {
            return false;
        }
        // 自動啟動事務支持
        $this->startTrans();
        try {
            foreach ($sqlArray as $sql) {
                $this->execute($sql);
            }
            // 提交事務
            $this->commit();
        } catch (\Exception $e) {
            $this->rollback();
            throw $e;
        }
        return true;
    }

    /**
     * 獲得查詢次數
     * @access public
     * @param boolean $execute 是否包含所有查詢
     * @return integer
     */
    public function getQueryTimes($execute = false)
    {
        return $execute ? Db::$queryTimes + Db::$executeTimes : Db::$queryTimes;
    }

    /**
     * 獲得執行次數
     * @access public
     * @return integer
     */
    public function getExecuteTimes()
    {
        return Db::$executeTimes;
    }

    /**
     * 關閉伺服器
     * @access public
     */
    public function close()
    {
        $this->linkID = null;
    }

    /**
     * 獲取最近一次查詢的sql語句
     * @access public
     * @return string
     */
    public function getLastSql()
    {
        return $this->queryStr;
    }

    /**
     * 獲取最近插入的ID
     * @access public
     * @param string  $sequence     自增序列名
     * @return string
     */
    public function getLastInsID($sequence = null)
    {
        return $this->linkID->lastInsertId($sequence);
    }

    /**
     * 獲取返回或者影響的記錄數
     * @access public
     * @return integer
     */
    public function getNumRows()
    {
        return $this->numRows;
    }

    /**
     * 獲取最近的錯誤信息
     * @access public
     * @return string
     */
    public function getError()
    {
        if ($this->PDOStatement) {
            $error = $this->PDOStatement->errorInfo();
            $error = $error[1] . ':' . $error[2];
        } else {
            $error = '';
        }
        if ('' != $this->queryStr) {
            $error .= "\n [ SQL語句 ] : " . $this->queryStr;
        }
        return $error;
    }

    /**
     * SQL指令安全過濾
     * @access public
     * @param string $str SQL字符串
     * @param bool   $master 是否主庫查詢
     * @return string
     */
    public function quote($str, $master = true)
    {
        $this->initConnect($master);
        return $this->linkID ? $this->linkID->quote($str) : $str;
    }

    /**
     * 伺服器調試 記錄當前SQL及分析性能
     * @access protected
     * @param boolean $start 調試開始標記 true 開始 false 結束
     * @param string  $sql 執行的SQL語句 留空自動獲取
     * @return void
     */
    protected function debug($start, $sql = '')
    {
        if (!empty($this->config['debug'])) {
            // 開啟伺服器調試模式
            if ($start) {
                Debug::remark('queryStartTime', 'time');
            } else {
                // 記錄操作結束時間
                Debug::remark('queryEndTime', 'time');
                $runtime = Debug::getRangeTime('queryStartTime', 'queryEndTime');
                $sql     = $sql ?: $this->queryStr;
                $log     = $sql . ' [ RunTime:' . $runtime . 's ]';
                $result  = [];
                // SQL性能分析
                if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) {
                    $result = $this->getExplain($sql);
                }
                // SQL監聽
                $this->trigger($sql, $runtime, $result);
            }
        }
    }

    /**
     * 監聽SQL執行
     * @access public
     * @param callable $callback 回調方法
     * @return void
     */
    public function listen($callback)
    {
        self::$event[] = $callback;
    }

    /**
     * 觸發SQL事件
     * @access protected
     * @param string    $sql SQL語句
     * @param float     $runtime SQL運行時間
     * @param mixed     $explain SQL分析
     * @return bool
     */
    protected function trigger($sql, $runtime, $explain = [])
    {
        if (!empty(self::$event)) {
            foreach (self::$event as $callback) {
                if (is_callable($callback)) {
                    call_user_func_array($callback, [$sql, $runtime, $explain]);
                }
            }
        } else {
            // 未註冊監聽則記錄到日志中
            Log::record('[ SQL ] ' . $sql . ' [ RunTime:' . $runtime . 's ]', 'sql');
            if (!empty($explain)) {
                Log::record('[ EXPLAIN : ' . var_export($explain, true) . ' ]', 'sql');
            }
        }
    }

    /**
     * 初始化伺服器連接
     * @access protected
     * @param boolean $master 是否主伺服器
     * @return void
     */
    protected function initConnect($master = true)
    {
        if (!empty($this->config['deploy'])) {
            // 采用分佈式伺服器
            if ($master) {
                if (!$this->linkWrite) {
                    $this->linkWrite = $this->multiConnect(true);
                }
                $this->linkID = $this->linkWrite;
            } else {
                if (!$this->linkRead) {
                    $this->linkRead = $this->multiConnect(false);
                }
                $this->linkID = $this->linkRead;
            }
        } elseif (!$this->linkID) {
            // 默認單伺服器
            $this->linkID = $this->connect();
        }
    }

    /**
     * 連接分佈式伺服器
     * @access protected
     * @param boolean $master 主伺服器
     * @return PDO
     */
    protected function multiConnect($master = false)
    {
        $_config = [];
        // 分佈式伺服器配置解析
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
            $_config[$name] = explode(',', $this->config[$name]);
        }

        // 主伺服器序號
        $m = floor(mt_rand(0, $this->config['master_num'] - 1));

        if ($this->config['rw_separate']) {
            // 主從式采用讀寫分離
            if ($master) // 主伺服器寫入
            {
                $r = $m;
            } elseif (is_numeric($this->config['slave_no'])) {
                // 指定伺服器讀
                $r = $this->config['slave_no'];
            } else {
                // 讀操作連接從伺服器 每次隨機連接的伺服器
                $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1));
            }
        } else {
            // 讀寫操作不區分伺服器 每次隨機連接的伺服器
            $r = floor(mt_rand(0, count($_config['hostname']) - 1));
        }
        $dbMaster = false;
        if ($m != $r) {
            $dbMaster = [];
            foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
                $dbMaster[$name] = isset($_config[$name][$m]) ? $_config[$name][$m] : $_config[$name][0];
            }
        }
        $dbConfig = [];
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
            $dbConfig[$name] = isset($_config[$name][$r]) ? $_config[$name][$r] : $_config[$name][0];
        }
        return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
    }

    /**
     * 析構方法
     * @access public
     */
    public function __destruct()
    {
        // 釋放查詢
        if ($this->PDOStatement) {
            $this->free();
        }
        // 關閉連接
        $this->close();
    }
}

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *