一.序言
使用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);
}
这样通过静态方法可以不实例化直接调用,使用起来很方便。
总的来说,这个从调研到封装到修改测试上线,花了笔者很多的心血,造*是一个寂寞又不太容易获取满足感的事情,希望通过写这篇博客,能有所帮助,实际上呢,独立写这样一个封装类,对我而言也是一个极大的锻炼和提高