众所周知,TP(thinkphp简称)中存在验证器,但需要你自己调用,因而有没有办法可以不用每个action中都写上验证器的代码呢??

这里需要引入中间件(middleware)的概念:

中间件主要用于拦截或过滤应用的HTTP请求,并进行必要的业务处理。1

在TP中中间件是处理中间流程,或者一些通用流程(比如:接口需要写入访问日志)时,需要调用或者执行的代码块。

关于如何自动验证,我们应该有如下思路:

  1. 有一个中间件来调用验证器,在这个中间件中必须能知道访问路径。

  2. 根据访问路径来调用对应的验证器类。

  3. 根据访问路径对应的action来判断验证器是否存在场景(scene)

  4. 是否验证成功

  5. 验证失败抛出异常

创建Validate中间件

因此我们可以创建一个Validate的中间件,用于过滤一部分错误的请求。(毕竟连参数都不正确,我的接口怎么会给你调用呢?)

//在终端中输入
# php think make:middleware Validate

// 此时你可以得到一个名叫Validate的middleware。
# Validate.php文件
<?php

namespace app\middleware;

class Validate
{
  public function handel($request, $next){
    
  }
}

获取请求路径并分析

如果是TP5,我们大可以使用如下代码来获取请求路径:

$m = $request->module(true);      // 获取请求模块名
$c = $request->controller(true);  // 获取请求控制器名
$a = $request->action(true);      // 获取请求方法名

但是TP6取消了module方法,且在应用初始化前无法获得contoller和action的名称。

class Validate extends Middleware{
  public function handel($request, $next){
    //前置事件
    // todo code
    echo $request->controller();   #输出''
    $resp = $next($request);      //这一步会执行action,所以验证不能在之后执行。
    //后置事件
    // todo code
    echo $request->controller();   #输出'index'
    return $resp;
  }
}

因此无法调用TP的方法,这时候该怎么办呢??

经过查阅Tp文档,发现Request2中有:

pathinfo

当前请求URL的pathinfo信息(含URL后缀)

这个方法,但这个方法中含有url后缀,(既.html或者.htm,等等),因此我们可以看到这个方法:

ext

当前URL的访问后缀

因此我们可以使用这行代码来获取请求路径:

$path = $request->ext()
        ? substr($request->pathinfo(), 0, -(strlen($request->ext()) + 1))
        : $request->pathinfo();

这样得到了请求路径,比如:you.domain.com/index/test/aaa的访问中我们就得到了index/test/aaa这个字符串,可以使用explode函数来根据'/'切割字符串。

$path = $request->ext()
        ? substr($request->pathinfo(), 0, -(strlen($request->ext()) + 1))
        : $request->pathinfo();
$namePath = explode( '/', $path);

正常情况下$namePath是一个长度为3的字符串数组,但当你的请求路径如you.domain.com/index/test时,我们得到的路径就是index/test,而如果请求路径仅仅只有域名时,我们得到的路径可能是一个空字符串。所以我们需要做判断,默认有3层,请看下面代码。

    /** 路径解析
     *  @param string $className 基础类名  比如: app\\Validate\\
     *  @param array $namePath   经过上面分割得字符串数组
     *  @param string $action    需要调用得action名字
     *  @return string 验证器类名
     */
    private function pathParse(string $className,array $namePath,string &$action): string{
        $model = (isset($namePath[0]) && !$namePath[0]) ? $namePath[0] : 'index';
        $className .= $model . '\\';
        $controller = (isset($namePath[1]) && !$namePath[1]) ? $namePath[1] : 'index';
        $className .= Str::studly($controller);
        $action = (isset($namePath[2]) && !$namePath[2]) ? $namePath[2] : 'index';
        return $className;
    }

如此我们就获取到对应模块的验证器类了。

调用验证器并验证参数

经过上述思路,我们成功拿到了验证器类名了。

不过万一这个接口不需要验证,不久没有验证器了么??

那我们需要判断这个验证器是否存在。

存在时则开始执行验证,否则可以跳过验证器步骤。

    $className = pathParse($className, $namePath, $action);
    if (class_exists($className)) {
      /**
       * @var $valid \think\Validate
       */
      $valid = new $className();
      if ($valid->hasScene($action) && !$valid->scene($action)->check(input())) {
          $error = $valid->getError();
          return response($error,200, [], 'json');
      }
    }

接下来就贴上完成代码:

<?php

namespace app\middleware;

use app\Request;
use think\helper\Str;
use think\Middleware;

class Validate extends Middleware
{
    public function handle(Request $request, \Closure $next)
    {
        $path = $request->ext()
            ? substr($request->pathinfo(), 0, -(strlen($request->ext()) + 1))
            : $request->pathinfo();
        $className = $this->getValidateClassName($path, $action);
        if (class_exists($className)) {
            /**
             * @var $valid \think\Validate
             */
            $valid = new $className();
            if ($valid->hasScene($action) && !$valid->scene($action)->check(input())) {
                $error = $valid->getError();
                return response($error,200, [], 'json');
            }
        }
        return $next($request);
    }

    public function getValidateClassName($path, &$action): string
    {
        $namePath = explode( '/', $path);
        $className = 'app\\validate\\';
        if (count($namePath) > 3) {
            $className = $this->deepPathParse($className, $namePath, $action);
        }else{
            $className = $this->shallowPathParse($className, $namePath, $action);
        }
        return $className . 'Validate';
    }

    /**
     * 深度路径解析
     * @param string $className 初始类名
     * @param array $namePath   类名路径
     * @param string $action    最终执行方法
     * @return string
     */
    private function deepPathParse(string $className, array $namePath, string &$action): string {
        $controller = $namePath[count($namePath) - 2];
        $action = $namePath[count($namePath) - 1];
        foreach ($namePath as $name) {
            if ($name == $action || $name == $controller) {
                continue;
            }
            $className .= $name . '\\';
        }
        $className .= Str::studly($controller);
        return $className;
    }

    /**
     * 浅路径解析
     * @param string $className
     * @param array $namePath
     * @param string $action
     * @return string
     */
    private function shallowPathParse(string $className,array $namePath,string &$action): string{
        $model = (isset($namePath[0]) && !$namePath[0]) ? $namePath[0] : 'index';
        $className .= $model . '\\';
        $controller = (isset($namePath[1]) && !$namePath[1]) ? $namePath[1] : 'index';
        $className .= Str::studly($controller);
        $action = (isset($namePath[2]) && !$namePath[2]) ? $namePath[2] : 'index';
        return $className;
    }
}

感谢:

  1. 中间件 · ThinkPHP6.0完全开发手册 · 看云

  2. 请求信息 · ThinkPHP6.0完全开发手册 · 看云