自编PHP框架一(数据库操作封装)

作者: merjiezo | 来源:发表于2016-06-07 17:35 被阅读1572次

    自编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 active
    public function isActive() {
        return $this->connect;
    }
    
    //close connection
    public 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 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;
    }
    

    写到这边很多人就有疑问了,介绍了这么多方法,都是数据库连接的,光连接数据库没有什么用处啊!还是不对数据库做一点的操作,而且按照现在的介绍应该会new两个类,进行互不相干的联系,这样不是很low!还不如封装成一个!

    有这个想法的人很好!说明在思考,因为我当时在阅读Yii2.0源码的时候打开它也有这同样的想法,但之后看到了他对这个的解决方法,我茅塞顿开

    • 在连接数据库内部有一个数据库操作类的工厂方法
    • 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 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; //从数据库内取出数据的默认属性
    
    //Connection.php to new a command
    public 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 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!');
        }
    }
    

    下面介绍事务,作为一个数据库操作,那就肯定需要事务,没有事务的话,往往就出现致命的错误,突然说到事务有可能会有点唐突,下面就举个很简单的例子,关于银行的

    银行内部不同的人会对应不同的账户,每个账户都是一条数据库记录,下面模拟一个转账业务:A转账给B 100块

    当A转账给B的时候,A账户内的钱减去100,此时是成功的,当给B内的帐户添加100块的时候,由于某些原因(由于死锁导致执行时间超时,系统内部错误等),B的帐户内没有多100块钱,这个转账事务其实是失败的,但是一旦没有事务的约束,只有拋个错,没有纪录和回滚的话其实对于一个转账业务来说这是致命的!!!!

    在这个基础上,就诞生了事务的概念,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;
        }
    }
    

    以上就是基础的一些方法的封装,那应该如何使用这些方法呐,当然就是增删改查咯!

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

    方法很多,其实是有规律的

    • 这个工厂方法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

    好了!说了这么多,笔者要去吃饭了!明天还有一天的高考,祝大家高考顺利!

    相关文章

      网友评论

        本文标题:自编PHP框架一(数据库操作封装)

        本文链接:https://www.haomeiwen.com/subject/zcwddttx.html