一.序言
从PHP5到PHP7自我封装MongoDB以及平滑升级,使用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); }
这样通过静态方法可以不实例化直接调用,使用起来很方便。