在 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
<?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() 方法申明
<?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
<?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
<?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,新增:移动端 – 选题
/* 移动端 */ // 移动端 - 选题 [ '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() 方法申明,覆盖方法类文件
<?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
<?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() 方法可删除
<?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() 方法可删除
<?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),这样的话,即使存在差异,也仅需要微调了。
近期评论