参数规则:接口参数规则配置
“播下一种思想,收获一种行为;播下一种行为,收获一种习惯;播下一种习惯,收获一种性格;播下一种性格,收获一种命运。” --《成君忆:水煮三国》
1.12.1 参数解析
参数,对于接口来说,是非常重要的输入。对于外部调用来说,同等重要。
因此,对于参数这块,我们是希望能够既减轻后台开发对接口参数获取、判断、验证、文档编写的痛苦;又便于客户端方便的、自由的调用;既利已又利他。
由此,我们引入了 参数解析 这一概念,即:通过配置参数的规则,即可自动实现参数的获取和验证。
1.12.2 参数解析的配置规则
熟悉Yii的同学,对于以下的规则配置应该倍感亲切,但是不熟悉的同学也可以同样快速上手。因为,你会慢慢发现,这样的规则很符合我们PHP开发的规范,如果没有,我们继续努力改进。
格式如下:
array(
'参数名' => array('name' => '接口参数名称', 'type' => '类型', 'default' => '默认值', ...),
... ...
)
1.12.3 示例
(1)简单的示例
假设这样的业务场景,我们需要提供一个用户登录的接口,其中需要用户名和密码,因此:
<?php
class Api_User extends PhalApi_Api
{
public function getRules()
{
return array(
'login' => array(
'username' => array('name' => 'username'),
'password' => array('name' => 'password'),
),
);
}
public function login()
{
return array('username' => $this->username, 'password' => $this->password);
}
}
当我们这样调用接口时:
/?service=User.Login&username=test&password=123456
就可以获取到需要的参数:
{"ret":0,"data":{"username":"test","password":"123456"},"msg":""}
从中,可以很容易理解:参数规则需要统一配置在接口实现类里面的 getRules() 函数,随后即可以通过类成员属性方式获取,如: $this->username 。
(2)更完善的示例
很多时候我们都会对用户名和密码作一些验证,如是否必须、长度、最值,以及默认值等。
继续上面的业务场景,我们登录下用户名和密码必须,且密码长度至少为6个字符,则可以调整参数规则:
'login' => array(
'username' => array('name' => 'username', 'require' => true),
'password' => array('name' => 'password', 'require' => true, 'min' => 6),
),
尝试一下非法的参数请求,如无任何参数的情况下,访问/?service=User.Login,返回:
{"ret":400,"data":[],"msg":"Illegal Param: wrong param: username"}
再尝试一下密码长不对的情况,访问/?service=User.Login&username=test&password=123,返回:
{"ret":400,"data":[],"msg":"Illegal Param: password.len should >= 6, but now password.len = 3"}
1.12.4 三级参数
(1)系统参数
已被系统固定占有的参数,目前只有一个,即:service,为需要调用的服务,类型为字符串,格式为:XXX.XXX,首字母不区分大小写,建议统一以大写开头。
以下是一些示例:
#推荐写法
/?service=User.GetBaseInfo
#正确写法(开头小写)
/?service=user.getBaseInfo
#正确写法(方法名小写,但类名只能开头小写,否则会导致linux系统下文件加载失败)
/?service=user.getbaseinfo
#错误写法(缺少方法名)
/?service=User
#错误写法(缺少点号分割)
/?service=UserGetBaseInfo
#错误写法(默认只支持点号分割)
/?service=User|GetBaseInfo
(2)应用参数
应用参数是指在一个项目中,全部接口都需要的参数,或者通用的参数规则。假如我们的项目中全部需要签名sign参数,且必须;以及非必须的版本号,则可以在./Config/app.php中的apiCommonRules配置:
//$vim ./Config/app.php
<?php
return array(
/**
* 应用接口层的统一参数
*/
'apiCommonRules' => array(
//签名
'sign' => array(
'name' => 'sign', 'require' => true,
),
//客户端App版本号,如:1.0.1
'version' => array(
'name' => 'version', 'default' => '',
),
),
... ...
(3)接口参数
接口参数即为上面在各个接口子类中配置的规则,为特定接口所持有。同时,为了方便同一套接口的规则重用,可以使用下标为 '*' 表示是本接口通用规则,如我们为了加强安全性,为全部的用户接口操作都加上4位的验证码:
public function getRules()
{
return array(
'*' => array(
'code' => array('name' => 'code', 'require' => true, 'min' => 4, 'max' => 4),
),
'login' => array(
'username' => array('name' => 'username', 'require' => true),
'password' => array('name' => 'password', 'require' => true, 'min' => 6),
),
);
}
在完成对上面的应用参数规则、接口通用规则和指定规则的参数进行配置后,对用户登录的接口进行请求时就需要这样访问:
/?service=User.login&sign=77f81c17d512302383e5f26b99dae4d9&username=test&password=123456&code=abcd
温馨提示:在Api类里面配置规则时,下标不区分大小写。因为框架会自动将请求的函数名和全部的规则下标转换成小写进行匹配。
这里,再小结一下,接口参数可以分为两种: 通用接口参数 和 指定接口参数 。前者用 * 号下标表示,后者则用函数名作为下标表示。
(4)多个参数规则的优先级
当同一个参数规则分别在应用参数、接口通用参数及特定接口参数出现时,后面的规则会覆盖前面的,即具体化的规则会替换通用的规则,以保证接口在特定场合的定制性。
简而言之,多个参数规则的优先级从高到下,分别是(正如你想到的那样):
- 1、指定接口参数
- 2、通用接口参数
- 3、应用参数
- 4、系统参数(通常忽略,因为只有service)
1.12.5 在线接口参数查询工具
为了便于理解上面全部的参数规则,对于具体接口调用的要求,这里可以使用在线接口参数查询工具在浏览器访问查看:
/demo/checkApiParams.php?service=User.Login
可以看到:
此工具同时也可以方便客户端实时查看接口文档时,进行辅助的接口规则说明。
自描述数据
这里值得一提的是,我们这里所定义的参数规则实际上也是自描述数据。即配置的代码真实同步反映了参数的相关属性。
1.12.6 参数传递的方式
系统下GET和POST皆可,但是推荐:
1、service参数以GET方式传递,接口统一以/?service=XXX.XXX链接请求,便于交流,更重要的是当接口发生问题时,可以快速在服务器上通过nginx日志定位问题;
2、其他参数以POST方式传递,特别对于敏感数据,如密码,以相对保护数据安全;
3、在编写文档,或者进行调试时,可以全部临时使用GET方式,如本文档的写法,同时在浏览器时也可以使用GET;
- 4、如果需要对数据包进行加密或者压缩、自定义参数格式,可以重载PhalApi_Request::genData(),然后再继续使用参数规则解析;
1.12.7 参数规则
类型type | 参数名称 name | 是否必须require | 默认值default | 最小值&最大值min&max | 更多 |
---|---|---|---|---|---|
字符串 | string | true/false,默认false | 应为字符串 | 可选 | regex下标为正则匹配的规则;format下标可用于定义字符编码的类型,如utf8、gbk,gb2312 |
整数 | int | true/false,默认false | 应为整数 | 可选 | --- |
浮点数 | float | true/false,默认false | 应为浮点数 | 可选 | --- |
布尔值 | boolean | true/false,默认false | true/false | --- | 以下值会转换为true: ok, true, success, on, yes, 1 |
时间戳/日期 | date | true/false,默认false | 会按格式转换 | 可选,仅当为timestamp时才判断 | 格式:format 为timestamp时会将字符串的日期转换 |
数组 | array | true/false,默认false | 为非数组会自动转换/解析成数组 | 可选,判断数组元素个数 | 格式:format 为explode时,会根据separator将字符串分割成数组, 为json时,会json解析 |
枚举 | enum | true/false,默认false | 应为range中的某个元素 | --- | 必须,range,以数组指定枚举的范围 |
文件 | file | true/false,默认false | 数组类型 | min和max表示文件大小范围 | range下标表示允许上传的文件类型,ext表示需要过滤的文件扩展名 |
回调 | callable | true/false,默认false | --- | callback设置回调函数,params为回调函数的第三个参数,第一个为参数值,第二个为所配置的规则 |
温馨提示:
全部的参数规则,都可以配置desc下标,对应在线接口文档的”说明“部分。
如: array('name' => 'username', 'desc' => '用户名')
下面是对各类型的示例说明。
(1)字符串 string
当一个参数规则 未指定类型时,默认为string。一个完整的写法可以为:
array('name' => 'username', 'type' => 'string', 'require' => true, 'default' => 'nobody', 'min' => 1, 'max' => 10)
若传递的参数长度过长,如&username=alonglonglonglongname,则会异常失败返回:
{"ret":400,"data":[],"msg":"Illegal Param: username.len should <= 10, but now username.len = 21"}
但是当需要验证的是类型是中文的话会出现一点问题一个中文字符会占用3个字节所以在min和max验证的时候会出现一些问题,PhalApi提供了format方式对你需要验证长度的string进行指定格式可以排除此问题
array('name' => 'username', 'type' => 'string','format' => 'utf8', 'min' => 1, 'max' => 10)
对于正则表达式的验证,一个邮箱的例子是:
'email' => array(
'name' => 'email',
'require' => true,
'min' => '1',
'regex' => "/^([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)$/i",
'desc' => '邮箱',
),
(2)整型 int
如通常数据库中的id,即可配置成:
array('name' => 'id', 'type' => 'int', 'require' => true, 'min' => 1 )
当传递的参数,不在其配置的范围内时,如&id=0,则会异常失败返回:
{"ret":400,"data":[],"msg":"Illegal Param: id should >= 1, but now id = 0"}
(3)浮点 float
浮点型,类似整型的配置,此处略。
(4)布尔值 boolean
布尔值,主要是可以对一些字符串转换成布尔值,如ok, true, success, on, yes, 以及会被PHP解析成true的字符串,都会转换成true,方便调用。如通常的是否记住我:
array('name' => 'isRememberMe', 'type' => 'boolean', 'default' => true)
(5)日期 date
日期可以按自己约定的格式传递,当需要将字符串的日期转换成timestamp时,可以这样配置:
array('name' => 'registerData', 'type' => 'date')
对应地,risterData=2015-01-31 10:00:00则会被获取到为:"2015-01-31 10:00:00"。
如果是配置成:
array('name' => 'registerData', 'type' => 'date', 'format' => 'timestamp')
则上面的参数再请求时,则会被转换成:1422669600。
(6)数组 array
很多时候在接口进行批量获取时,都需要提供一组参数,所以这时可以使用数组来进行配置。如:
array('name' => 'uids', 'type' => 'array', 'format' => 'explode', 'separator' => ',')
对应&uids=1,2,3则会被转换成:
array ( 0 => '1', 1 => '2', 2 => '3', )
又如接口需要使用JSON来传递整块参数时,可以这样配置:
array('name' => 'params', 'type' => 'array', 'format' => 'json')
对应¶ms={"username":"test","password":"123456"}则会被转换成:
array ( 'username' => 'test', 'password' => '123456', )
特别地,当配置成了数组,却未指定格式format时,会转换成一个元素的数组,如:&name=test,会转换成:array('test')。
(7)枚举 enum
在需要对接口参数进行范围限制时,可以使用此枚举型。如对于性别的参数,可以这样配置:
array('name' => 'sex', 'type' => 'enum', 'range' => array('female', 'male'))
当传递的参数不合法时,如&sex=unknow,则会被拦截,返回失败:
{"ret":400,"data":[],"msg":"Illegal Param: sex should be in female\/male, but now sex = unknow"}
关于枚举类型的配置,这里需要特别注意配置时,应尽量使用字符串的值。
因为通常而言,接口通过GET/POST方式获取到的参数都是字符串的,而如果配置规则时指定范围用了整型,会导致底层规则验证时误。如:
//接口参数为: &type=N
//接口参数规则为:
array('name' => 'type', 'type' => 'enum', 'range' => array(0, 1, 2))
//误判,因为:
var_dump(in_array('N', array(0, 1, 2))); //结果为true,因为 'N' == 0
为了避免这类情况发生,应该这样配置:
//接口参数规则为(使用字符串):
array('name' => '&type', 'type' => 'enum', 'range' => array(`0`, `1`, `2`))
(8)文件 file
在需要对上传的文件进行过滤、接收和处理时,可以使用文件类型,如:
array(
'name' => 'upfile',
'type' => 'file',
'min' => 0,
'max' => 1024 * 1024,
'range' => array('image/jpeg', 'image/png') ,
'ext' => array('txt','xml')
)
其中,min和max分别对应文件大小的范围,单位为字节;range为允许的文件类型,使用数组配置,且不区分大小写。
如果成功,返回的值对应的是$_FILES["upfile"],即会返回:
array(
'name' => '',
'type' => '',
'size' => '',
'tmp_name' => '',
)
对应的是:
- $_FILES["upfile"]["name"] - 被上传文件的名称
- $_FILES["upfile"]["type"] - 被上传文件的类型
- $_FILES["upfile"]["size"] - 被上传文件的大小,以字节计
- $_FILES["upfile"]["tmp_name"] - 存储在服务器的文件的临时副本的名称
- $_FILES["upfile"]["error"] - 由文件上传导致的错误代码
若需要配置默认值default选项,则也应为一数组,且其格式应类似如上。
其中,ext是对文件后缀名进行验证,当如果上传文件后缀名不匹配时将抛出异常。文件扩展名的过滤可以类似这样进行配置:
//单个后缀名 - 数组形式
'ext' => array('jpg')
//单个后缀名 - 字符串形式
'ext' => 'jpg'
//多个后缀名 - 数组形式
'ext' => array('jpg', 'jpeg', 'png', 'bmp')
//多个后缀名 - 字符串形式(以英文逗号分割)
'ext' => 'jpg,jpeg,png,bmp'
(9)回调 callable
当需要利用已有函数进行自定义验证时,可采用回调参数规则,如:
//配置规则
array('name' => 'version', 'type' => 'callable', 'callback' => array('Common_MyVersion', 'formatVersion'))
然后,回调时将调用下面这个函数:
//新增一个自定义的版本检测函数
class Common_MyVersion {
public static function formatVersion($value, $rule) {
if (count(explode('.', $value)) < 3) {
throw new PhalApi_Exception_BadRequest('版本号格式错误');
}
}
}
温馨提示:第一个为参数值,第二个为所配置的规则,第三个参数为配置规则中的params(可忽略)
1.12.8 关于参数设计的原则
(1)通配的$_REQUEST
使用$_REQUEST获取参数,便于在不同场合下GET/POST之间的切换,同时在初始化DI()->request服务时,可以指定传递的参数,以便于灵活的单元测试;
(2)更自由的名称映射
之所以没把规则配置的下标默认成与客户端传递的name一致,是为了更自由的名称映射;
如可能我们PHP后台开发喜欢用驼峰法来表示,但客户端想用下划线来分割,则通过这样配置:
array(
'isRememberMe' => array('name' => 'is_remember_me', 'type' => 'boolean', 'default' => true),
)
更重要的是,有时我们希望能缩短客户端请求的参数名称以节省流量时,可以这样配置:
array(
'isRememberMe' => array('name' => 're', 'type' => 'boolean', 'default' => true),
)
(3)异常返回
对于客户端参数不合法时,以异常失败返回,而不是隐性地转换,是因为后台接口往往需要手动对传递的参数进行人工的验证,而不是希望得到隐性转换的值。即当客户端参数传递不对时,我们需要明确提示说:参数非法。
1.12.9 扩展你的参数
当PhalApi提供的参数规则不能满足接口参数的规则验证时,除了使用callable类型进行扩展外,还可以扩展PhalApi_Request_Formatter接口来定制项目需要的类型。
一如既往,分两步:
- 1、扩展实现PhalApi_Request_Formatter接口
- 2、在DI注册你的类型
下面以大家所熟悉的邮件类型为例,说明扩展的步骤。
首先,我们需要一个实现了邮件类型验证的功能类:
<?php
class Common_MyFormatter_Email implements PhalApi_Request_Formatter {
public function parse($value, $rule) {
if (!preg_match('/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/', $value)) {
throw new PhalApi_Exception_BadRequest('邮箱地址格式错误');
}
return $value;
}
}
然后,注册一下:
DI()->_formatterEmail = 'Common_MyFormatter_Email';
温馨提示:在DI中手动注册服务时,名称的格式为: 下划线("_") + 统一前缀("formatter") + 参数类型(全部小写后,首字母大写);
若需要实现自动注册,扩展的类名格式须为:class PhalApi_Request_Formatter_{类型名称} implements PhalApi_Request_Formatter { ...
系统已自动注册的格式化服务有:
- _formatterArray 数组格式化服务
- _formatterBoolean 布尔值格式化服务
- _formatterCallable 回调格式化服务
- _formatterDate 日期格式化服务
- _formatterEnum 枚举格式化服务
- _formatterFile 上传文件格式化服务
- _formatterFloat 浮点数格式化服务
- _formatterInt 整数格式化服务
- _formatterString 字符串格式化服务
至此,便可使用自己定制的类型规则了,
array('name' => 'user_email', 'type' => 'email')