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

时间:2022-06-20 15:47:48

一.序言

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

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

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

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

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

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

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

二.自我封装的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();
    }

以上为单例模式的构造方法,显然定义了两个没有意义的获取参数的函数,实际上初始化的入口应该在继承的子类中完成重写。每一个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);
    }

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

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;

    }

在处理的时候用到了栈的思想,在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);

}

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.");
        }
    }

三.平滑过度

话说的是平滑过度,但实际上并不可能。举例而论,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);
    }

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

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