分类
php

通过参数校验来体验封装的乐趣

对于后端来说,前端传递的参数都是需要校验的。但是如何将校验的代码写得优雅呢?这并不是每个程序员都能做到的事情,或者说做起来并不是那么容易的,所以框架就需要对类似于校验这样的功能给出解决方案。也想通过对校验的封装,我们来看看好的代码应该是什么样子的。

坏代码的味道

我们来看看不好的代码,也是我们经常在项目中看到的代码是什么样子的。我们就以登录接口为例:

<?php

public function login() 
{
	$params = $_GET;
	if (mb_strlen($params['name']) <= 2) {
		throw new Exception('Username length should great than 2'); 
	}
	if (mb_strlen($params['password']) > 8) {
		throw new Exception('Password length should great than 8');
	} 
}

这端代码看上去还算整齐,但是并不算是好的代码。主要有两个问题,一个是对于校验这样使用频率非常高的逻辑并没有封装成方法,另一个是逻辑和数据耦合。所以,我们很快就能想到,可以封装成方法。如下示例:

<?php

function stringLengthBetween(string $str, int $maxLength)
{
 	// 逻辑省略 
}

函数确实是一种将逻辑和数据分离的好做法,也是复用代码的最常见也是最基础的做法。但是,上面的代码是面向过程的,而我们的框架是基于面向对象思想编写的。所以,下文中我们就使用面向对象的思想来封装一套校验代码。

抽取数据

对于校验这样的功能来说,数据是变化的。这个很好理解,每一个接口的传参都是不一样的。所以,我们要把变化的数据抽取出来,封装成一个类,如下形式:

<?php

class UserLogin extends Validate
{
    protected $rules = [
        'username' => [			// username 是要校验的参数
            // 校验的规则以及所需的参数,username 的长度介于 3 到 64 位之间
            'lengthBetween' => [3, 64],			
        ],
        'password' => [
            'lengthBetween' => [8, 72],
        ]
    ];

    protected $messages = [
      	// 当 username 的 lengthBetween 规格校验错误的时候返回的错误信息
        'username.lengthBetween' => '用户名长度在 3 到 64 个字符之间',		
        'password.lengthBetween' => '密码长度在 8 到 72 个字符之间',
    ];
}

我们看到,上面的 UserLogin 类中,拥有两类数据,一类是 rules ,定义了每一个校验参数对应的规则,以及规则所需的参数。在 login 控制器中,我们希望如下这样的形式去校验参数:

<?php

public function login()
{
 	$params = $_GET;
  (new UserLogin)->each($params);
}

UserLogin 这个类当中并没有 each 方法,这个方法来自于其继承的 Validate 类。

each 方法的作用是遍历每一个需要校验的参数(在 UserLogin 类中定义了规则的参数),然后调用指定的规则去校验,如果校验不通过则抛出异常。这就是通用的逻辑,校验的参数以及规则都是变化的数据。我们已经把变化的数据提取出来封装在了 UserLogin 类中,现在需要封装通用的逻辑了。

封装逻辑

接下来,我们要来完成上文中已经出现的 Validate  这个父类,它的作用是封装校验的逻辑。我们先来封装 each 方法:

<?php
class Validate
{
    public function each(array $params)
    {
			foreach ($params as $key $param) {
        // 跳过在 UserLogin 类中没有定义校验规则的参数
        if (!isset($this->rules[$key])) continue;
        // 调用 validate 方法来校验参数
        [$result, $rule] = $this->validate($key, $params);
        // 校验出错则抛出异常
        if (!$result) throw new Exception('Validate error!');
      }
    }
  
  	public function validate(string $key, $params): array
    {
      	// 遍历每一个要校验参数的多个规则
				foreach ($this->rules[$key] as $rule => $args) {
          		// 规则的名字就是在 Rules 类中定义的静态方法的名字,然后将要校验的参数以及校验规则的参数传入
             $result = Rules::$rule($params, $key, $args);
             if ($result === false) return [false, $rule];		// 校验错误,则返回 false, 以及对应的错误的规则名字
         }
         return [true, null];
    }
}

最后我们给出校验规则的一个示例,也就是上面 Rules 类中对规则的封装:

<?php
class Rules
{
    /**
     * 校验字符串长度
     */
    public static function lengthBetween(array $params, string $key, array $range): bool
    {
        $length = mb_strlen($params[$key]);
        [$min, $max] = $range;

        return ($length >= $min && $length <= $max);
    }

    /**
     * 校验整型
     */
    public static function integer(array $params, string $key): bool
    {
        return filter_var($params[$key], FILTER_VALIDATE_INT);
    }

    /**
     * 校验数值返回
     */
    public static function numberBetween(array $params, string $key, array $range): bool
    {
        return ($params[$key] >= $range[0] && $params[$key] <= $range[1]);
    }

    /**
     * 校验中国手机号
     */
    public static function chinaPhone(array $params, string $key): bool
    {
        return preg_match('/^1[3456789]\d{9}$/', $params[$key]);
    }

    /**
     * 校验 IP 地址
     */
    public static function ip(array $params, string $key): bool
    {
        return filter_var($params[$key], FILTER_VALIDATE_IP);
    }

    /**
     * 校验网址
     */
    public static function url(array $params, string $key): bool
    {
        return filter_var($params[$key], FILTER_VALIDATE_URL);
    }
}

好的代码应该是什么样子的

这个示例我们已经完成了,然后我们来想想看好的代码应该是什么样子的?或者说面向对象的代码应该如何写?

  • 好的代码应该将数据和逻辑分离,比如上面我们将校验的参数以及校验的规则和“校验”这件事情分离开去。
  • 好的代码应该合理的封装,封装是为了复用代码,也是为了简化调用,你看我们控制器中的代码只有一行,调用非常简单。
  • 好的代码应该是易于扩展的,符合面向对象设计原则中的开闭原则的,允许新增代码,不建议修改代码。比如上面的示例,我们需要新增一个规则,只需要在 Rules  类中添加一个方法就可以了,业务的代码、以及 Validate 类中的代码,我们都不需要变动。
  • 好的代码是健壮的,如果每次校验都使用 if...else... 去写的话,非常容易出错,比如校验字符串长度或者日期范围,大于小于大于等于小于等于这种比较运算非常容易弄错造成 BUG。
  • 更多好的代码的规则,但是不好的代码往往比好的代码更常见,更普遍,任重道远。

发表评论

邮箱地址不会被公开。 必填项已用*标注