自編PHP架構之資料庫PDO層封裝和模型類部分方法的編寫
如果你是噴子,問我造輪子花這麼多精力有什麼用的話,那就走,看看我的這篇文章 為什麼我要寫自己的架構?架構所有的代碼都在筆者的Github上做展示,並且有一個庫存管理資訊系統的執行個體,Github帳號請看筆者簡介
這是自編寫架構的第一篇,之後計劃寫路由篇、小工具篇、工廠篇等
資料庫操作可以說是網頁應用程式的核心,他直接決定了你的程式是幹什麼的看大標題,很明顯的發現這是一篇和PDO封裝有關係的文章,再一次感謝Yii2.0架構給我設計模式方面的啟發。廢話不多說,我們開始
封裝分為兩個類:Connection類 | Command類
首先,作為PHP,一個請求就會對應一個PHP線程,在這個大環境下,一個線程有多個資料庫連接豈不是很浪費,因此,我運用單例模式讓請求的整個生命週期內共用資料庫串連
//Connection類private static $_instance;private function __construct() {}private function __clone() {}//單例public static function getinstance() { if (!(self::$_instance instanceof self)) { self::$_instance = new self(); } return self::$_instance;}//類被回收的時候調用,此時關閉資料庫連接public function __destruct() { $this->close();}
有了單例模式之後我們就需要進行實際的串連資料庫操作了通過config設定資料庫,運用PDO獨特的文法dsn:
return [ 'dsn' => 'mysql:host=127.0.0.1;dbname=dbname', 'user' => 'root', 'password' => 'pass', 'charset' => 'utf8', 'slaves' => [ [ 'dsn' => 'mysql:host=127.0.0.1;dbname=dbname', 'user' => 'root', 'password' => 'pass', 'charset' => 'utf8', ], ],];
下面這些是Connection的變數,這個設計把從伺服器所有的串連執行個體化都儲存在一個私人變數中,統一進行管理
//執行個體化後資料庫連接屬性public $connect;private $db; //資料庫連接資訊//伺服器資訊,私人屬性private $dsn;private $user;private $pass;private $charset;private $rightNowDb; //當前伺服器資訊私人屬性的伺服器名private $PDOSlaves; //從伺服器執行個體化後資料庫連接屬性
擷取資料庫連接資訊,一旦資訊不完整
- 拋出錯誤:One of the PDO::DB Parameter is empty!
- 記錄在系統的錯誤日之內(errordb.log檔案夾內),LogWrite類是筆者自己封裝的日誌記錄類,採用鏈式調用。
private function getInfo() { $this->db = require (dirname(__FILE__).'/../../config/db.php'); if ($this->db['dsn'] && $this->db['user'] && $this->db['password']) { $this->dsn = $this->db['dsn']; $this->user = $this->db['user']; $this->pass = $this->db['password']; $this->charset = $this->db['charset']?$this->db['charset']:'utf8'; $this->rightNowDb = 'master'; } else { $this->err('One of the PDO::DB Parameter is empty!'); }}private function err($err) { $err = 'ErrInfo: '.$err; LogWrite::getinstance()->IntoWhere('errordb')->Info($err)->execute(); throw new Exception($err);}
PDO串連方法,這就是PDO的串連方法,運用new PDO進行操作
- exec方法執行sql語句
- getAttribute擷取屬性
private function nowConnect() { try { $connect = new PDO($this->dsn, $this->user, $this->pass); } catch (PDOException $e) { $this->err($e->getMessage()); } if (!$connect) { $this->err('PDO connect error'); } $connect->exec('SET NAMES '.$this->charset); $connect->getAttribute(constant("PDO::ATTR_SERVER_VERSION")); return $connect;}
資料庫關閉、資料庫是否活躍方法(簡單,直接貼代碼,不做解釋)
public function getConnect() { $this->getInfo(); if ($this->isActive()) { return $this->connect; } else { $this->connect = $this->nowConnect(); return $this->connect; }}//if there is activepublic function isActive() { return $this->connect;}//close connectionpublic function close() { if ($this->isActive()) { $this->connect = null; }}
下面就到了我們實際使用資料庫連接的方法了,如果程式員使用到資料庫連接,這些方法是最常用的,那就是擷取串連
- 擷取串連使用判斷,如果不是active,則調用封裝的PDO串連方法,賦值給類內的connect屬性,返回此屬性
public function getConnect() { $this->getInfo(); if ($this->isActive()) { return $this->connect; } else { $this->connect = $this->nowConnect(); return $this->connect; }}
下面就是從伺服器串連的代碼
- 從伺服器的話則從PDOSlaves屬性內拿值,判斷同主伺服器,不同的從伺服器串連對應PDOSlaves內不同的索引值對
- setToSlaves方法使私人資料庫連接屬性變成需要操作的資料庫
- setMaster私人資料庫連接屬性變回主伺服器
- 大家都看到了return $this;了吧,這就是鏈式調用的核心!
public function getSlavesConnect($num) { $this->setToSlaves($num); $key = 'slave'.$num; if ($this->PDOSlaves[$key]) { return $this->PDOSlaves[$key]; } else { $connect = $this->nowConnect(); $this->PDOSlaves[$key] = $connect; return $this->PDOSlaves[$key]; }}//Serval attributes change to slaver DataBasepublic function setToSlaves($num) { if ($this->db['slaves'][$num]['dsn'] && $this->db['slaves'][$num]['user'] && $this->db['slaves'][$num]['password']) { $this->dsn = $this->db['slaves'][$num]['dsn']; $this->user = $this->db['slaves'][$num]['user']; $this->pass = $this->db['slaves'][$num]['password']; $this->rightNowDb = 'slaves'.$num; } else { $this->err('slaves '.$num.':: missing info!'); }}public function setMaster() { $this->getInfo(); return $this;}public function getRightNowDb() { return $this->rightNowDb;}
寫到這邊很多人就有疑問了,介紹了這麼多方法,都是資料庫連接的,光串連資料庫沒有什麼用處啊!還是不對資料庫做一點的操作,而且按照現在的介紹應該會new兩個類,進行互不相干的聯絡,這樣不是很low!還不如封裝成一個!
有這個想法的人很好!說明在思考,因為我當時在閱讀Yii2.0源碼的時候開啟它也有這同樣的想法,但之後看到了他對這個的解決方案,我茅塞頓開
- 在串連資料庫內部有一個資料庫操作類的Factory 方法
- sql可有可無
public function createCommand($sql = null) { //first connect the db $command = new Command([ 'db' => $this->getConnect(), 'sql' => $sql, ]); return $command;}public function createSlavesComm($num = 0, $sql = null) { $command = new Command([ 'db' => $this->getSlavesConnect($num), 'sql' => $sql, ]); return $command;}
這就是所有的資料庫連接的代碼,下面介紹Command類,首先是構造方法和一些屬性,資料庫連接很顯然就儲存在類內pdo的屬性內
//this save sql wordprivate $sql;//pdo connectprivate $pdo;//the pdo statementprivate $pdoStmt;//the last db select is in hereprivate $lastCommandDb;private $dataType = PDO::FETCH_ASSOC; //從資料庫內取出資料的預設屬性//Connection.php to new a commandpublic function __construct($arr) { $this->sql = $arr['sql']?$arr['sql']:''; $this->pdo = $arr['db'];}
資料庫sql搜尋,分為準備和執行兩步,運用PDO的方法,一旦有錯誤,拋錯,記錄日誌,沒有準備就執行的話,系統會拋throw new PDOException('PDO is Fail to use execute before prepare!');錯誤
//Must handleprivate function prepare() { //if there have stmt if ($this->pdoStmt) { $this->lastCommandDb = $this->pdoStmt; } $this->pdoStmt = $this->pdo->prepare($this->sql);}//execute it and returnprivate function execute($method) { if ($this->pdoStmt) { $pdoStmt = $this->pdoStmt; $pdoStmt->execute(); $res = $pdoStmt->$method($this->dataType); if (!$res) { $msg = 'The result is empty, The sql word is :: '.$this->sql; LogWrite::getinstance()->IntoWhere('errordb')->Info($msg)->execute(); return false; } return $res; } else { throw new PDOException('PDO is Fail to use execute before prepare!'); }}
下面介紹事務,作為一個資料庫操作,那就肯定需要事務,沒有事務的話,往往就出現致命的錯誤,突然說到事務有可能會有點唐突,下面就舉個很簡單的例子,關於銀行的
銀行內部不同的人會對應不同的賬戶,每個賬戶都是一條資料庫記錄,下面類比一個轉賬業務:A轉賬給B 100塊
當A轉賬給B的時候,A賬戶內的錢減去100,此時是成功的,當給B內的帳戶添加100塊的時候,由於某些原因(由於死結導致執行時間逾時,系統內部錯誤等),B的帳戶內沒有多100塊錢,這個轉賬事務其實是失敗的,但是一旦沒有事務的約束,只有拋個錯,沒有紀錄和復原的話其實對於一個轉賬業務來說這是致命的!!!!
在這個基礎上,就誕生了事務的概念,PDO也有對其的一套完整的解決方案,那我們下面就來封裝它
//transction handleprivate function transction() { try { $this->pdo->beginTransaction(); $res = $this->pdo->exec($this->sql); if ($this->pdo->errorInfo()[0] != '00000') { throw new PDOException('DB Error::Fail to change the database!! The sql is: '.$this->sql.' The Error is :: '.$this->pdo->errorInfo()[2]); } $this->pdo->commit(); return true; } catch (PDOException $e) { $this->pdo->rollback(); LogWrite::getinstance()->IntoWhere('errordb')->Info($e)->execute(); return false; }}//change it latelyprivate function transctions(array $sqlArr = array()) { try { $this->pdo->beginTransaction(); foreach ($sqlArr as $value) { $res = $this->pdo->exec($value); print_r($this->pdo->errorInfo()); if ($this->pdo->errorInfo()[0] != '00000') { throw new PDOException('DB Error::Fail to change the database!! The sql is: '.$value.' The Error is :: '.$this->pdo->errorInfo()[2]); } } $this->pdo->commit(); return true; } catch (PDOException $e) { $this->pdo->rollback(); LogWrite::getinstance()->IntoWhere('errordb')->Info($e)->execute(); return false; }}
以上就是基礎的一些方法的封裝,那應該如何使用這些方法呐,當然就是增刪改查咯!
public function queryAll($fetchMode = null) { return $this->queryInit('fetchAll', $fetchMode);}public function queryOne($fetchMode = null) { return $this->queryInit('fetch', $fetchMode);}//insert into databasepublic function insert($table, $arr) { $this->sql = Merj::sql()->insert($table)->value($arr)->sqlVal(); return $this->transction();}//insert serval databasepublic function insertSomeVal($table, array $key, array $arr) { $this->sql = Merj::sql()->insert($table)->servalValue($key, $arr)->sqlVal(); return $this->transction();}//update the databasepublic function update($table, $arr, $where) { $this->sql = Merj::sql()->update($table)->set($arr)->where($where)->sqlVal(); return $this->transction();}public function updateTrans(array $sqlArr = array()) { return $this->transctions($sqlArr);}//delete one recordpublic function delete($table, $whereArr) { $this->sql = Merj::sql()->delete($table)->where($whereArr)->sqlVal(); return $this->transction();}private function queryInit($method, $fetchMode = null) { if ($fetchMode) { $this->dataType = $fetchMode; } if ($this->sql && $this->pdo) { $this->prepare(); $result = $this->execute($method); return $result?$result:''; } else { $err = 'Sql or PDO is empty; The sql is '.$this->sql; LogWrite::getinstance()->IntoWhere('errordb')->Info($this->sql)->execute(); throw new PDOException($err); return false; }}
方法很多,其實是有規律的
- 這個Factory 方法Merj::sql()是一個鏈式方法,執行個體化拼接sql語句的類
public static function sql() { return new QueryBuilder();}沒錯,架構運用到的就是sql語句的鏈式調用拼接方法
- 搜尋方法調用queryInit方法
- 增刪改都運用剛剛提到的transction方法
- 如果需要使用完整的方法,只需要像下面這樣,是不是很方便!
$connect = Connection::getinstance();$connect->createCommand($sql)->queryOne();$connect->createCommand($sql)->queryAll();$connect::db()->createCommand()->insert('tableName', [ 'Val Key1' => 'Val1', 'Val Key2' => 'Val2', ]);Merj::db()->createSlavesComm(0, 'SELECT * FROM content')->queryAll(); //從伺服器操作資料庫
資料庫的模型類方法部分展示,所有模型繼承至此類:
/** * 返回一條紀錄內的一個值 * @param 想要取出的值 * @param 主鍵對應的值 * @return 返回一個值 **/public function findOnlyOne($target, $idVal) { $sql = Merj::sql()->select($target)->from($this->tableName)->where([ $this->primKey => $idVal, ])->sqlVal(); $rows = Merj::db()->createCommand($sql)->queryOne(); if ($rows) { return $rows[$target]; } else { return false; }}/** * 返回一條紀錄 * @param 主鍵屬性 * @param 主鍵對應的值 * @return 返回這條紀錄 **/public function findOneRecord($userIdVal) { $sql = Merj::sql()->select()->from($this->tableName)->where([ $this->primKey => $userIdVal, ])->sqlVal(); $rows = Merj::db()->createCommand($sql)->queryOne(); if ($rows) { return $rows; } else { return false; }}/** * 通過sql語句尋找 * @param sql words * @return results **/public function findBySql($sql) { return Merj::db()->createCommand($sql)->queryAll();}/** * @param Insert info * @return success or not **/ public function insertOne($arr) { return Merj::db()->createCommand()->insert($this->tableName, $arr);}/** * @param Insert infos * @return success or not **/public function insertNum(array $key = array(), array $arr = array()) { return Merj::db()->createCommand()->insertSomeVal($this->tableName, $key, $arr);}/** * 更新一條記錄 * @param * @param * @return success or not **/public function updateOneRec($arrUpDate, $idVal) { return Merj::db()->createCommand()->update($this->tableName, $arrUpDate, [ $this->primKey => $idVal, ]);}/** * 多個sql語句更新 * @param sql array * @return success or not **/public function updateTrans($sqlArr) { return Merj::db()->createCommand()->updateTrans($sqlArr);}/** * 刪除一條記錄 * @param where $arr * @return success or not **/public function deleteOne($arr) { return Merj::db()->createCommand()->delete($this->tableName, $arr);}/** * object to array * @param object * @return array **/public function obj_arr($obj) { if (is_object($obj)) { $array = (array) $obj; }if (is_array($obj)) { foreach ($obj as $key => $value) { $array[$key] = $this->obj_arr($value); } } return $array;}public function jsonUtf8Out($arr) { foreach ($arr as $key => $value) { if (is_array($value)) { $arr[$key] = $this->jsonUtf8Out($value); } else { $arr[$key] = urlencode($value); } } return $arr;}
最後兩個方法為兩個遞迴方法
- 對象變成數組
- 數組內元素urlencode,由於變成json的json_encode方法會轉譯中文,所以需要讓數組內所有元素urlencode再urldecode
好了!說了這麼多,筆者要去吃飯了!明天還有一天的高考,祝大家高考順利!