在 Yii 2.0 中,基于桌面应用端的 RESTful APIs,在移动应用端的复用、覆盖微调的实现 (一)

1、打开桌面应用端,界面,如图1

图1

2、接口:我的选题(获取选题列表),在 Postman 中的响应结构,如图2

图2

3、在移动应用端的原型设计,如图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

图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

图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

图7

17、在 Postman 中打开移动端接口,如图8

图8

18、如果此接口的移动端与桌面端存在差异,可覆盖 run() 方法,进行调整的。后续争取做到 run() 方法中一部份可复用的代码抽取出来,实现为一个方法(放置于文件:\api\rests\plan\HaveAction.php),这样的话,即使存在差异,也仅需要微调了。

 

永夜