入参设计和Laravel表单验证

入参设计

在设计函数时,入参是一个重要组成部分,并且随着需求不断扩展,入参也需要进行改写。

示例:

<?php
	/**
	* 设置会员基本信息
	* @param string $name 会员姓名
	* @param string $phoneNum 电话号码
	**/
	function setMember(string $name , string $phoneNum):void
	{
		$member = new Member();
		$member->name = $name;
		$member->phoneNum = $phoneNum;
		//后续省略...
	}
	//调用方法
	setMember('Andy','80654321');

上述示例的设置会员基本信息方法,存在两个入参,$name$phoneNum。在此处仅仅设置了姓名和电话号码。
假设现在会员新增了一个“avatar”属性,对该方法进行改造:

<?php
	/**
	* 设置会员基本信息
	* @param string $name 会员姓名
	* @param string $phoneNum 电话号码
	* @param string $avatar 头像
	**/
	function setMember(string $name , string $phoneNum , string $avatar):void
	{
		$member = new Member();
		$member->name = $name;
		$member->phoneNum = $phoneNum;
		//新增头像属性
		$member->avatar = $avatar;
		//后续省略...
	}
	//调用方法
	setMember('Andy','80654321','images/avatar/S3a2100.jpg');

在函数入参不多、并且逻辑较为简单的情况下,这样改造看起来没有问题,调用时多传入一个头像变量$avatar即可。

在其他一些语言中,支持指定参数名称,如python:

def setMember(name: str, phoneNum: str, avatar: str) -> None:
    member = Member()
    member.name = name
    member.phoneNum = phoneNum
    member.avatar = avatar

#普通调用,此时和php一样,按照函数定义的顺序入参:
setMember(
    'Andy', 
    '80654321', 
    'images/avatar/S3a2100.jpg'
)
#指定参数名称入参,顺序不影响,php不支持此种方式:
setMember(
    phoneNum ='80654321', 
    avatar = 'images/avatar/S3a2100.jpg',
    name = 'Andy'
)

非常遗憾,php并不支持指定参数名称的方式来入参,只能严格按照函数定义的顺序进行入参。在实际项目中,函数的入参数量非常多时,这种严格的顺序要求将给调用者造成障碍。
(此处勘误:php8.0开始已经支持命名参数。20241224)
(参见https://www.php.net/manual/zh/functions.arguments.php#functions.named-arguments)
为了解决这种情况,php一般使用数组来代替众多无法预测的入参,将极大提高函数可扩展性。

对上述函数进行改造:

<?php
	/**
	* 设置会员基本信息
	* @param array $attrs 会员属性
	* - string name 会员姓名
	* - string phoneNum 电话号码
	* - string avatar 头像
	**/
	function setMember(array $attrs):void
	{
		$member = new Member();
		$member->name = $attrs['name'];
		$member->phoneNum = $attrs['phoneNum'];
		$member->avatar = $attrs['avatar'];
		//后续省略...
	}
	//调用方法,数组键值对的顺序不影响
	setMember([
	    'phoneNum' => '80654321',
	    'avatar' => 'images/avatar/S3a2100.jpg',
	    'name' => 'Andy',
	]);

api接口在接受表单提交时,大多数情况下入参较多,一般使用这种方式来增强兼容性和扩展性。

表单验证

前面提到,入参较多时,使用数组代替入参,实现像python等语言支持的指定参数入参。这时会产生另一个问题,强制类型提示只能判断入参是一个数组,而无法判断是否存在需要的键值对。

还是前面的例子:

<?php
	/**
	* 设置会员基本信息
	* @param array $attrs 会员属性
	* - string name 会员姓名
	* - string phoneNum 电话号码
	* - string avatar 头像
	**/
	function setMember(array $attrs):void
	{
		$member = new Member();
		$member->name = $attrs['name'];
		$member->phoneNum = $attrs['phoneNum'];
		$member->avatar = $attrs['avatar'];
		//后续省略...
	}
	//错误调用,缺失必要的键值对,产生Undefined Index异常
	setMember([
	    'phoneNum' => '80654321',
	]);

上述示例中,当运行到$member->name = $attrs['name'];这一行时,由于入参的$attrs数组内不存在$attrs['name']这个键值对,将产生一个Undefined Index异常。
在产生异常之前,函数已经运行了一部分,这使得函数变的非原子性,将导致不可预料的后果。为了解决这个问题,需要对入参的键值对进行预校验。

对函数进行改造:

<?php
	/**
	* 设置会员基本信息
	* @param array $attrs 会员属性
	* - string name 会员姓名
	* - string phoneNum 电话号码
	* - string avatar 头像
	**/
	function setMember(array $attrs):void
	{
		//函数实际功能运行之前,对入参数组进行预校验
		attrsValidate($attrs);
		//原函数
		$member = new Member();
		$member->name = $attrs['name'];
		$member->phoneNum = $attrs['phoneNum'];
		$member->avatar = $attrs['avatar'];
		//后续省略...
	}

	/**
	* 预校验函数
	**/
	function attrsValidate(array $attrs):void
	{
		if(
			!isset($attrs['name']) || 
			!isset($attrs['phoneNum']) || 
			!isset($attrs['avatar'])
		)
		{
        	throw new Exception('Missing required attributes');
    	}
	}

	//错误调用,缺失必要的键值对,抛出预料之内的Missing required attributes异常
	setMember([
	    'phoneNum' => '80654321',
	]);

改造后的函数,在面对错误的调用方法时,将会抛出一个在预料之内的异常,并且及时中止,保证了函数的原子性。此次改造极大地提高了代码健壮。

Laravel表单验证

实际业务中,并不仅仅只是简单校验某个键值对是否存在,参数的要求非常多,类型、长度、数值大小、字符串格式等等都有可能。因此需要设计更为复杂的预校验函数来满足不同的校验需求。Laravel框架提供的表单验证功能,可以适用大部分情况。

框架Request类自带了validate方法,对请求入参进行校验。
示例:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Member;

class MemberController extends Controller
{
	/**
	* 设置会员基本信息
	* @param array $attrs 会员属性
	* - string name 会员姓名
	* - string phoneNum 电话号码
	* - string avatar 头像
	**/
	public function setMember(Request $request):void
	{
		//入参校验,校验失败将重定向到提交前的页面
		$request->validate([
			'name' => 'required|string',
			'phoneNum' => 'required|digits:8',
			'avatar' => 'required|string'
		]);
		$attrs = $request->input();
		//原函数
		$member = new Member();
		$member->name = $attrs['name'];
		$member->phoneNum = $attrs['phoneNum'];
		$member->avatar = $attrs['avatar'];
		//后续省略...
	}
}

这种方式验证失败时,将会重定向到之前的页面,相当于在浏览器点击返回上一页,因此仅限于对请求入参进行校验。如果是纯api的项目,不使用laravel提供的视图组件的情况下,其自由度较低,不太适合使用。并且需要校验入参的并不仅限于请求控制器,其他类或方法同样会用到。示例中这种简易的方法实际项目中基本不会使用。

推荐使用Facades门面中的Validator类,可以自由控制验证规则、校验消息、参数提示。
使用方法如下:

use Illuminate\Support\Facades\Validator;
//Validator类中的make方法
public static function make(
    array $data,
    array $rules,
    array $messages = [],
    array $customAttributes= []
) :\Illuminate\Contracts\Validation\Validator{ }

Validator类的make方法接受四个入参:

$data需要校验的关联数组;
$rule校验规则;
$messages校验失败时返回的描述消息;
$customAttributes指定参数别名;

$rule校验规则写法:

[
	'参数名' => '规则1|规则2|规则3|...'
	
	//示例:phoneNum不能为空,并且是一个8位数字
	'phoneNum' => 'bail|required|digits:8',
]

常用校验规则:

规则示例
bail:校验失败立即停止,不再校验后面的规则
类型要求string:要求是一个字符串; integer:要求是一个整数
存在要求required:要求不为空; nullable:允许为空
数值要求min:最小值; max:最大值; between:取值区间
更多规则请查询Laravel文档

$messages描述消息写法:

[
	'规则' => '描述消息',
	
	//示例,:attribute将被自动替换成参数(别)名
	'required' => ':attribute不能为空'
]

$customAttributes指定参数别名写法:

[
	'参数名' => '别名',
	
	//示例
	'phoneNum' => '电话号码'
]

此方法将返回一个Illuminate\Contracts\Validation\Validator实例,该实例包含了校验内容及其结果。

Validator实例方法:

方法描述
after()校验完成之后的动作,接受传入一个闭包
fails()返回校验是否失败
errors()返回校验失败的描述消息

此外还有其他一些方法,因为使用频率较低不再一一展出。

构造校验器时,一般将$messages$customAttributes传入,不传将使用框架默认规则,和使用参数原名

完整示例:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Member;
use Illuminate\Support\Facades\Validator;

class MemberController extends Controller
{
	/**
	* 设置会员基本信息
	* @param array $attrs 会员属性
	* - string name 会员姓名
	* - string phoneNum 电话号码
	* - string avatar 头像
	**/
	public function setMember(Request $request):void
	{
		//入参校验
		$attrs = $request->input();
		$this->paramsValidate($attrs);
		//原函数
		$member = new Member();
		$member->name = $attrs['name'];
		$member->phoneNum = $attrs['phoneNum'];
		$member->avatar = $attrs['avatar'];
		//后续省略...
	}
	
	/**
	* 参数校验方法
	* 
	**/
	private function paramsValidate($params)
	{
		$validator = Validator::make($params, self::$rule, self::$messages , self::$attributes);
		
		//校验失败抛出异常
        if ($validator->fails()) {
        	//异常消息为校验失败的第一个描述信息
            throw new \Exception($validator->errors()->first());
        }
	}

	/**
	* @var array 自定义校验规则
	**/
	private static array $rule = [
		'name' => 'bail|required|string',
		'phoneNum' => 'bail|required|digits:8',
		'avatar' => 'bail|required'
 	];
	
	/**
	* @var array 自定义描述消息
	**/
	private static array $messages = [
		'required' => ':attribute不能为空',
		'digits' => ':attribute必须是一个:digits位的数字'
 	];
 	
	/**
	* @var array 自定义参数别名
	**/
	private static array $attributes = [
		'name' => '会员姓名',
		'phoneNum' => '电话号码',
		'avatar' => '头像'
 	];

}

上述示例代码中,paramsValidate方法、$rule$messages属性通用性较强,建议放入通用的工具类作为静态方法和属性提供对外调用,此处不再赘述示例。

总结:

入参验证的本质原因,是对函数方法入参异常进行更加精细地控制,以保证其原子性进而提升代码健壮。Laravel框架提供了一系列表单验证的方法,可以在日常项目中应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值