一、序言

米扑科技部分项目用到了PHP+MongoDB,因为MongoDB对非结构化数据的存储很方便,又有内存加速机制。

在PHP5及以前,官方提供了两个扩展,Mongo和MongoDB,其中Mongo是对以MongoClient等几个核心类为基础的类群进行操作,封装得很方便,所以基本上都会选择Mongo扩展,

Mongo扩展详情请见官方手册:http://php.net/manual/en/class.mongoclient.php

 

但是,随着PHP5升级到PHP7,官方不再支持Mongo扩展,只支持MongoDB

因为,PHP7的性能提升巨大,让人无法割舍,所以怎么把Mongo替换成MongoDB成为了一个亟待解决的问题。

MongoDB引入了命名空间,但是功能封装非常差,如果非要用原生的扩展,几乎意味着写原生的Mongo语句。

网友对其吐槽如下:

MongoDB\Driver\Manager::executeBulkWrite
这玩意还配置了一个巨大的类库来配合调用:就是这个:https://github.com/mongodb/mongo-php-library   ,纯粹是脱裤子放P,多此一举,本来原生扩展就已经巨长、巨难用,还配一个根本没有简化任何代码的库有什么意义?甚至于调用这个类库比原生的名字更长、更啰嗦,而且多了一层封装就多一次bug机会。
这个库文件总共63个,看看,如果你随便写个脚本访问一下mongodb,还得包含63个文件,我TM整个项目都没有63个文件好不好?这些个写惯了java的没事就老老实实写java,都跑来写php,php是要解析执行的,又不像jvm那么编译优化执行,搞那么多文件,不影响执行速度吗?真是狗屎,所以完全不推荐用这个狗屎库。
但是这个扩展最奇怪的就是文档超级简陋,根本不能获得任何有价值的信息,所以只能靠摸索来了。

 

MongoDB 命令空间的原始封装类,这种想法很违背ORM简化DB IO操作带来的语法问题而专注逻辑优化的思路。

详情也可参见官方手册:http://php.net/manual/en/class.mongodb-driver-manager.php

The MongoDB\Driver\Manager is the main entry point to the extension. It is responsible for maintaining connections to MongoDB (be it standalone server, replica set, or sharded cluster).

No connection to MongoDB is made upon instantiating the Manager. This means the MongoDB\Driver\Manager can always be constructed, even though one or more MongoDB servers are down.

Any write or query can throw connection exceptions as connections are created lazily. A MongoDB server may also become unavailable during the life time of the script. It is therefore important that all actions on the Manager to be wrapped in try/catch statements.

 

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

详情参见:https://github.com/mongodb/mongo-php-library

This library provides a high-level abstraction around the lower-level drivers for PHP and HHVM (i.e. the mongodbextension).

While the extension provides a limited API for executing commands, queries, and write operations, this library implements an API similar to that of the legacy PHP driver. It contains abstractions for client, database, and collection objects, and provides methods for CRUD operations and common commands (e.g. index and collection management).

If you are developing an application with MongoDB, you should consider using this library, or another high-level abstraction, instead of the extension alone.

 

实际上在我们使用的过程中,总是希望能够实现尽可能的解耦,于是分层清晰变得尤为重要。

由于官方的库并不能实现笔者分离和特定的功能需要,于是笔者自己造了一次轮子。

 

二、自我封装的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 

值得注意的是官方的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);
}

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

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

 

 

参考推荐

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

PHP7 连接使用MongoDB API

PHP操作MongoDB数据库

MongoDB 用户认证权限总结