分享一个多功能的验证器,可以对数据进行转换、验证、过滤

chaz6chez

这个项目由来很久了,大概至少有6年的历史,最早我工作的时候接触的一个项目叫struct,他可以把数组映射到对象属性上进行数据类型内容等判断或者过滤,也可以进行转换,也可以自行注册handle进行过滤、判断;后来我把这个项目重构了一下,适用在常驻内存的环境下,同时,这个项目也是我用在生产环境中的一个项目;

这个项目有点像残疾的注解,不过我觉得用起来还挺好的,所以推荐给大家

测试覆盖率应该超过了80%,常用的几种方案都是100%覆盖的;

项目地址:https://github.com/chaz6chez/structure

1.x已经没有再维护了,现在只维护2.x

引入

composer require chaz6chez/structure

应用场景

  • 出入参的判断及过滤
  • 数据转化及映射

示例

验证场景

  • 创建一个Structure
namespace YoursNamespace;
use Structure\Struct;
class User extends Struct {

    // 当id为非null值时,必须满足int 10 < id < 20
    /**
     * @rule int,min:10,max:20|id format error:1001
     */
    public $id; 

    // name为必填,必须满足string 1 < name长度 < 20
    /**
     * @rule string,min:1,max:20|name format error:2001
     * @required true|name cannot be empty:2002 
     */
    public $name;

    // 当sex为null时默认 string female
    // 满足string 0 < sex长度 < 10
    /**
     * @rule string,min:0,max:10|sex format error:1001
     * @default string:female
     */
    public $sex;
}
  • 使用
    $struct = \YoursNamespace\User::factory();
    $struct->create([
        'id' => 12,
        'name' => 'John Smith'
    ]);

    // or

    $struct = \YoursNamespace\User::factory([
        'id' => 12,
        'name' => 'John Smith'
    ]);

    // or

    $struct = new \YoursNamespace\User();
    $struct = \YoursNamespace\User::factory();

    $struct->id = 12;
    $struct->name = 'John Smith';

    // or

    $struct = new \YoursNamespace\User([
        'id' => 12,
        'name' => 'John Smith'
    ]);

    if($struct->hasError()) {
        throw new \RuntimeException(
            $struct->getError()->getMessage() . '->' . $struct->getError()->getPosition(),
            $struct->getError()->getCode()
        );
    }
    return $struct->output(); // array

使用说明

  • 继承 Structure\Struct 及实现结构体
  • public属性接参
namespace Example;

use Structure\Struct;

class User extends Struct{
    public $id;
    public $name;
    public $sex;
}
  • 对要操作和转化的public属性进行注释

    /**
     * @rule string,min:10,max:20|name format error:1001 
     */
    public $name;
  • 标签分为四个区域
    • a区:标签区
    • b区:场景区
    • c区:验证区
    • d区:内容信息
/**
 *  a区   b区        c区              d区
 * @标签 [场景]   验证方式   | 错误信息     : 错误码
 *      ↓           ↓           ↓             ↓
 * @rule[check] string,min:1|error message:error code  
 */                       

标签区:

  • 转换类的标签配合 filter()在output() 方法内生效,
    会对包含该标签的属性执行转换或者过滤操作

  • 验证类的标签在 validate() 中生效返回布尔值,
    通过getError() 可以获得错误信息

标签名 方式 类型 说明
@default Structure\Handler、func、method 转换 func与method是将返回值默认赋予该标签
@required true 验证 判断是否为必要值
@rule Structure\Handler、func、method 验证 以func与method的bool返回类型判断验证
@skip 验证 跳过验证
@ghost 转换 跳过输出
@key 转换 标记钥匙属性
@mapping 映射键名 转换 映射键转换
@operator true、func、method 转换 键值特殊转换

@default

  • 将该属性标记默认模式
  • 当该属性值为null且具备@default标签时生效
    /**
     * @default string:abc
     * @default int:123
     * @default float:1.1
     * @default object:Handler\Help
     * @default map:{"a":"1"}
     * @default array:["1"]
     * @default bool:true
     */
    public $name;
  • 验证区可使用func、method进行方法赋值
    • method:className,methodName 必须是静态方法
    • 方法执行过程中抛出的任何异常都会被忽略,并以默认Null赋值
    /**
     * @default func:is_array              会找到is_array函数
     * @default method:_set                会定位当前类的_set方法
     * @default method:Handler\Help,get   会定位Handler\Help类的get方法 
     */
    public $name;
    public static function _set(){
        return 'abc';
    }
  • @default仅在output()输出时生效,若要直接使用类属性获取@default赋值,请使用以下方法:
  • 但不建议频繁使用,会多执行一次object clone操作
    // 以name的@default标签为string:John举例
    /**
     * @default string:John
     */
    public $name;
    $struct = new Struct();
    // @default无法生效,值为null
    $struct->name;
    // @default可以生效,值为字符串John
    $struct()->name;

@required

    /**
     * @required true|name cannot empty
     */
    public $name;

@rule

  • 通过预置Handler进行验证
    /**
     * @rule string,min:10,max:20|name format error
     * @rule int,min:10,max:20|name format error
     * @rule float,min:1.0,max:2.1,scale:3|name format error
     * @rule bool,true|name format error
     * @rule object,class:|name format error
     * @rule array,min:10,max:20,values:string|name format error
     * @rule map,min:1,max:5,keys:string,values:int|name format error
     * @rule url,path:true,query:true|name format error
     * @rule ip,ipv4:false,ipv6:false,private:false,reserved:false|name format error
     * @rule regex,min:10,max:20,regex:/.?/|name format error
     */
    public $name;
  • 验证区可使用func、method进行方法判断
    • method:className,methodName 必须是静态方法
    • 方法执行过程中任何异常会转化成StructureException抛出
    /**
     * @rule func:_set                  会找到_set函数
     * @rule method:_set                会定位当前类的_set方法
     * @rule method:Handler\Help,get   会定位Handler\Help类的get方法
     */
    public $name;

    public static function _set($value) : bool
    {
        return $value === '_method';
    }
}

function _set($value) : bool
{
    return $value === '_func';
}

@ghost

  • output() 不会输出该标签
    // @ghost true
    $user->id = 'id';

    $user->name = 'name';
    $user->output();
    // 以上会输出
    [
        'name' => 'name'
    ];

    $user->output(true);
    // 以上会输出
    [
        'id' => 'id',
        'name' => 'name'
    ];

@key

  • 将该属性标记钥匙字段
  • 通过 filter()->output() 可以做到仅输出钥匙字段
    // @key true
    $user->id = 'id';

    $user->name = 'name';
    $user->filter(STRUCT_FILTER_KEY)->output();
    // 以上会输出
    [
        'name' => 'name'
    ];

    $user->transfer(STRUCT_FILTER_KEY_REVERSE)->output();
    // 以上会输出
    [
        'id' => 'id',
    ];

@skip

  • 跳过验证,但不影响输出

@operator

1. 识别medoo语法-where 并转换

    /**
     * @operator true 
     */ 
    public $name;

通过 transfer()->output() 可以做到转换输出

    // @operator true
    $user->id = 'abc[>]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出
    [
        'id[>]' => 'abc'
    ];

    $user->id = '123,456[<>]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出
    [
        'id[<>]' => ['123','456'],
    ];
2.2以上版本完善了该标签下的类型转换
2.2以下版本中不会对类型转换
2.2以下版本:
    // @operator true
    $user->id = '123[>]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出
    [
        'id[>]' => '123'
    ];
    // 并非期待的
    [
       'id[>]' => 123
    ];
    // 此种状况会影响数据库查询的索引
2.2以上版本:
  • 不仅可以配合medoo语法做处理转换,也可以直接做类型转换
  • 字符串类型的数字默认会根据值类型转换
    • 整型内容字符串转换成整型
    • 小数字符串转换成浮点型
    // @operator true
    $user->id = '123[>]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id[>]' => 123
    ];

    // @operator true
    $user->id = '123';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id' => 123
    ];

    $user->id = '1.23[>]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 float
    [
        'id[>]' => 1.23
    ];

    $user->id = '1.23';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出
    [
        'id' => 1.23
    ];
  • 可以使用强制转换标签

# String :

    // @operator true
    $user->id = '123[String][>]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id[>]' => '123'
    ];

    // @operator true
    $user->id = '123[String]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id' => '123'
    ];

# Int :

    // @operator true
    $user->id = '1[Int][>]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id[>]' => 1
    ];

    // @operator true
    $user->id = '1[Int]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id' => 1
    ];

# Float :

    // @operator true
    $user->id = '123[Float][>]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id[>]' => 123.0
    ];

    // @operator true
    $user->id = '123[Float]';
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id' => 123.0
    ];

# Bool :

    // @operator true
    $user->id = '1[Bool][>]'; // 0 false
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id[>]' => true
    ];

    // @operator true
    $user->id = '1[Bool]'; // 0 false
    $user->transfer(STRUCT_TRANSFER_OPERATOR)->output();
    // 以上会输出 int
    [
        'id' => true
    ];

2.使用func、method进行转换

  • method:className,methodName 必须是静态方法
  • 方法执行过程中任何异常会转化成StructureException抛出

    /**
     * @operator func:_add                  相当于_add($name)
     * @operator method:_add                相当于$this->_add($name)
     * @operator method:Handler\Help,_add  相当于Handler\Help::_add($name)
     */
    public $name;

@mapping

  • 将该属性标记映射处理
  • 通过 transfer()->output() 可以做到转换输出
    // @mapping key
    $user->id = 123;

    $user->name = 'john';
    $user->output();
    // 输出
    [
        'id' => 123,
        'name' => 'john'
    ];

    $user->transfer(STRUCT_TRANSFER_MAPPING)->output();
    // 输出
    [
        'key' => 123,
        'name' => 'john'
    ];

方法

  • 实例化
    $user = User::factory([
        'id' => 1,
        'name' => 'john'
    ],'check');
  • 输入

    • 使用create方法输入数据
    • 使用属性赋值输入数据
    • 使用create可以保存原始数据,使用属性赋值则不会保留原始数据
    // 1.使用create输入数据
    $user->create([
        'id' => 1,
        'name' => 'john'
    ]); // return $this
    // 2.使用属性赋值输入数据
    $user->id = 1;
    $user->name = 'john';

    // 使用create可以保存原始数据,建议使用create输入数据
  • 获取原始数据
    $user->getRaw(); // return array
  • 设置场景
    $user->scene('check'); // return $this
  • 转换

    • STRUCT_TRANSFER_MAPPING
    • STRUCT_TRANSFER_OPERATOR
    $user->transfer(STRUCT_TRANSFER_MAPPING); // return $this

    // STRUCT_TRANSFER_MAPPING
    // STRUCT_TRANSFER_OPERATOR

    // 接受可变长参数
    $user->transfer(
        STRUCT_TRANSFER_MAPPING,
        STRUCT_TRANSFER_OPERATOR
    ); // return $this
  • 过滤
    • STRUCT_FILTER_NULL
    • STRUCT_FILTER_EMPTY
    • STRUCT_FILTER_ZERO
    • STRUCT_FILTER_KEY
    • STRUCT_FILTER_KEY_REVERSE
    • STRUCT_FILTER_OPERATOR
    • STRUCT_FILTER_OPERATOR_REVERSE
    $user->filter(STRUCT_FILTER_NULL); // return $this

    // STRUCT_FILTER_NULL
    // STRUCT_FILTER_EMPTY
    // STRUCT_FILTER_ZERO
    // STRUCT_FILTER_KEY
    // STRUCT_FILTER_KEY_REVERSE
    // STRUCT_FILTER_OPERATOR
    // STRUCT_FILTER_OPERATOR_REVERSE

    // 接受可变长参数
    $user->filter(
        STRUCT_FILTER_NULL,
        STRUCT_FILTER_EMPTY
    ); // return $this
  • 验证
    $user->validate(); // return bool
    $user->hasError(); // return bool

    // true 有错误,验证未通过
    // false 无错误,验证通过
  • 获取错误

    • 需要在验证执行后才能获取错误信息
    $user->getError(); // return Structure\Error

    $user->getError()->getMessage();  // 错误信息 string
    $user->getError()->getCode();     // 错误码 string
    $user->getError()->getField();    // 字段名 string
    $user->getError()->getPosition(); // 错误定位 对应Handler对应的options字段

    $user->getErrors(); // return Structure\Error[]
  • 输出

    • 全量输出会进行转换和default赋值
    • 全量输出不进行过滤
    $user->output(); // return array

    $user->output(true); // 全量输出
  • 清洗
    $user->clean(); // 默认不装载raw数据

    $user->clean(true); // 装载raw数据

补充

  • Handler 接受自定义注册
    \Structure\Handler::register();
  • StructureException
    try {
        // ...                          
    }catch (\Structure\Exceptions\StructureException $exception){
        $exception->getPosition(); // 错误定位信息
        $exception->getMessage(); // Structure Exception [{position info}]
        $exception->getCode(); // 始终以-666返回
    }
1907 5 1
5个评论

Tinywan

感谢分享!

admin

不错

torrise

感谢分享,枚举类的规则如何处理

  • chaz6chez 2022-11-11

    之前在写这个组件的时候没有考虑枚举类,其实可以自行实现handler,然后注册进入使用

powerbowen

感谢分享,这个东西看上去不错
以下是提出的一些关于使用场景的问题
1.入参是多维数组的情况是是否有相应的验证规则,如下

$a = [
    ['a' => [1,2,3,'child' => [666]]]
];

2.在很多业务场景中前端会传过来很多乱七八糟的数据,如果需要参数仅需要某些字段的时候是否有相关的校验方式

//现在的
$liat = [
    'name' => 'xxx',
    'mobile' => 'xxx',
    'pwd' => 'xxx'
];
$model->name = $list['name'];
$model->mobile = $list['mobile'];

//增加验证后就可以验证存在非name和mobile字段,直接提示参数异常
$model->attributes = $list;

望指导

  • chaz6chez 2022-11-11

    你是说不能确定哪些参数需要验证,整个字段的验证是随机的?
    如果是固定字段,但有可能客户端传来的参数并不一定传入该参数的话,其实可以用skip标签

  • powerbowen 2022-11-11

    不好意思,上面的没有表述清楚,我现在有修改个人资料接口,是以数组形式传过来的键值对参数列表,比如姓名、手机号,性别等,在赋值的时候我要判断有什么赋值什么
    理想情况是直接把给过来的东西进行数据验证,然后塞到属性里即可,但是对方可能把其它字段拿过来进行试探,比如修改企业id,修改角色,修改密码
    我这个修改资料接口仅支持修改姓名、性别、手机号、邮箱、身份证,如接收到的请求数据存在这些以外的数据就进行报错
    不知道我表述的清不清晰

  • chaz6chez 2022-11-11

    那就嵌套一下呗,就是这个数组再创建一个struct,这个struct对象是外层struct的某个属性,然后外层用func的方式获取内层struct的message

Tinywan

  • 暂无评论
年代过于久远,无法发表评论

chaz6chez

4314
积分
0
获赞数
0
粉丝数
2018-11-16 加入
🔝