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

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

打开桌面应用端,界面

图1

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

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

图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

打开 http://m.youtube.com/ ,在移动端的显示

图4

打开 http://m.youtube.com/ ,在桌面端与移动端的显示(会自动判断请求端,如果为桌面端,自动跳转至:https://www.youtube.com/ )

图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

复制 /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

在 Postman 中打开桌面端接口

图7

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

在 Postman 中打开移动端接口

图8

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

 

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/250461.html

(0)
上一篇 2022年4月29日
下一篇 2022年4月29日

相关推荐

发表回复

登录后才能评论