在 Yii 2.0 中,基于桌面应用端的 RESTful APIs,在移动应用端的复用、覆盖微调的实现 (一)
1、打开桌面应用端,界面,如图1
2、接口:我的选题(获取选题列表),在 Postman 中的响应结构,如图2
3、在移动应用端的原型设计,如图3
4、现阶段的需求:后续迭代开发阶段,桌面端与移动端,在同一个接口:我的选题(获取选题列表)中,不可避免地会存在一定的差异性,所以,决定分别规划出对应的路由以及对应的 Action 入口文件,但是,在当前阶段/后续迭代开发阶段中的大部分接口,桌面端与移动端是完全一致的,所以,希望能够复用 Action 中的实现,只有当存在差异的时候,才覆盖微调。其理念可参考(用于移动和桌面的单独站点):https://developer.mozilla.org/en-US/docs/Web/Guide/Mobile/Separate_sites ,打开 http://m.youtube.com/ ,在桌面端与移动端的显示(会自动判断请求端,如果为桌面端,自动跳转至:https://www.youtube.com/ ),如图4、图5
5、urlManager 应用程序组件的配置,\api\config\urlManager.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <?php return [ 'class' => yii\web\UrlManager:: class , 'enablePrettyUrl' => true, 'enableStrictParsing' => true, 'showScriptName' => false, 'rules' => [ [ 'class' => 'yii\rest\UrlRule' , 'controller' => [ 'v1/user' ], ], /* 选题管理 */ [ 'class' => 'yii\rest\UrlRule' , 'controller' => [ 'v1/plan' ], 'only' => [ 'index' , 'create' , 'view' , 'update' , 'delete' , 'have' , 'wait-review' , 'cmc-group' , 'edit' , 'submit' , 'refuse' , 'pass' , 'return' , 'disable' , 'enable' , 'invite' , 'invite-accept' ], 'tokens' => [ '{id}' => '<id:\\w[\\w,:;]*>' ], 'extraPatterns' => [ 'GET have' => 'have' , 'GET wait-review' => 'wait-review' , 'GET cmc-group/{id}' => 'cmc-group' , 'GET edit/{id}' => 'edit' , 'PUT submit/{id}' => 'submit' , 'PUT refuse/{id}' => 'refuse' , 'PUT pass/{id}' => 'pass' , 'PUT return/{id}' => 'return' , 'PUT disable/{id}' => 'disable' , 'PUT enable/{id}' => 'enable' , 'POST invite/{id}' => 'invite' , 'PUT invite-accept/{id}' => 'invite-accept' , ], ], ], ]; |
6、控制器类:\api\controllers\PlanController.php,通过 actions() 方法申明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | <?php namespace api\controllers; use yii\rest\ActiveController; class PlanController extends ActiveController { public $serializer = [ 'class' => 'api\rests\plan\Serializer' , 'collectionEnvelope' => 'items' , ]; /** * @inheritdoc */ public function actions() { $actions = parent::actions(); // 禁用"options"动作 unset( $actions [ 'options' ]); $actions [ 'index' ][ 'class' ] = 'api\rests\plan\IndexAction' ; $actions [ 'create' ][ 'class' ] = 'api\rests\plan\CreateAction' ; $actions [ 'view' ][ 'class' ] = 'api\rests\plan\ViewAction' ; $actions [ 'update' ][ 'class' ] = 'api\rests\plan\UpdateAction' ; $actions [ 'delete' ][ 'class' ] = 'api\rests\plan\DeleteAction' ; $actions [ 'have' ] = [ 'class' => 'api\rests\plan\HaveAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'wait-review' ] = [ 'class' => 'api\rests\plan\WaitReviewAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'cmc-group' ] = [ 'class' => 'api\rests\plan\CmcGroupAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'edit' ] = [ 'class' => 'api\rests\plan\EditAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'submit' ] = [ 'class' => 'api\rests\plan\SubmitAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'refuse' ] = [ 'class' => 'api\rests\plan\RefuseAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'pass' ] = [ 'class' => 'api\rests\plan\PassAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'return' ] = [ 'class' => 'api\rests\plan\ReturnAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'disable' ] = [ 'class' => 'api\rests\plan\DisableAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'enable' ] = [ 'class' => 'api\rests\plan\EnableAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'invite' ] = [ 'class' => 'api\rests\plan\InviteAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; $actions [ 'invite-accept' ] = [ 'class' => 'api\rests\plan\InviteAcceptAction' , 'modelClass' => $this ->modelClass, 'checkAccess' => [ $this , 'checkAccess' ], ]; return $actions ; } } |
7、使用模块,将不同版本的代码隔离,\api\modules\v1\controllers\PlanController.php
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace api\modules\v1\controllers; /** * Plan controller for the `v1` module */ class PlanController extends \api\controllers\PlanController { public $modelClass = 'api\modules\v1\models\Plan' ; } |
8、Action 动作文件:\api\rests\plan\HaveAction.php 继承至 \yii\rest\ActiveController 默认提供的动作:\yii\rest\IndexAction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | <?php /** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace api\rests\plan; use Yii; use api\models\Plan; use api\models\PlanQuery; use api\models\redis\cmc_console\User as RedisCmcConsoleUser; use yii\base\InvalidConfigException; use yii\data\ActiveDataProvider; use yii\web\UnprocessableEntityHttpException; /** * 我的选题(获取选题列表):/plans/have(plan/have) * * 1、请求参数列表 * (1)filter[created_at][gte]:可选,选题的开始时间,默认:null * (2)filter[created_at][lte]:可选,选题的结束时间,默认:null * (3)filter[status]:可选,选题状态,0:禁用;1:编辑;2:待审;3:通过;4:拒绝;5:指派;6:完成,默认:null * (4)filter[title][like]:可选,选题标题,默认:null * * 2、输入数据验证规则 * (1)整数:created_at、status * (2)字符串(最大长度:64):title * (3)范围([0, 1, 2, 3, 4, 5, 6]):status * * 3、查询规则 * (1)栏目是否被删除,0:否 * (2)选题是否被删除,0:否 * (3)(选题的租户ID为当前租户ID && 选题创建用户ID为当前登录用户ID && 栏目人员是否被删除,0:否) (选题的租户ID为当前租户ID && 选题执行(负责)用户ID为当前登录用户ID && 栏目人员是否被删除,0:否) || (选题的租户ID为当前租户ID && 栏目人员配置角色标识包含栏目负责人标识) || (选题模型的是否跨租户(不隔离),1:是 && 选题与租户的关联模型的关联的租户ID为当前租户ID && 选题与租户的关联模型的是否邀请者,0:否 && 选题与租户的关联模型的接受状态,0:待接受;2:已拒绝 && 选题与租户的关联模型是否被删除,0:否) || (选题模型的是否跨租户(不隔离),1:是 && 选题与租户的关联模型的关联的租户ID为当前租户ID && 选题与租户的关联模型的是否邀请者,0:否 && 选题与租户的关联模型的接受状态,1:已接受 && 选题与租户的关联模型的接受用户ID为当前登录用户ID && 选题与租户的关联模型是否被删除,0:否 && 栏目人员是否被删除,0:否) || (选题模型的是否跨租户(不隔离),1:是 && 选题与租户的关联模型的关联的租户ID为当前租户ID && 选题与租户的关联模型的是否邀请者,0:否 && 选题与租户的关联模型的接受状态,1:已接受 && 选题与租户的关联模型是否被删除,0:否 && 栏目人员配置角色标识包含栏目负责人标识) * * For more details and usage information on IndexAction, see the [guide article on rest controllers](guide:rest-controllers). * * @author Qiang Wang <shuijingwanwq@163.com> * @since 1.0 */ class HaveAction extends \yii\rest\IndexAction { public $dataFilter = [ 'class' => 'yii\data\ActiveDataFilter' , 'searchModel' => 'api\models\PlanSearch' , 'attributeMap' => [ 'created_at' => '{{%plan}}.[[created_at]]' , 'status' => '{{%plan}}.[[status]]' , 'title' => '{{%plan}}.[[title]]' , ], ]; /** * Prepares the data provider that should return the requested collection of the models. * @return ActiveDataProvider * @throws InvalidConfigException if a registered parser does not implement the [[RequestParserInterface]]. * @throws UnprocessableEntityHttpException */ protected function prepareDataProvider() { $requestParams = Yii:: $app ->getRequest()->getBodyParams(); if ( empty ( $requestParams )) { $requestParams = Yii:: $app ->getRequest()->getQueryParams(); } $filter = null; if ( $this ->dataFilter !== null) { $this ->dataFilter = Yii::createObject( $this ->dataFilter); if ( $this ->dataFilter->load( $requestParams )) { $filter = $this ->dataFilter->build(); if ( $filter === false) { $firstError = '' ; foreach ( $this ->dataFilter->getFirstErrors() as $message ) { $firstError = $message ; break ; } throw new UnprocessableEntityHttpException(Yii::t( 'error' , Yii::t( 'error' , Yii::t( 'error' , '224003' ), [ 'first_error' => $firstError ])), 224003); } } } if ( $this ->prepareDataProvider !== null) { return call_user_func( $this ->prepareDataProvider, $this , $filter ); } // 当前用户的身份实例,未认证用户则为 Null /* @var $identity RedisCmcConsoleUser */ $identity = Yii:: $app ->user->identity; /* @var $modelClass Plan */ $modelClass = $this ->modelClass; /* @var $haveQuery PlanQuery */ // 获取查询对象(我的选题(获取选题列表)) $haveQuery = $modelClass ::getHaveQuery( $identity ); $query = $haveQuery ->orderBy([ $modelClass ::tableName() . '.id' => SORT_DESC]); if (! empty ( $filter )) { $query ->andFilterWhere( $filter ); } return Yii::createObject([ 'class' => ActiveDataProvider::className(), 'query' => $query , 'pagination' => [ 'params' => $requestParams , ], 'sort' => [ 'params' => $requestParams , ], ]); } } |
9、由于同一个接口,其业务逻辑基本上是一致的,而业务逻辑的实现存在于目录:\common\models、\common\logics、\common\services、\api\models、\api\services。因此,无需要基于模块来区分,可基于控制器的子目录来实现。新建移动端目录:\api\controllers\mobile,复制 \api\controllers\PlanController.php 至 \api\controllers\mobile\PlanController.php
10、urlManager 应用程序组件的配置,\api\config\urlManager.php,新增:移动端 – 选题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /* 移动端 */ // 移动端 - 选题 [ 'class' => 'yii\rest\UrlRule' , 'controller' => [ 'v1/mobile/plan' ], 'only' => [ 'index' , 'create' , 'view' , 'update' , 'delete' , 'have' , 'wait-review' , 'cmc-group' , 'edit' , 'submit' , 'refuse' , 'pass' , 'return' , 'disable' , 'enable' , 'invite' , 'invite-accept' ], 'tokens' => [ '{id}' => '<id:\\w[\\w,:;]*>' ], 'extraPatterns' => [ 'GET have' => 'have' , 'GET wait-review' => 'wait-review' , 'GET cmc-group/{id}' => 'cmc-group' , 'GET edit/{id}' => 'edit' , 'PUT submit/{id}' => 'submit' , 'PUT refuse/{id}' => 'refuse' , 'PUT pass/{id}' => 'pass' , 'PUT return/{id}' => 'return' , 'PUT disable/{id}' => 'disable' , 'PUT enable/{id}' => 'enable' , 'POST invite/{id}' => 'invite' , 'PUT invite-accept/{id}' => 'invite-accept' , ], ], |
11、控制器类:\api\controllers\mobile\PlanController.php,继承至:\api\controllers\PlanController,通过 actions() 方法申明,覆盖方法类文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <?php namespace api\controllers\mobile; class PlanController extends \api\controllers\PlanController { public $serializer = [ 'class' => 'api\rests\mobile\plan\Serializer' , 'collectionEnvelope' => 'items' , ]; /** * @inheritdoc */ public function actions() { $actions = parent::actions(); $actions [ 'index' ][ 'class' ] = 'api\rests\mobile\plan\IndexAction' ; $actions [ 'create' ][ 'class' ] = 'api\rests\mobile\plan\CreateAction' ; $actions [ 'view' ][ 'class' ] = 'api\rests\mobile\plan\ViewAction' ; $actions [ 'update' ][ 'class' ] = 'api\rests\mobile\plan\UpdateAction' ; $actions [ 'delete' ][ 'class' ] = 'api\rests\mobile\plan\DeleteAction' ; $actions [ 'have' ][ 'class' ] = 'api\rests\mobile\plan\HaveAction' ; $actions [ 'wait-review' ][ 'class' ] = 'api\rests\mobile\plan\WaitReviewAction' ; $actions [ 'cmc-group' ][ 'class' ] = 'api\rests\mobile\plan\CmcGroupAction' ; $actions [ 'edit' ][ 'class' ] = 'api\rests\mobile\plan\EditAction' ; $actions [ 'submit' ][ 'class' ] = 'api\rests\mobile\plan\SubmitAction' ; $actions [ 'refuse' ][ 'class' ] = 'api\rests\mobile\plan\RefuseAction' ; $actions [ 'pass' ][ 'class' ] = 'api\rests\mobile\plan\PassAction' ; $actions [ 'return' ][ 'class' ] = 'api\rests\mobile\plan\ReturnAction' ; $actions [ 'disable' ][ 'class' ] = 'api\rests\mobile\plan\DisableAction' ; $actions [ 'enable' ][ 'class' ] = 'api\rests\mobile\plan\EnableAction' ; $actions [ 'invite' ][ 'class' ] = 'api\rests\mobile\plan\InviteAction' ; $actions [ 'invite-accept' ][ 'class' ] = 'api\rests\mobile\plan\InviteAcceptAction' ; return $actions ; } } |
12、使用模块,将不同版本的代码隔离,复制 \api\modules\v1\controllers\PlanController.php 至 \api\modules\v1\controllers\mobile\PlanController.php,其继承至 \api\controllers\mobile\PlanController
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace api\modules\v1\controllers\mobile; /** * Plan controller for the `v1` module */ class PlanController extends \api\controllers\mobile\PlanController { public $modelClass = 'api\modules\v1\models\Plan' ; } |
13、复制 \api\rests\plan 至 \api\rests\mobile\plan,批量替换命名空间,如图6
14、编辑 Action 文件:\api\rests\mobile\plan\HaveAction.php,继承至 \api\rests\plan\HaveAction,由于此接口的移动端与桌面端暂无差异,因此,文件中的 run() 方法可删除
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php /** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace api\rests\mobile\plan; class HaveAction extends \api\rests\plan\HaveAction { } |
15、资源对象转换为数组,编辑数据序列化文件:\api\rests\mobile\plan\Serializer.php,继承至 \api\rests\plan\Serializer,由于此接口的移动端与桌面端暂无差异,因此,文件中的 serializeDataProvider() 方法可删除
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php /** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace api\rests\mobile\plan; class Serializer extends \api\rests\plan\Serializer { } |
16、在 Postman 中打开桌面端接口,如图7
17、在 Postman 中打开移动端接口,如图8
18、如果此接口的移动端与桌面端存在差异,可覆盖 run() 方法,进行调整的。后续争取做到 run() 方法中一部份可复用的代码抽取出来,实现为一个方法(放置于文件:\api\rests\plan\HaveAction.php),这样的话,即使存在差异,也仅需要微调了。
近期评论