在 Yii 2.0 上,RESTful 风格的 Web Service 服务的 API,PUT 批量更新资源的实现

1、编辑 \api\config\urlManager.php,定义路由以支持 PUT close/{id}

        // 任务管理
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['v1/plan-task'],
            'only' => ['index', 'create', 'view', 'update', 'claim', 'delete', 'finish', 'transfer', 'video-edit', 'write', 'commit-article', 'article-review', 'close'],
            'tokens' => ['{id}' => '<id:\\w[\\w,:;]*>'],
            'extraPatterns' => [
                'PUT claim/{id}' => 'claim',
                'DELETE /{id}' => '',
                'PUT finish/{id}'  => 'finish',
                'GET {id}' => 'view',
                'PUT update' => 'update',
                'PUT transfer/{id}' => 'transfer',
                'GET video-edit/{id}' => 'video-edit',
                'GET write/{id}' => 'write',
                'GET commit-article/{id}' => 'commit-article',
                'GET article-review/{id}' => 'article-review',
                'PUT close/{id}'  => 'close',
            ],
        ],

2、编辑 \api\controllers\PlanTaskController.php,定义动作以支持 close 方法

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/05/05
 * Time: 11:43
 */
namespace api\controllers;

use yii\rest\ActiveController;

class PlanTaskController extends ActiveController
{
    public $serializer = [
        'class'              => 'api\rests\plan_task\Serializer',
        'collectionEnvelope' => 'items',
    ];
    
    /**
     * @inheritdoc
     */    public function actions()
    {
        $actions = parent::actions();
        // 禁用"options"动作
        unset($actions['options']);
        $actions['index']['class'] = 'api\rests\plan_task\IndexAction';
        $actions['create']['class'] = 'api\rests\plan_task\CreateAction';
        $actions['view']['class'] = 'api\rests\plan_task\ViewAction';
        $actions['update']['class'] = 'api\rests\plan_task\UpdateAction';
        $actions['delete']['class'] = 'api\rests\plan_task\DeleteAction';
        $actions['claim'] = [
            'class' => 'api\rests\plan_task\ClaimAction',
            'modelClass' => $this->modelClass,
            'checkAccess' => [$this, 'checkAccess'],
        ];
        $actions['finish'] = [
            'class' => 'api\rests\plan_task\FinishAction',
            'modelClass' => $this->modelClass,
            'checkAccess' => [$this, 'checkAccess'],
        ];
        $actions['transfer'] = [
            'class' => 'api\rests\plan_task\TransferAction',
            'modelClass' => $this->modelClass,
            'checkAccess' => [$this, 'checkAccess'],
        ];
        $actions['video-edit'] = [
            'class' => 'api\rests\plan_task\VideoEditAction',
            'modelClass' => $this->modelClass,
            'checkAccess' => [$this, 'checkAccess'],
        ];
        $actions['write'] = [
            'class' => 'api\rests\plan_task\WriteAction',
            'modelClass' => $this->modelClass,
            'checkAccess' => [$this, 'checkAccess'],
        ];
        $actions['commit-article'] = [
            'class' => 'api\rests\plan_task\CommitArticleAction',
            'modelClass' => $this->modelClass,
            'checkAccess' => [$this, 'checkAccess'],
        ];
        $actions['article-review'] = [
            'class' => 'api\rests\plan_task\ArticleReviewAction',
            'modelClass' => $this->modelClass,
            'checkAccess' => [$this, 'checkAccess'],
        ];
        $actions['close'] = [
            'class' => 'api\rests\plan_task\CloseAction',
            'modelClass' => $this->modelClass,
            'checkAccess' => [$this, 'checkAccess'],
        ];
        return $actions;
    }
}

3、编辑 \api\rests\plan_task\Action.php,复制 public function findModel($id) 为 public function findModels($id),以支持查找多个模型

    /**
     * Returns the data model based on the primary key given.
     * If the data model is not found, a 404 HTTP exception will be raised.
     * @param string $id the ID of the model to be loaded. If the model has a composite primary key,
     * the ID must be a string of the primary key values separated by commas.
     * The order of the primary key values should follow that returned by the `primaryKey()` method
     * of the model.
     * @return ActiveRecordInterface the model found
     * @throws NotFoundHttpException if the model cannot be found
     */    public function findModels($id)
    {
        if ($this->findModel !== null) {
            return call_user_func($this->findModel, $id, $this);
        }

        /* @var $modelClass ActiveRecordInterface */        $modelClass = $this->modelClass;
        $keys = $modelClass::primaryKey();
        if (count($keys) > 1) {
            $values = explode(',', $id);
            if (count($keys) === count($values)) {
                $model = $modelClass::find()->where(['in', $keys, $values])->indexBy($keys)->all();
            }
        } elseif ($id !== null) {
            $values = explode(';', $id);
            $model = $modelClass::find()->where(['in', 'id', $values])->indexBy('id')->all();
        }

        if (!empty($model)) {
            // ID的数目与模型资源数目是否相等,如果不相等,响应失败
            $flipValues = array_flip($values);
            if (count($model) == count($flipValues)) {
                return $model;
            } else {
                $id = implode(";", array_keys(array_diff_key($flipValues, $model)));
            }

        }

        throw new NotFoundHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20014'), ['id' => $id])), 20014);
    }

4、所有ID皆必须存在,否则响应失败,如图1

图1

5、新建 \api\rests\plan_task\CloseAction.php,支持 批量验证模型、批量更新模型

<?php

namespace api\rests\plan_task;

use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
use api\models\PlanTask;
use yii\web\ServerErrorHttpException;

/**
 * 关闭(关闭任务)
 *
 * 1、请求参数列表
 * (1)ids 必填,多个ID(以半角;分隔);
 *
 * 2、输入数据验证规则
 * (1)必填:ids;
 * (2)所有选题任务ID皆必须存在;
 * (3)所有选题任务状态皆必须为,1:未开始;2:进行中(已认领);
 * (4)选题创建用户ID为当前登录用户ID || (栏目人员配置租户ID为当前登录用户租户ID && 栏目人员配置角色标识包含栏目负责人标识 && 栏目人员配置状态的非取值范围 -1、0);
 *
 * 3、操作数据(事务)
 * (1)更新选题任务的状态,4:关闭;
 *
 * For more details and usage information on CloseAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class CloseAction extends Action
{
    /**
     * Updates an existing model.
     *
     * @param string $id the primary key of the model.
     *
     * @return \yii\db\ActiveRecordInterface the model being updated
     * @throws ServerErrorHttpException if there is any error when updating the model
     */    public function run($id)
    {
        /* @var $model ActiveRecord */        $models = $this->findModels($id);

        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id, $models);
        }

        // 状态,不是 1:未开始;2:进行中(已认领) 的ID
        $ids = [];
        foreach ($models as $key => $model) {
            $model->scenario = 'close';
            $models[$key] = $model;
            if ($model->status != PlanTask::PLAN_TASK_STATUS_NOT_BEGINNING && $model->status != PlanTask::PLAN_TASK_STATUS_BEGINNING) {
                $ids[] = $model->id;
            }
        }

        if (!empty($ids)) {
            $ids = implode(";", $ids);
            return ['code' => 20036, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20036'), ['ids' => $ids]))];
        }

        // 批量验证模型
        if (Model::validateMultiple($models)) {
            $modelResult = $model->closeMultiple($models);
            if (!$modelResult) {
                return ['code' => 20038, 'message' => Yii::t('error', '20038')];
            }
        } else {
            foreach ($models as $model) {
                if ($model->hasErrors()) {
                    $response = Yii::$app->getResponse();
                    $response->setStatusCode(422, 'Data Validation Failed.');
                    foreach ($model->getFirstErrors() as $message) {
                        $firstErrors = $message;
                        break;
                    }
                    return ['code' => 20004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20004'), ['firstErrors' => $firstErrors]))];
                }
            }

            throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
        }

        foreach ($models as $key => $model) {
            $model->task_data = unserialize($model->task_data);
            $models[$key] = $model;
        }

        return ['code' => 10000, 'message' => Yii::t('success', '10017'), 'data' => ['items' => array_values($models)]];
        
    }
}

6、编辑 \common\logics\PlanTask.php,支持 验证规则定义、批量更新模型

    /**
     * @inheritdoc
     */    public function rules()
    {
        $rules = [
            /* 关闭(关闭任务) */            [['id'], 'validateClosePermission', 'on' => 'close'], //close
        ];
        $parentRules = parent::rules();
        return ArrayHelper::merge($rules, $parentRules);
    }

    /**
     * Validates the close permission.
     * This method serves as the inline validation for close permission.
     *
     * @param string $attribute the attribute currently being validated
     * @param array $params the additional name-value pairs given in the rule
     */    public function validateClosePermission($attribute, $params)
    {
        if (!$this->hasErrors()) {
            // 当前用户的身份实例,未认证用户则为 Null
            $identity = Yii::$app->user->identity;
            if (!$this->checkColumnOrPlanOwner($identity->group_id, $identity->login_name)) {
                $this->addError($attribute, Yii::t('error', '20037'));
            }
        }
    }

    /**
     * 批量关闭(关闭任务)
     *
     * @param $models
     *
     * @return bool the saved model or false if saving fails
     * @throws \Throwable
     */    public function closeMultiple($models)
    {
        $transaction = Yii::$app->db->beginTransaction();
        try {
            foreach ($models as $model) {
                $model->status = self::PLAN_TASK_STATUS_CLOSE;
                if(!$model->save(false)) {
                    throw new ServerErrorHttpException(Yii::t('error', '20038'), 20038);
                }
            }
            $transaction->commit();

            return true;
        } catch (\Throwable $e) {
            $transaction->rollBack();
            throw $e;
        }
    }

7、执行结果成功,批量更新模型成功,响应成功更新后的模型数据,如图2

图2

{
    "code": 10000,
    "message": "关闭(关闭任务)成功",
    "data": {
        "items": [
            {
                "id": 14,
                "group_id": "015ce30b116ce86058fa6ab4fea4ac63",
                "config_column_id": 1,
                "plan_id": 1,
                "sort_order": 1,
                "title": "无线成都1的任务2",
                "config_task_id": 1,
                "create_user_id": 8,
                "create_name": "13281105967",
                "exec_user_id": 186,
                "exec_name": "test2",
                "place": "<p>选题摘要</p>",
                "task_info": "",
                "task_data": [],
                "ended_at": 1530696654,
                "current_step_id": 0,
                "status": 4,
                "created_at": 1528104654,
                "updated_at": 1528270437
            },
            {
                "id": 15,
                "group_id": "015ce30b116ce86058fa6ab4fea4ac63",
                "config_column_id": 1,
                "plan_id": 1,
                "sort_order": 1,
                "title": "无线成都1的任务1",
                "config_task_id": 1,
                "create_user_id": 8,
                "create_name": "13281105967",
                "exec_user_id": 185,
                "exec_name": "test1",
                "place": "地点",
                "task_info": "<p>选题摘要</p>",
                "task_data": [],
                "ended_at": 1530754303,
                "current_step_id": 90,
                "status": 4,
                "created_at": 1528162303,
                "updated_at": 1528270437
            }
        ]
    }
}
永夜