Yii2 用动态模型验证接口数据
2024-04-10 07:20:43  阅读数 637

  本人之前用Yii2框架做了一些http接口开发,一般使用模型(yii\base\Model)对数据进行验证。当接口较少的时候还是很方便的,但是如果接口比较多,一个模型中的规则集可能会变得庞大,维护起来就有点麻烦。
  后来想到Yii2支持基于类的action,同时还有动态模型可以使用(yii\base\DynamicModel),就想组装一下,在action中实现对数据的基本验证,即一个接口对应一个action类文件,数据验证规则写在action类中,不同接口的验证规则不会放在一起,减少维护的麻烦。
  下面就拿个演示项目来讲一下具体是如何实现:

  1. 创建项目
    常规做法了,进入nginx的web根目录
    composer create-project --prefer-dist -vv yiisoft/yii2-app-basic yii2-api-demo

  2. 目录结构
    为实现动态模型进行接口数据验证,新建了若干目录及类文件,详见下图:


    02.png
  3. 代码-接口基类

namespace app\common\base;

use app\common\validators\ValidateModel;
use yii\base\Action;
use yii\web\Response;

class BaseAction extends Action
{
    /**
     * @var array
     */
    public $params = [];

    /**
     * 验证属性
     * @return array
     */
    protected function attributes() {
        return [];
    }

    /**
     * 验证规则
     * @return array
     */
    protected function rules() {
        return [];
    }

    /**
     * 初始化
     */
    function init() {
        // 初始化输入参数
        $request = \Yii::$app->request;
        $this->params = array_merge($request->get(), $request->bodyParams);
    }

    /**
     * 创建验证模型
     * @param array $data
     * @param array $append
     * @return ValidateModel
     * @throws \yii\base\InvalidConfigException
     */
    protected function createValideModel(array $data = [], array $append = []) {
        if (empty($data)) {
            $input = file_get_contents('php://input');
            $data = array_merge(\Yii::$app->request->get(), $input ? json_decode($input, true): \Yii::$app->request->bodyParams, $append);
        }
        return ValidateModel::create($this->attributes(), $this->rules(), $data);
    }

    /**
     * 执行验证
     * @param array $data
     * @param array $append
     * @return ValidateModel
     * @throws \Exception
     */
    protected function validate(array $data = [], array $append = []) {
        return $this->createValideModel($data, $append)->execute();
    }

    /**
     * 成功响应
     * @param array $data
     * @param string $message
     * @return array
     */
    protected function success($data = [], $message = '') {
        \Yii::$app->response->format = Response::FORMAT_JSON;
        return ['code' => 0, 'message' => $message, 'data' => $data];
    }

    /**
     * 失败响应
     * @param array $data
     * @param string $message
     * @return array
     */
    protected function error($message, $data = []) {
        \Yii::$app->response->format = Response::FORMAT_JSON;
        return ['code' => 1, 'message' => $message, 'data' => $data];
    }
}
  1. 代码-验证模型
namespace app\common\validators;

use yii\base\DynamicModel;
use yii\base\InvalidConfigException;
use yii\validators\Validator;

class ValidateModel extends DynamicModel
{
    /**
     * 创建
     * @param array $attributes
     * @param array $rules
     * @param array $data
     * @return static
     * @throws InvalidConfigException
     */
    public static function create(array $attributes, array $rules = [], array $data = []) {
        $attributes = self::fillAttributes($attributes, $data);
        $model = new self($attributes);
        return $model->addRules($rules);
    }

    /**
     * 填充属性集
     * @param array $attributes
     * @param array $data
     * @return array
     */
    protected static function fillAttributes(array $attributes, array $data = []) {
        $attrs = [];
        $data || $data = array_merge(\Yii::$app->request->get(), \Yii::$app->request->bodyParams);
        foreach ($attributes as $key) {
            $attrs[$key] = $data[$key] ?? null;
        }
        return $attrs;
    }

    /**
     * @param array $rules
     * @return static
     * @throws InvalidConfigException
     */
    public function addRules(array $rules) {
        $validators = $this->getValidators();
        foreach ($rules as $rule) {
            if ($rule instanceof Validator) {
                $validators->append($rule);
            } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type
                $validator = Validator::createValidator($rule[1], $this, (array)$rule[0], array_slice($rule, 2));
                $validators->append($validator);
            } else {
                throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.');
            }
        }
        return $this;
    }

    /**
     * 执行验证
     * @return static
     * @throws \Exception
     */
    public function execute() {
        $this->validate();
        if ($this->hasErrors()) {
            throw new \Exception($this->getFirstErrorsText());
        }
        return $this;
    }


    /**
     * 获取第一个错误文本信息
     * @param bool $all
     * @return string
     */
    public function getFirstErrorsText(bool $all = false)
    {
        $firstErrors = parent::getFirstErrors();
        if ($firstErrors) {
            return $all ? json_encode($firstErrors) : current($firstErrors);
        } else {
            return '';
        }
    }

    /**
     * 获取属性集
     * @param bool $canNull 是否返回值为null的属性
     * @return array
     */
    public function attrs(bool $canNull = false) {
        $attrs = [];
        foreach ($this->getAttributes() as $name => $val) {
            if ($canNull || $val !== null) {
                $attrs[$name] = $val;
            }
        }
        return $attrs;
    }
}
  1. 代码-列表接口
namespace app\controllers\actions\demo;

use app\common\base\BaseAction;

class IndexAction extends BaseAction
{
    /**
     * 入口
     */
    public function run()
    {
        try {
            // 模拟数据
            $data = [
                ['id' => 1, 'name' => '赵'],
                ['id' => 2, 'name' => '钱'],
                ['id' => 3, 'name' => '孙'],
                ['id' => 4, 'name' => '李'],
                ['id' => 5, 'name' => '周'],
                ['id' => 6, 'name' => '吴'],
                ['id' => 7, 'name' => '陈'],
                ['id' => 8, 'name' => '王'],
                ['id' => 9, 'name' => '冯'],
                ['id' => 10, 'name' => '陈'],
                ['id' => 11, 'name' => '诸'],
                ['id' => 12, 'name' => '卫'],
                ['id' => 13, 'name' => '蒋'],
                ['id' => 14, 'name' => '沈'],
                ['id' => 15, 'name' => '王'],
                ['id' => 16, 'name' => '杨'],
            ];
            
            // 数据验证
            $attrs = $this->validate()->attrs();

            // 根据name查找
            $list = $attrs['name'] ? array_filter($data, function ($v) use($attrs) {
                return $v['name'] == $attrs['name'];
            }) : $data;

            // 模拟分页
            $list && $list = array_slice($list, ($attrs['page'] - 1) * $attrs['rows'], $attrs['rows']);

            return $this->success(['total' => count($list), 'list' => $list]);
        } catch (\Exception $e) {
            return $this->error($e->getMessage());
        }
    }

    /**
     * 可接受的参数字段,rules方法中用到的字段必须先在此定义
     * @return array
     */
    public function attributes()
    {
        return ['name', 'page', 'rows'];
    }

    /**
     * 验证规则,与系统模型中的规则一样的
     * @return array
     */
    public function rules() {
        return [
            ['name', 'string', 'message' => '`名称`不合法'],
            ['name', 'filter', 'filter' => function($v) { return trim($v); }],
            ['page', 'default', 'value' => 1],
            ['page', 'integer', 'min' => 1, 'message' => '`页码`不合法'],
            ['rows', 'default', 'value' => 5],
            ['rows', 'integer', 'min' => 1, 'message' => '`行数`不合法'],
        ];
    }
}

6 代码-详情接口

namespace app\controllers\actions\demo;

use app\common\base\BaseAction;

class ViewAction extends BaseAction
{
    public function run() {
        try {
            // 模拟数据
            $data = [
                ['id' => 1, 'name' => '赵'],
                ['id' => 2, 'name' => '钱'],
                ['id' => 3, 'name' => '孙'],
                ['id' => 4, 'name' => '李'],
                ['id' => 5, 'name' => '周'],
                ['id' => 6, 'name' => '吴'],
                ['id' => 7, 'name' => '郑'],
                ['id' => 8, 'name' => '王'],
                ['id' => 9, 'name' => '冯'],
                ['id' => 10, 'name' => '陈'],
                ['id' => 11, 'name' => '诸'],
                ['id' => 12, 'name' => '卫'],
                ['id' => 13, 'name' => '蒋'],
                ['id' => 14, 'name' => '沈'],
                ['id' => 15, 'name' => '韩'],
                ['id' => 16, 'name' => '杨'],
            ];

            // 数据验证
            $attrs = $this->validate()->attrs();

            // 根据id查找
            $detail = $attrs['id'] ? array_filter($data, function ($v) use($attrs) {
                return $v['id'] == $attrs['id']; 
            }) : [];

            return $this->success($detail ? current($detail) : []);
        } catch (\Exception $e) {
            return $this->error($e->getMessage());
        }
    }


    /**
     * 可接受的参数字段,rules方法中用到的字段必须先在此定义
     * @return array
     */
    public function attributes()
    {
        return ['id']; 
    }

    /**
     * 验证规则,与系统模型中的规则一样的
     * @return array
     */
    public function rules() {
        return [
            ['id', 'required', 'message' => '`id`必须填写'],
            ['id', 'integer', 'min' => 1, 'message' => '`id`不合法'],
        ];
    }
}
  1. 代码-演示控制器
namespace app\controllers;

use yii\web\Controller;

class DemoController extends Controller
{
    /**
     * action映射
     * @return array
     */
    public function actions() {
        return [
            'index' => \app\controllers\actions\demo\IndexAction::class, // 列表
            'view' => \app\controllers\actions\demo\ViewAction::class, // 详情
        ];
    }
}
  1. 演示
    以下是几张postman截图,对比下相应接口中的规则及请求时的参数,就会发现结果还是很好玩的_
    03.png

    03-1.png

    04.png

    04-1.png

总结

好了,就写这么多吧,有空再来优化一下。