fred-ye / summary

my blog
43 stars 9 forks source link

[PHP]MongoDB与Yii搭建API服务-2 #41

Open fred-ye opened 9 years ago

fred-ye commented 9 years ago

MongoDB与Yii搭建API服务-2

在上一篇文章中主要记录了Yii对MongoDB的基本操作,本文章主要记录整个API设计的一个大概结构。我做PHP较少,不知道更好的设计是什么样子,但是从觉得当前的设计来看,结构还是蛮清晰的,感觉还是不错的。以前做javaWeb的时候,结构差不多也是这样设计,但和PHP相比,java确实是重一些。在此之前,推荐一个不错的API设计的文章,点这里

Model的设计

在上一篇文章中其实已经介绍过了Model的设计,此时再回顾一下。首先,我们会定义一个BaseModel,这个里面定义了一些公用的属性和rules,其它的实体类需要继承自它。通常在Model中会添加deleted, created_time, updated_timeMongoDB中的主键是_id,这BaseModel中定义了,其它子类中也不用再写了。由于BaseModel没有对应的collection,所以getCollectionName方法必须返回nullrules里面定义的是一些基本的验证规则。在子类中,可以将这些rules Merge进去。

<?php
class BaseModel extends EMongoDocument
{
    public $_id;
    public $created_time;
    public $updated_time;
    public $deleted;
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function getCollectionName()
    {
        return null;
    }
    public function rules()
    {
        return array(
            array('_id', 'default', 'value' => new MongoId()),
            array('deleted', 'default', 'value' => Constant::UNDELETE),
            array('created_time', 'default', 'value' => new MongoDate()),
            array('updated_time', 'default', 'value' => new MongoDate()),
        );
    }
    public function attributeLabels()
    {
    }
}

User类.

class User extends BaseModel {
    public $name;
    public $password;
    public $nickname;
    public $email;
    public $height;
    public $weight;
    public $gender;
    public $register_time;
    public $last_login_time;

    public static function model($className=__CLASS__) {
        return parent::model($className);
    }

    /**
     * @return string the associated collection name
     */
    public function getCollectionName() {
        return 'user';
    }

    public function rules() {
        return  array_merge(parent::rules(), array(
            ... //User类中属性的验证规则
        ));
    }
}

Error Message的设计

作为API端,为移动端提供数据,在正常情况下会直接返回移动端所需要的数据。但由于其响应结果会受移动端传过来的参数,网络,Server等从多因素的影响,肯定会有Error Message的输出。很容易理解一个例子就是,当用户登录时,如果输错了用户名和密码,则不能进入系统,此时的API返回的便是一个user name or password error。 在Server端返回数据的时候,通常会返回一个json字符串,一般来讲我们会定义这个字符串的返回格式:

{status:"fail", error_code: 2010001, error_msg: "user name or pwd error"};

error_code:错误代码,在开发过程中可以约定,error_code由6位整数组成,第一位代表error的级别,如1表示系统级别的error(DB error), 2代表服务级别的错误。第二位和第三位代表某个feature, 如10代表用户模块。后三位代表具体的错误,001代表用户名和密码错误,002代表该帐号已锁定,等等。

error_msg:是对错误的简单描述,这个error_msg通常是给程序员看的,所以里面的内容不太友好也没有关系。当移动端如果接收到这个error_msg时,不会把它显示出来给最终用户,移动端的程序应该读取server端返回的error_code, 然后根据这个error_code加载相应的错误消息返回给最终用户。 所以一般来讲,我们会在{Yii}/protected/components下定义两个类,一个是ErrorCode, 一个是ErrorMsg。两个类中的属性都是常量,如下:

class ErrorCode {
    // System level error here
    const SERVER_ERROR = 101001;
    const DB_ERROR = 101002;
    const ILLEGAL_PARAMETER = 101003;
    .....

    const USER_NAME_EXISTED = 2010001;
}
class ErrorMsg {
    // System level error here
    const SERVER_ERROR = "Server error";
    const DB_ERROR = "Database error";
    const ILLEGAL_PARAMETER = "Illegal params";
    .....

    const USER_NAME_EXISTED = "user name existed";
}

Controller的设计

Controller是API端和移动端数据交换的接口,接收移动端的请求,并做相应处理后,将结果返回给移动端。作何一个项目会有多个Controller, 在设计Controller之前,我们可以分析一下,大部份Controller都会做这么几件事:

  1. 接收移动端的请求数据。
  2. 返回结果给移动端。
  3. 对于大多数Controller中的方法(对应的是一个URL),一定是只有用户登录后才可以访问,比如,修改密码。而大部份API不会做Session,此时可能就需要一个userId或者token之类的东西,表示当前用户的一个身份。
  4. .....

于是,想着和Model设计一样的方式,也定义一个BaseController, BaseController是继承自CController.

针对以上的几个问题:

1.假设我们和移动端的开发约定,移动端如果需要传参数,只能发post请求,且请求体必须是格式良好的json, 于是我们就可以添加一个方法getRequestParams()来获取请求的数据.

protected function getRequestParams() {
    $params = Yii::app()->request->getRawBody();
    if(isset($params)) {
        try{
            $params = CJSON::decode($params);
            return is_array($params) ? $params : array();
        } catch (Exception $e) {
            Yii::log('Param parse error:' . $params, CLogger::LEVEL_ERROR, 'BaseController');
            //给前端返回一个error msg.
        }
    } else {
        return array();
    }
}

2.API返回给移动端的数据全部都是json格式,于是定义一个response方法。

protected function response($response) {
    header('Content-type: application/json');
    echo CJSON::encode($response);
    Yii::app()->end();
}

3.针对特定API的访问。在Yii的CController中有一个beforeAction方法,从名字上看就知道这个方法一定是在每一个Actioin调用之前执行。为此我们覆盖这个方法, 在里面定义我们自己的规则:

public function beforeAction($action) {
    $this->initRequestUser();
    return true;
} 

private function initRequestUser() {
    $id = Yii::app()->request->getQuery('user_id');
    $actionItem = $this->getRoute();

    if('login' == $actionItem
        || 'register' == $actionItem
        || ......
        ) {

       return ;
    }
    //对于其它的URL如果没有传user_id这个参数,就认为是非法参数,不做处理。
    if(!isset($id)) {
            $this->error(Yii::app()->request->requestUri, ErrorCode::EMPTY_USERID, ErrorMsg::EMPTY_USERID);
    }
}

在API的设计时,我们是将user_id这个参数直接带在url上,并没有做为请求体中的参数(json)传过来。如修改密码的url为/api/changpassword/ab33397997cd7989e9d0。其中ab33397997cd7989e9d0便是user_id。在方法initRequestUser中id的获取$id = Yii::app()->request->getQuery('user_id'); ,是因为我们采用的URL重写规则是

'urlManager'=>array(
    'urlFormat'=>'path',
    'caseSensitive'=>false,
    'showScriptName'=>false,
    'rules'=>array(
             '<controller:\w+>/<action:\w+>/<user_id:.*?>'=>'<controller>/<action>',
             '<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
    ),
)