Swoft2 框架精华教程:Validator 校验器详解

[toc]

校验器

校验器是 swoft2 中一个常用的组件。

校验器在 RPC服务、WS服务、HTTP 服务中均有涉及,用来校验客户端上传到服务端的数据是否合法。

校验器类型

校验器一般分为两种,系统校验器,和自定义校验器。

系统校验器是通过 @Validator 标签进行注解的一个校验器类(没有方法只有属性),每个属性上通过不同的注解(如:@Length @IsString )来说明校验时候参数要符合的规则。

自定义校验器,同样需要 @Validator 标签对校验的类进行注解,但是类必须要实现 ValidatorInterface 接口,只有实现接口validate(array $data, array $params),才会称为自定义校验器(源码中有此判断,参见:src/Annotation/Parser/ValidatorParser.php)。注意自定义校验器,只会校验 body 中数据,且自定义校验器,只会使用 body 中数据和 params,其他 @Validate 属性,并不会使用。

php 复制
/**
 * Class CustomerValidator
 *
 * @since 2.0
 *
 * @Validator(name="userValidator")
 */
class CustomerValidator implements ValidatorInterface
{
    /**
     * @param array $data		这是自定义校验器中
     * @param array $params
     *
     * @return array
     * @throws ValidatorException
     */
    public function validate(array $data, array $params): array
    {
        $start = $data['start'] ?? null;
        $end   = $data['end'] ?? null;
        if ($start === null && $end === null) {
            throw new ValidatorException('Start time and end time cannot be empty');
        }

        if ($start > $end) {
            throw new ValidatorException('Start cannot be greater than the end time');
        }

        return $data;
    }
}

@Validate 注解

属性说明

php 复制
/**
 * Class
 *
 * @since 2.0
 *
 * @Annotation
 * @Target("METHOD")
 * @Attributes({
 *     @Attribute("validator", type="string"),
 *     @Attribute("fields", type="array"),
 *     @Attribute("params", type="array"),
 *     @Attribute("message", type="string"),
 * })
 */
class Validate {}

以上为 @Validate 注解的参数要求,具体说明见下表:

注解参数 参数类型 是否必须 备注
validator 字符串 已经定义好的校验器的名字
fields 数组 校验器中的属性(对应请求中的参数)。
不指定,默认校验校验器中所有属性。
如果指定的值在校验器中不存在,那么控制器中仍然可以接收到参数,但是不会有任何校验!!!
unfields 数组 不进行校验的属性
params 数组 用在自定义校验器中,用户手动传递到校验器的数据
注:自定义校验器时候才会使用
message 字符串 校验失败时候提示信息
type 字符串 校验数据所在请求对象中的位置(get/body/path)

@Validate 注解用于 method 上,示例:

php 复制
// 示例1:通过系统默认校验器,校验 post 请求中的参数
/**
 * @RequestMapping("account")
 * @param Request $request
 * @param Response $response
 * @return Response
 * @throws InvalidArgumentException
 * @Validate(validator="userValidator", fields={"name", "password"}, type="body")
 */
public function account(Request $request, Response $response): Response;

// 示例2:通过系统默认的校验器校验自定义 path 参数

/**
 * @RequestMapping(route="/[list-{page}.html|index.html]", params={"page"="[2-9]\d*|1\d+"}, method={"GET"})
 * @View("home/index")
 * @Validate(validator=PageListDto::class, fields={"page", "size"}, type="path")
 *
 * @param Request $request
 * @param Response $response
 * @return Response
 */
public function index(Request $request, Response $response): Response

注意:@Validate 的 type 参数十分重要,默认值为 body,也就是默认校验 post 请求参数。如果是校验 get 参数,或者自定义 path 中的自定义参数,必须要写明类型。否则校验出错。

校验器校验主要流程

以 HTTP 服务为例,可以参照 http-server 组件中的 src/Middleware/ValidatorMiddleware.php 中间件,此中间件可以通过定义核心 Bean 的方式,将其挂载到 HTTP 服务上。从相关代码可以看出,中间件运行期间,通过 ValidateRegister::getValidates()方法提取访问接口上绑定的校验器相关信息。

php 复制
class ValidatorMiddleware implements MiddlewareInterface
{
    /**
     * @param ServerRequestInterface  $request
     * @param RequestHandlerInterface $handler
     *
     * @return ResponseInterface
     * @throws ValidatorException
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // 获取路由匹配结果,如果未匹配成功,此中间件不做处理,交给下一个中间件。
        /* @var Route $route */
        [$status, , $route] = $request->getAttribute(Request::ROUTER_ATTRIBUTE);
        if ($status !== Router::FOUND) {
            return $handler->handle($request);
        }
		// 如果路由匹配成功,获取路由绑定的处理器(controller/action)
        // Controller and method
        $handlerId = $route->getHandler();
        [$className, $method] = explode('@', $handlerId);

        // 获取 controller/action 方法上通过注解绑定的校验器(可以多个)
        // Query validates
        $validates = ValidateRegister::getValidates($className, $method);
        // 如果没有,说明不用校验,交给下一个中间件处理
        if (empty($validates)) {
            return $handler->handle($request);
        }
		// 获取校验涉及的相关数据
        $data  = $request->getParsedBody();
        $query = $request->getQueryParams();
        $path  = $route->getParams();

        // ParsedBody is empty string
        $parsedBody    = $data = empty($data) ? [] : $data;
        $notParsedBody = !is_array($data);
        if ($notParsedBody) {
            $parsedBody = [];
        }

        // 获取校验器组件的实例对象
        /* @var Validator $validator */
        $validator = BeanFactory::getBean('validator');

        // 校验此次请求对象中的数据
        /* @var Request $request */
        [$parsedBody, $query, $path] = $validator->validateRequest($parsedBody, $validates, $query, $path);
        if ($notParsedBody) {
            $parsedBody = $data;
        }
		// 将校验后的数据重新放入此次请求,并且交给后续的中间件进行处理
        $request = $request->withParsedBody($parsedBody)->withParsedQuery($query)->withParsedPath($path);

        return $handler->handle($request);
    }
}
php 复制
/**
 * @param array $body
 * @param array $validates
 * @param array $query
 * @param array $path
 *
 * @return array
 * @throws ValidatorException
 */
public function validateRequest(array $body, array $validates, array $query = [], array $path = []): array
{
    // 此处为从指定请求方法上获取的绑定的校验器
    foreach ($validates as $name => $validate) {
        // 通过校验器名称,获取其实例对象
        $validator = ValidatorRegister::getValidator($name);

        // 如果方法上绑定的校验器的名字,但是并没有实际的实例对象,会直接报异常
        if (empty($validator)) {
            throw new ValidatorException(sprintf('Validator(%s) is not exist!', $name));
        }
		// 依次获取校验器的相关数据
        $type     = $validator['type'];
        $fields   = $validate['fields'] ?? [];
        $unfields = $validate['unfields'] ?? [];
        $params   = $validate['params'] ?? [];
		
        // 校验器类型 (get/path)类型都是系统校验器,body数据可以用系统校验器也可以是自定义校验器
        $validateType = $validate['type'];

        // Get query params
        if ($validateType === ValidateType::GET) {
            $query = $this->doValidate($query, $type, $name, $params, $validator, $fields, $unfields);
            continue;
        }

        // Route path params
        if ($validateType === ValidateType::PATH) {
            $path = $this->doValidate($path, $type, $name, $params, $validator, $fields, $unfields);
            continue;
        }

        $body = $this->doValidate($body, $type, $name, $params, $validator, $fields, $unfields);
    }

    return [$body, $query, $path];
}

系统校验器每个属性存储结构

系统校验器,每个属性的结构存储示例如下:

php 复制
/**
 * Class UserValidator
 * @since 2.0
 *
 * @Validator(name="userValidator")
 */
class UserValidator
{
    /**
     * @IsString()
     * @Length(min=6, max=32, message="name length must between 6 and 32")
     *
     *
     * @var string
     */
    protected $name = '';

    /**
     * @IsString()
     * @Length(min=8, max=32, message="password length must between 8 and 32")
     *
     * @var string
     */
    protected $password;
}
php 复制
// name 属性的 property 结构
array (
  'required' => false,
  'type' => 
  array (
    'default' => '',
    'annotation' => 
    Swoft\Validator\Annotation\Mapping\IsString::__set_state(array(
       'message' => '',
       'name' => '',
    )),
  ),
  'annotations' => 
  array (
    0 => 
    Swoft\Validator\Annotation\Mapping\Length::__set_state(array(
       'min' => 6,
       'max' => 32,
       'message' => 'name length must between 6 and 32',
    )),
  ),
)
// password 的 property 结构
array (
  'required' => false, // @Required 注解加上后会变成 true
  'type' => 
  array (
    'default' => NULL,
    'annotation' => 
    Swoft\Validator\Annotation\Mapping\IsString::__set_state(array(
       'message' => '',
       'name' => '',
    )),
  ),
  'annotations' => // 除了类型注解、@Required 注解之外的其他验证规则注解
  array (
    0 => 
    Swoft\Validator\Annotation\Mapping\Length::__set_state(array(
       'min' => 8,
       'max' => 32,
       'message' => 'password length must between 8 and 32',
    )),
  ),
)

由以上可以看出系统校验器中,每个属性的基本结构为:

  • required 是否必须属性(可选

  • type:类型注解(必须,否则服务启动会报错)

    类型注解(如:IsString|IsInt|IsBool|IsFloat)继承\Swoft\Validator\Annotation\Mapping\Type的注解为类型注解,可以用来标记系统校验器的属性的类型。同时需要有一个相应的规则

    • default 默认值(通过系统校验器中的属性默认值获得)
    • annotation 类型关联的注解
  • annotations 其他注解(可选),用来校验属性的其他规则

每个校验器属性的校验代码:

注意: 校验器属性加上 @Required 注解,表示这个属性一定要校验。

如果以下条件都成立,则会跳过此属性不校验(参数为可选):

  1. 被校验的数据中不存在(名字为校验器属性名)参数
  2. 校验器属性没有标识为 @Required
  3. 校验器属性没有默认值
php 复制
protected function validateDefaultValidator(array $data, array $validator, array $fields, array $unfields): array
{
    $properties = $validator['properties'] ?? [];
	// 系统校验器中的每个属性依次进行校验
    foreach ($properties as $propName => $property) {
        /* @var IsString|IsInt|IsBool|IsFloat $type */
        $type = $property['type']['annotation'] ?? null;
        // 如果属性没有类型校验器直接跳过
        if ($type === null) {
            continue;
        }
		// 如果属性没有在指定的校验域中,跳过
        if ($fields && !in_array($propName, $fields, true)) {
            continue;
        }
		// 如果在排除域中,直接跳过
        // Un-fields - exclude validate
        if (in_array($propName, $unfields, true)) {
            continue;
        }
		// 属性名字,如果在类型上有给定名称,则为类型注解给的名称
        $propName = $type->getName() ?: $propName;
        // 如果数据中没有此属性,属性不是必选,属性没有设置默认值,直接跳过此属性校验
        if (!isset($data[$propName]) && !$property['required'] && !isset($property['type']['default'])) {
            continue;
        }
		// 默认值给了则使用默认值,否则为 null
        $defaultVal  = $property['type']['default'] ?? null;
        // 其他校验注解
        $annotations = $property['annotations'] ?? [];

        // 校验当前属性细节
        // Default validate item(Type) and other item
        $data = $this->validateDefaultItem($data, $propName, $type, $defaultVal);
        foreach ($annotations as $annotation) {
            if ($annotation instanceof Required) {
                continue;
            }
            $data = $this->validateDefaultItem($data, $propName, $annotation);
        }
    }

    return $data;
}
/**
 * 校验每个校验器注解的规则
 * @param array  $data
 * @param string $propName
 * @param object $item
 * @param mixed  $default
 *
 * @return array
 */
protected function validateDefaultItem(array $data, string $propName, $item, $default = null): array
{
    $itemClass = get_class($item);

    //support i18n
    $msg    = $item->getMessage();
    $msgLen = strlen($msg) - 1;
    if (strpos($msg, '{') === 0 && strrpos($msg, '}') === $msgLen) {
        $item->setMessage(Swoft::t(substr($msg, 1, -1)));
    }
    // 此处为关键代码:通过注解名字获取对应的注解规则的 bean 实例
    /* @var RuleInterface $rule */
    $rule = BeanFactory::getBean($itemClass);
    // 校验属性的规则是否匹配,校验失败抛出 ValidatorException 异常
    $data = $rule->validate($data, $propName, $item, $default, $this->strict);
    // 校验通过,返回被校验通过的数据
    return $data;
}

校验器规则定义,注解、注解解析器定义

校验器规则是真正校验数据的逻辑代码,其必须实现 RuleInterface 接口。由于 swoft2 的校验是通过注解绑定指定方法进行校验的。所以还需要对校验规则定义相应的注解和注解解析器(mapping 和 parser),这样绑定方法后,校验器才能正常运行。

校验器注解使用

每个校验器对应一个校验器规则,校验器规则定义需要实现 RuleInterface,且校验器规则,需要用 @Bean 注解进行注解,bean 名称需要和校验器注解类名保持一致。如下示例:

php 复制
/**
 * Class IsStringRule
 *
 * @since 2.0
 *
 * @Bean(IsString::class)
 */
class IsStringRule implements RuleInterface {
    
}

实现一个自定义的校验器

校验器注解

注解的实体对象,需要定义一个类,标注为 @Annotation 表示是一个注解对象,@Target 注解,标识此注解可以放的目标。

php 复制
// @Target 注解取值如下
private static $map = [
    'ALL'        => self::TARGET_ALL,
    'CLASS'      => self::TARGET_CLASS,
    'METHOD'     => self::TARGET_METHOD,
    'PROPERTY'   => self::TARGET_PROPERTY,
    'FUNCTION'   => self::TARGET_FUNCTION,
    'ANNOTATION' => self::TARGET_ANNOTATION,
];

一个示例:

php 复制
/**
 * Class Username
 *
 * @since 2.0
 *
 * @Annotation
 * @Target("PROPERTY")
 * @Attributes({
 *     @Attribute("min",type="int"),
 *     @Attribute("max",type="int"),
 *     @Attribute("message",type="string"),
 * })
 */
class Username
{
    /**
     * @var int
     */
    private int $min = 0;

    /**
     * @var int
     */
    private int $max = 0;

    /**
     * @var string
     */
    private string $message = '';

    /**
     * Username constructor.
     *
     * @param array $values
     */
    public function __construct(array $values)
    {
        if (isset($values['value'])) {
            $this->message = $values['value'];
        }
        if (isset($values['min'])) {
            $this->min = $values['min'];
        }
        if (isset($values['max'])) {
            $this->max = $values['max'];
        }
        if (isset($values['message'])) {
            $this->message = $values['message'];
        }
    }

    /**
     * @return int
     */
    public function getMin(): int
    {
        return $this->min;
    }

    /**
     * @return int
     */
    public function getMax(): int
    {
        return $this->max;
    }

    /**
     * @return string
     */
    public function getMessage(): string
    {
        return $this->message;
    }
}

校验器注解解析器

解析器主要是用来,在框架启动时后,注解数据进行解析期间,需要对注解做何处理的操作。需要一个类注解 @AnnotationParser 注解,标注相应的注解对象的类。

一个用作校验器的注解解析器示例如下:

php 复制
/**
 * Class UsernameParser
 * @since 2.0
 *
 * @AnnotationParser(Username::class)
 */
class UsernameParser extends Parser
{

    /**
     * @inheritDoc
     * @throws ReflectionException
     * @throws AnnotationException
     */
    public function parse(int $type, $annotationObject): array
    {
        if ($type != self::TYPE_PROPERTY) {
            throw new AnnotationException('`@Username` must be defined by property!');
        }
        ValidatorRegister::registerValidatorItem($this->className, $this->propertyName, $annotationObject);
        return [];
    }
}

校验器规则

规则是一个校验器在运行时真正要运行的代码(此代码在业务运行时期间)。需要实现规则接口 RuleInterface

php 复制
/**
 * Class UsernameRule
 * @since 2.0
 *
 * @Bean(Username::class)
 */
class UsernameRule implements RuleInterface
{

    /**
     * @inheritDoc
     */
    public function validate(array $data, string $propertyName, $item, $default = null, $strict = false): array
    {
        /* @var Username $item */
        $min = $item->getMin();
        $max = $item->getMax();



        // 如果数据中没有校验的目标参数,且未提供默认值,则直接返回
        if (!isset($data[$propertyName]) && $default === null) {
            return $data;
        }
        // 如果数据中没有校验的目标参数,且提供了默认值,则使用默认值
        if (!isset($data[$propertyName]) && $default !== null) {
            $data[$propertyName] = $default;
        }
        // 校验用户名长度和格式
        $value = $data[$propertyName];
        if (ValidatorHelper::validatelength($value, $min, $max) && $this->validateUsername($value)) {
            return $data;
        }
        $message = $item->getMessage();
        $message = !empty($message) ? $message :
            sprintf('%s length (min=%d, max=%d), and consist of (0-9a-zA-Z-_|).', $propertyName, $min, $max);
        throw new ValidatorException($message);
    }

    /**
     * 校验用户名格式
     * @param string $username
     * @return bool
     */
    private function validateUsername(string $username): bool
    {
        if (preg_match('/^[a-zA-Z0-9_\-|]+$/', $username)) {
            return true;
        }
        return false;
    }
}

系统校验器

注意:自定义校验器通过 @Validate 注解进行校验时,必须配置 params 参数。自定义校验器通过 params 参数指定需要校验的字段类型(相当于系统默认校验器的 fields 字段)

Controller/Action 绑定校验器

以下为一个自定义系统校验器 UserDto

php 复制
/**
 * Class UserDto
 *
 * @since 2.0
 *
 * @Validator()
 */
class UserDto
{
	/**
     * @Required()
     * @IsString()
     * @Username(min=4, max=10)
     *
     * @var string|null
     */
    protected ?string $name = null;
}

控制器示例:

php 复制
/**
 * Class UserController
 *
 * @since 2.0
 *
 * @Controller("user")
 */
class UserController 
{
	/**
     * @Inject()
     * @var UserManageLogic
     */
    private UserManageLogic $userManageLogic;
    
	/**
     * 添加用户
     *
     * @RequestMapping(route="add", method={"POST"})
     * @Validate(validator=UserDto::class, fields={"name"})
     * @param Request $request
     * @param Response $response
     * @return void
     * @throws ApiException
     * @throws Exception
     */
    public function add(Request $request, Response $response): Response
    {
        return $this->userManageLogic->create($request, $response);
    }
}