入参设计
在设计函数时,入参是一个重要组成部分,并且随着需求不断扩展,入参也需要进行改写。
示例:
<?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框架提供了一系列表单验证的方法,可以在日常项目中应用。