自編PHP框架一(數(shù)據(jù)庫操作封裝)

自編PHP框架之?dāng)?shù)據(jù)庫PDO層封裝和模型類部分方法的編寫

如果你是噴子屋灌,問我造輪子花這么多精力有什么用的話洁段,那就走,看看我的這篇文章 為什么我要寫自己的框架共郭?
框架所有的代碼都在筆者的Github上做展示祠丝,并且有一個庫存管理信息系統(tǒng)的實例,Github賬號請看筆者簡介

這是自編寫框架的第一篇除嘹,之后計劃寫路由篇写半、小組件篇、工廠篇等

數(shù)據(jù)庫操作可以說是網(wǎng)頁應(yīng)用程序的核心尉咕,他直接決定了你的程序是干什么的
看大標(biāo)題叠蝇,很明顯的發(fā)現(xiàn)這是一篇和PDO封裝有關(guān)系的文章,再一次感謝Yii2.0框架給我設(shè)計模式方面的啟發(fā)年缎。廢話不多說蟆肆,我們開始

封裝分為兩個類:Connection類 | Command類

首先,作為PHP晦款,一個請求就會對應(yīng)一個PHP線程,在這個大環(huán)境下枚冗,一個線程有多個數(shù)據(jù)庫連接豈不是很浪費缓溅,因此,我運用單例模式讓請求的整個生命周期內(nèi)共享數(shù)據(jù)庫連接

//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;
}

//類被回收的時候調(diào)用赁温,此時關(guān)閉數(shù)據(jù)庫連接
public function __destruct() {
    $this->close();
}

有了單例模式之后我們就需要進行實際的連接數(shù)據(jù)庫操作了
通過config配置數(shù)據(jù)庫坛怪,運用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的變量,這個設(shè)計把從服務(wù)器所有的連接實例化都儲存在一個私有變量中股囊,統(tǒng)一進行管理

//實例化后數(shù)據(jù)庫連接屬性
public $connect;

private $db; //數(shù)據(jù)庫連接信息

//服務(wù)器信息袜匿,私有屬性
private $dsn;
private $user;
private $pass;
private $charset;

private $rightNowDb; //當(dāng)前服務(wù)器信息私有屬性的服務(wù)器名

private $PDOSlaves;  //從服務(wù)器實例化后數(shù)據(jù)庫連接屬性

獲取數(shù)據(jù)庫連接信息,一旦信息不完整

  • 拋出錯誤:One of the PDO::DB Parameter is empty!
  • 記錄在系統(tǒng)的錯誤日之內(nèi)(errordb.log文件夾內(nèi))稚疹,LogWrite類是筆者自己封裝的日志記錄類居灯,采用鏈?zhǔn)秸{(diào)用。
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方法執(zhí)行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;
}

數(shù)據(jù)庫關(guān)閉义锥、數(shù)據(jù)庫是否活躍方法(簡單,直接貼代碼岩灭,不做解釋)

public function getConnect() {
    $this->getInfo();
    if ($this->isActive()) {
        return $this->connect;
    } else {
        $this->connect = $this->nowConnect();
        return $this->connect;
    }
}

//if there is active
public function isActive() {
    return $this->connect;
}

//close connection
public function close() {
    if ($this->isActive()) {
        $this->connect = null;
    }
}

下面就到了我們實際使用數(shù)據(jù)庫連接的方法了拌倍,如果程序員使用到數(shù)據(jù)庫連接,這些方法是最常用的噪径,那就是獲取連接

  • 獲取連接使用判斷柱恤,如果不是active,則調(diào)用封裝的PDO連接方法找爱,賦值給類內(nèi)的connect屬性梗顺,返回此屬性
public function getConnect() {
    $this->getInfo();
    if ($this->isActive()) {
        return $this->connect;
    } else {
        $this->connect = $this->nowConnect();
        return $this->connect;
    }
}

下面就是從服務(wù)器連接的代碼

  • 從服務(wù)器的話則從PDOSlaves屬性內(nèi)拿值,判斷同主服務(wù)器缴允,不同的從服務(wù)器連接對應(yīng)PDOSlaves內(nèi)不同的鍵值對
  • setToSlaves方法使私有數(shù)據(jù)庫連接屬性變成需要操作的數(shù)據(jù)庫
  • setMaster私有數(shù)據(jù)庫連接屬性變回主服務(wù)器
  • 大家都看到了return $this;了吧荚守,這就是鏈?zhǔn)秸{(diào)用的核心!
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 DataBase
public 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;
}

寫到這邊很多人就有疑問了练般,介紹了這么多方法矗漾,都是數(shù)據(jù)庫連接的,光連接數(shù)據(jù)庫沒有什么用處氨×稀敞贡!還是不對數(shù)據(jù)庫做一點的操作,而且按照現(xiàn)在的介紹應(yīng)該會new兩個類摄职,進行互不相干的聯(lián)系誊役,這樣不是很low!還不如封裝成一個谷市!

有這個想法的人很好蛔垢!說明在思考,因為我當(dāng)時在閱讀Yii2.0源碼的時候打開它也有這同樣的想法迫悠,但之后看到了他對這個的解決方法鹏漆,我茅塞頓開

  • 在連接數(shù)據(jù)庫內(nèi)部有一個數(shù)據(jù)庫操作類的工廠方法
  • 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;
}

這就是所有的數(shù)據(jù)庫連接的代碼,下面介紹Command類创泄,首先是構(gòu)造方法和一些屬性艺玲,數(shù)據(jù)庫連接很顯然就儲存在類內(nèi)pdo的屬性內(nèi)

//this save sql word
private $sql;

//pdo connect
private $pdo;

//the pdo statement
private $pdoStmt;

//the last db select is in here
private $lastCommandDb;

private $dataType = PDO::FETCH_ASSOC; //從數(shù)據(jù)庫內(nèi)取出數(shù)據(jù)的默認屬性

//Connection.php to new a command
public function __construct($arr) {
    $this->sql = $arr['sql']?$arr['sql']:'';
    $this->pdo = $arr['db'];
}

數(shù)據(jù)庫sql搜索,分為準(zhǔn)備和執(zhí)行兩步鞠抑,運用PDO的方法饭聚,一旦有錯誤,拋錯搁拙,記錄日志秒梳,沒有準(zhǔn)備就執(zhí)行的話法绵,系統(tǒng)會拋throw new PDOException('PDO is Fail to use execute before prepare!');錯誤

//Must handle
private function prepare() {
    //if there have stmt
    if ($this->pdoStmt) {
        $this->lastCommandDb = $this->pdoStmt;
    }
    $this->pdoStmt = $this->pdo->prepare($this->sql);
}

//execute it and return
private 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!');
    }
}

下面介紹事務(wù),作為一個數(shù)據(jù)庫操作端幼,那就肯定需要事務(wù)礼烈,沒有事務(wù)的話,往往就出現(xiàn)致命的錯誤婆跑,突然說到事務(wù)有可能會有點唐突此熬,下面就舉個很簡單的例子,關(guān)于銀行的

銀行內(nèi)部不同的人會對應(yīng)不同的賬戶滑进,每個賬戶都是一條數(shù)據(jù)庫記錄犀忱,下面模擬一個轉(zhuǎn)賬業(yè)務(wù):A轉(zhuǎn)賬給B 100塊

當(dāng)A轉(zhuǎn)賬給B的時候,A賬戶內(nèi)的錢減去100扶关,此時是成功的阴汇,當(dāng)給B內(nèi)的帳戶添加100塊的時候,由于某些原因(由于死鎖導(dǎo)致執(zhí)行時間超時节槐,系統(tǒng)內(nèi)部錯誤等)搀庶,B的帳戶內(nèi)沒有多100塊錢,這個轉(zhuǎn)賬事務(wù)其實是失敗的铜异,但是一旦沒有事務(wù)的約束哥倔,只有拋個錯,沒有紀錄和回滾的話其實對于一個轉(zhuǎn)賬業(yè)務(wù)來說這是致命的W嶙E剌铩!蚂子!

在這個基礎(chǔ)上沃测,就誕生了事務(wù)的概念,PDO也有對其的一套完整的解決方案食茎,那我們下面就來封裝它

//transction handle
private 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 lately
private 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;
    }
}

以上就是基礎(chǔ)的一些方法的封裝蒂破,那應(yīng)該如何使用這些方法吶,當(dāng)然就是增刪改查咯别渔!

public function queryAll($fetchMode = null) {
    return $this->queryInit('fetchAll', $fetchMode);
}

public function queryOne($fetchMode = null) {
    return $this->queryInit('fetch', $fetchMode);
}

//insert into database
public function insert($table, $arr) {
    $this->sql = Merj::sql()->insert($table)->value($arr)->sqlVal();
    return $this->transction();
}

//insert serval database
public function insertSomeVal($table, array $key, array $arr) {
    $this->sql = Merj::sql()->insert($table)->servalValue($key, $arr)->sqlVal();
    return $this->transction();
}

//update the database
public 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 record
public 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;
    }
}

方法很多寞蚌,其實是有規(guī)律的

  • 這個工廠方法Merj::sql()是一個鏈?zhǔn)椒椒ǎ瑢嵗唇觭ql語句的類
public static function sql() {
    return new QueryBuilder();
}

沒錯钠糊,框架運用到的就是sql語句的鏈?zhǔn)秸{(diào)用拼接方法

  • 搜索方法調(diào)用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(); //從服務(wù)器操作數(shù)據(jù)庫

數(shù)據(jù)庫的模型類方法部分展示,所有模型繼承至此類:

/**
 * 返回一條紀錄內(nèi)的一個值
 * @param  想要取出的值
 * @param  主鍵對應(yīng)的值
 * @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  主鍵對應(yīng)的值
 * @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;
}

最后兩個方法為兩個遞歸方法

  • 對象變成數(shù)組
  • 數(shù)組內(nèi)元素urlencode管宵,由于變成json的json_encode方法會轉(zhuǎn)譯中文截珍,所以需要讓數(shù)組內(nèi)所有元素urlencode再urldecode

好了攀甚!說了這么多,筆者要去吃飯了岗喉!明天還有一天的高考秋度,祝大家高考順利!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钱床,一起剝皮案震驚了整個濱河市荚斯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌查牌,老刑警劉巖事期,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異纸颜,居然都是意外死亡兽泣,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門胁孙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唠倦,“玉大人,你說我怎么就攤上這事涮较〕肀牵” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵法希,是天一觀的道長枷餐。 經(jīng)常有香客問我,道長苫亦,這世上最難降的妖魔是什么毛肋? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮屋剑,結(jié)果婚禮上润匙,老公的妹妹穿的比我還像新娘。我一直安慰自己唉匾,他們只是感情好孕讳,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著巍膘,像睡著了一般厂财。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上峡懈,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天璃饱,我揣著相機與錄音,去河邊找鬼肪康。 笑死荚恶,一個胖子當(dāng)著我的面吹牛撩穿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谒撼,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼食寡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了廓潜?” 一聲冷哼從身側(cè)響起抵皱,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茉帅,沒想到半個月后叨叙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡堪澎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年擂错,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片樱蛤。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡钮呀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出昨凡,到底是詐尸還是另有隱情爽醋,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布便脊,位于F島的核電站蚂四,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏哪痰。R本人自食惡果不足惜遂赠,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望晌杰。 院中可真熱鬧跷睦,春花似錦、人聲如沸肋演。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽爹殊。三九已至蜕乡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間梗夸,已是汗流浹背异希。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人称簿。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像惰帽,于是被迫代替她去往敵國和親憨降。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容