从PHP5到PHP7自我封装MongoDB以及平滑升级

28 篇文章 1 订阅

一.序言

使用PHP+MongoDB的企业级用户很多,因为MongoDB对非结构化数据的存储很方便。在PHP5及以前,官方提供了两个扩展,Mongo和MongoDB,其中Mongo是对以MongoClient等几个核心类为基础的类群进行操作,封装得很方便,所以基本上都会选择Mongo扩展,详情请见官方手册:

http://php.net/manual/en/class.mongoclient.php
  
  
  • 1

但是随着PHP5升级到PHP7,官方不再支持Mongo扩展,只支持MongoDB,而PHP7的性能提升巨大,让人无法割舍,所以怎么把Mongo替换成MongoDB成为了一个亟待解决的问题。MongoDB引入了命名空间,但是功能封装非常差,如果非要用原生的扩展,几乎意味着写原生的Mongo语句。这种想法很违背ORM简化dbIO操作带来的语法问题而专注逻辑优化的思路。详情也可参见官方手册;

http://php.net/manual/en/class.mongodb-driver-manager.php
  
  
  • 1

在这种情况之下,MongoDB官方忍不住了,为了方便使用,增加市场占有率,推出了基于MongoDB扩展的库,详情参见:

https://github.com/mongodb/mongo-php-library
  
  
  • 1

实际上在我们使用的过程中,总是希望能够实现尽可能的解耦,于是分层清晰变得尤为重要。由于官方的库并不能实现笔者分离和特定的功能需要,于是笔者自己造了一次轮子。

二.自我封装的MongoDBClient类

1.构造函数
笔者希望构造函数能够有两种方式,一种以单例模式去实现对Model层继承传参构造,另一种是简单地直接构造,以实现代码的充分复用和封装类的广泛适用。

public $_client;
public $_manager;
public $_db;
public $_collection;

public function __construct(){
        $config=$this->getDbConnection();
        if(!empty($config['server']) && !empty($config['db'])){
            $uri=$config['server']."/".$config['db'];
            if(isset($config['urioptions'])){
                $urioptions=$config['urioptions'];
            }else{
                $urioptions=array();
            }
            if(isset($config['driveroptions'])){
                $driveroptions=$config['driveroptions'];
            }else{
                $driveroptions=array();
            }
            $this->setClient($uri,$urioptions,$driveroptions);
            $this->setDatabase($config['db']);
        }
        $collectionName=$this->collectionName();
        if($this->getDatabase()){
            $this->setCollection($collectionName);
        }
    }
 public function collectionName(){
        return '';
    }
 public function getDbConnection(){
        return array();
    }

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

以上为单例模式的构造方法,显然定义了两个没有意义的获取参数的函数,实际上初始化的入口应该在继承的子类中完成重写。每一个set函数,都会把实例传给该对象的属性以保存并在后续中调用。

public function initInstance($uri,$db,$collectionName)
    {
        // $config = $this->getMongoConfig();
        // $tempStr='mongodb://'.$config['username'].':'.$config['password'].'@'.$config['host'].'/'.$config['db'];
        // $mongodbclient=new MongoDBClient();
        $this->setClient($uri);
        $this->setDatabase($db);
        $this->setCollection($collectionName);
    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里为简单地直接初始化。而构造函数的设计决定了两者并不冲突,至于为何要设计两种初始化方法,是出于对清晰分层的更好支持的原因

2.filter过滤器的构造
从Mongodb官方的原生到php官方的扩展到Mongodb的依赖库,对于filter的构建方法非常粗暴,就是让人去直接写Mongodb的filter的原生语句,这与ORM简化语法耦合的思路大相径庭。为了方便别人使用复杂的过滤器,笔者对过滤器进行了简化的构造。具体思路是把一个语义化的过滤器看作一个算式,一组条件看作一个数,连接符则当作运算符,通过中缀表达式转后缀表达式实现去括号,然后再执行后缀表达式,实现语义化的连接符的语法化,从而简化了业务层的开发者的成本。详情如下:

public function filterConstructor($key,$operator,$value,$connector=array()){
        $filter=array();
        $subfilter=array();
            switch ($operator) {
                case '=':
                    $subfilter=array($key=>$value);
                    break;
                case '>':
                    $subfilter=array($key=>array('$gt'=>$value));
                    break;
                case '>=':
                    $subfilter=array($key=>array('$gte'=>$value));
                    break;
                case '<':
                    $subfilter=array($key=>array('$lt'=>$value));
                    break;
                case '<=':
                    $subfilter=array($key=>array('$lte'=>$value));
                    break;
                case '!=':
                    $subfilter=array($key=>array('$ne'=>$value));
                    break;
                default:
                    die();
                    break;
        }
        $filter=array_merge($filter,$subfilter);
        return $filter;
    }
    /*
     * construct a easy-and filter with double arrays via key-value input
     * @param (Array)$trible1 (Array)$trible2
     * @return an array of mongo-dialect filter
     * @author wangyang
     */
    public function andFilterConstructor($trible1,$trible2){
        $ret1=$this->filterConstructor($trible1[0],$trible1[1],$trible1[2]);
        $ret2=$this->filterConstructor($trible2[0],$trible2[1],$trible2[2]);
        array_merge($ret1,$ret2);
        return $ret1;
    }
    /*
     * construct a easy-or filter with double arrays via key-value input
     * @param (Array)$trible1 (Array)$trible2
     * @return an array of mongo-dialect filter
     * @author wangyang
     */
    public function orFilterConstructor($trible1,$trible2){
        $ret1=$this->filterConstructor($trible1[0],$trible1[1],$trible1[2]);
        $ret2=$this->filterConstructor($trible2[0],$trible2[1],$trible2[2]);
        $ret=array('$or'=>array());
        array_push($ret['$or'],$ret1);
        array_push($ret['$or'],$ret2);
        return $ret;
    }
     /*
     * construct a easy-and filter with double filters
     * @param (Array)$query1 (Array)$query2
     * @return an array of mongo-dialect filter
     * @author wangyang
     */
    public function onlyAndFilterConstructor($query1,$query2){
        $query1=array_merge_recursive($query1,$query2);
        return $query1;
    }
    /*
     * construct a easy-or filter with double filters
     * @param (Array)$query1 (Array)$query2
     * @return an array of mongo-dialect filter
     * @author wangyang
     */
    public function onlyOrFilterConstructor($query1,$query2){
        $query=array('$or'=>array());
        array_push($query['$or'],$query1);
        array_push($query['$or'],$query2);
        return $query;
    }
    /*
     * resolve the complicated connectors set filter
     * @param  (Array)$query e.g. array(filterarray1(),$connector,filterarray2())  
     *  e.g. array(arr1(),'or','(',arr2(),'and',arr3(),')')
     * @return an array of mongo-dialect filter
     * @author wangyang
     */
    public function queryFilterConstructor($query){
        $priority=array('('=>3,'and'=>2,'or'=>2,')'=>1);
        $stack1=array();
        $stack2=array();
        //transfer nifix expression to postfix expression
        foreach ($query as $key => $value) {
            if(is_array($value)){
                array_push($stack2,$value);
            }elseif($value=='('||empty($stack1)){
                array_push($stack1,$value);
            }elseif($value==')') {
                while(($top=array_pop($stack1))!=='('){
                    array_push($stack2,$top);
                }
            }elseif(end($stack1)=='('){
                array_push($stack1,$value);
            }else{
                while($priority[$value]<$priority[end($stack1)]){
                    $top=array_pop($stack1);
                    array_push($stack2,$top);
                }
                array_push($stack1,$value);
            }      
        }
        while(!empty($stack1)){
            $top=array_pop($stack1);
            array_push($stack2,$top);
        }
        foreach ($stack2 as $key => $value) {
            if(is_array($value)){
                $stack2[$key]=$this->filterConstructor($value[0],$value[1],$value[2]);
            }
        }
        //compute the postfix expression
        foreach ($stack2 as $key => $value) {
            if(is_array($value)){
                array_push($stack1,$value);
            }else{
                $top=array_pop($stack1);
                $subtop=array_pop($stack1);
                if($value=='and'){
                    $ret=$this->onlyAndFilterConstructor($top,$subtop);
                    array_push($stack1,$ret);
                }elseif($value=='or'){
                    $ret=$this->onlyOrFilterConstructor($top,$subtop);
                    array_push($stack1,$ret);
                }else{
                    die('undefined connector');
                }
            }
        }
        $ret=array_pop($stack1);
        return $ret;

    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139

在处理的时候用到了栈的思想,在PHP中使用数组进行代替,实际上,PHP的数组函数还是相当契合栈的思路的,比如插入array_push(),删除顶部元素array_pop(),而在转逆波兰式的过程中,完成对最基础的语句的拼装,后面的复杂语句通过迭代来实现。
比如要实现{“likes”: {$gt:50}, $or: [{“by”: “菜鸟教程”},{“title”: “MongoDB 教程”}]}这样一句查询过滤器,我们就可以用 [[‘likes’,’>’,’50’],’or’,’(‘,[‘by’,’=’,’菜鸟教程’],’and’,[‘title’,’=’,’MongoDB教程’],’)’]来代替了。这大概是我在这个类里最得意的部分了。

3.从数据库到聚合到具体文档的CURD
这个就直接上demo吧~
值得注意的是官方的find()返回为一个cursor,通过foreach遍历输出的结果却是一组documents,说明其实官方的对象设计得很不友好

require 'MongodbExtension.php';
$mongo=new MongoDBClient();
$mongo->setClient("mongodb://127.0.0.1:27017");


function DataBase($mongo){

    //列出所有数据库名
    $databases=$mongo->listDatabases();
    //创建数据库,获得数据库实例
    $database=$mongo->createDatabase('BUPT');
    //删除数据库
    $mongo->dropDatabase('BUPT');
    //选择数据库,获得数据库实例
    $database=$mongo->selectDatabase('wangyang');

}
function Collection($mongo){

    //列出所有集合
    $collections=$mongo->listCollections();
    //创建集合,获得集合实例
    $collection=$mongo->createCollection('BUPT');
    //删除集合
    $mongo->dropCollection('BUPT');
    //选择集合,获得集合实例
    $collection=$mongo->selectCollection('test');

}
function DocumentInsert($mongo){

    //插入一条数据
    $insert=array('name'=>'BUPT');
    $mongo->collectionInsertOne($insert);
    //插入多条数据
    $inserts=array(array('name'=>'BUPT'),array('by'=>'wangyang'));
    $mongo->collectionInsertMany($inserts);

}
function DocumentDelete($mongo){

    //简单的过滤器设置
    $filter=$mongo->filterConstructor('name','=','BUPT');
    //复杂的过滤器
    $filter=$mongo->queryFilterConstructor(array(array('by','=','me'),'or',array('title','=','BUPT')));
    //删除很多条,返回删了多少条
    $deletenum=$mongo->collectionDeleteMany($filter);
    //删除一条
    $mongo->collectionDeleteOne($filter);

}
function DocumentUpdate($mongo){

    //简单的过滤器设置
    $filter=$mongo->filterConstructor('name','=','BUPT');
    //复杂的过滤器
    $filter=$mongo->queryFilterConstructor(array(array('by','=','me'),'or',array('title','=','BUPT')));
    //更新后的键值对
    $update=$mongo->updateConstructor('title','THU');
    //更新一条
    $mongo->collectionUpdateOne($filter,$update);
    //更新很多条
    $mongo->collectionUpdateMany($filter,$update);

}
function DocumentFind($mongo){

    //简单的过滤器设置
    $filter=$mongo->filterConstructor('name','=','BUPT');
    //复杂的过滤器
    $filter=$mongo->queryFilterConstructor(array(array('by','=','me'),'or',array('title','=','BUPT')));
    //选项,目前只提供limit和sort设置,可为空
    $option=$mongo->optionConstructor(4,array('key'=>'_id','value'=>'-1'));
    //查找一条,返回一条数据实例
    $document=$mongo->collectionFindOne($filter,$option);
    //查找许多条,返回数据实例数组
    $documents=$mongo->collectionFindMany($filter,$option);

}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79

4.异常处理
全程try catch,进入一个异常处理函数,详情如下

public function throwException($e){
        if($e instanceof UnsupportedException){
            die("options are used and not supported by the selected server (e.g. collation, readConcern, writeConcern).");
        }elseif($e instanceof InvalidArgumentException){
            die("errors related to the parsing of parameters or options.");
        }elseif($e instanceof MongoDB\Driver\Exception\RuntimeException){
            die("other errors at the driver level (e.g. connection errors).");
        }elseif($e instanceof UnexpectedValueException){
            die(" the command response from the server was malformed.");
        }elseif($e instanceof MongoDB\Driver\Exception\BulkWriteException ){
            die("errors related to the write operation. Users should inspect the value returned by getWriteResult() to determine the nature of the error.");
        }
    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

三.平滑过度

话说的是平滑过度,但实际上并不可能。举例而论,Mongo和MongoDB很多相同功能的函数的返回值都不一样,设计上并不统一。其实这很能反应出老代码的设计模式有没有问题,如果分层清晰,那么在逻辑层里就根本不需要改动,需要改动的只是Model层到扩展层的关联层,但是很遗憾的是往往事与愿违,笔者花费了很多时间在业务代码上来做逻辑改动,只是为了适应新的版本。但是说到底,感觉自己单独设计后台逻辑时,也不会考虑这么多,毕竟谁能想到官方都这么坑呢?想得太多也是给自己挖坑,可能到了过度设计的范畴了。

需要注意的是Mongo的时间类MongoDate已经不再适用,而MongoDB的UTCDateTime的格式并不是简单的unix时间戳,而是以微秒为单位的时间戳,升级的时候需要注意这一点,这意味新的时间戳是13位,而旧的时间戳是10位,而且在获取时间戳的方式上也大不相同,MongoDate中设置了Get/set魔术方法,可以直接获取时间戳属性,而在UTCDateTime中则根本没有public的成员,只能通过调用内部函数获得时间戳。

此外,Mongo中的返回值是array结构的,MongoDB的返回值则是object结构的,需要通过BSONDocument类的getArrayCopy()方法进行转换,笔者通过(array)强制转换也是Ok的。

建议对这些部分也进行一个封装,如下

public static function getUTCDateTime($timestamp=NULL){
        return new MongoDB\BSON\UTCDateTime($timestamp);
    }
  
  
  • 1
  • 2
  • 3

这样通过静态方法可以不实例化直接调用,使用起来很方便。

总的来说,这个从调研到封装到修改测试上线,花了笔者很多的心血,造轮子是一个寂寞又不太容易获取满足感的事情,希望通过写这篇博客,能有所帮助,实际上呢,独立写这样一个封装类,对我而言也是一个极大的锻炼和提高


来源:http://blog.csdn.net/byr_wy/article/details/70768990

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值