“播下一種思想,收獲一種行為;播下一種行為,收獲一種習(xí)慣;播下一種習(xí)慣,收獲一種性格;播下一種性格,收獲一種命運(yùn)?!? --《成君憶:水煮三國》
參數(shù),對于接口來說,是非常重要的輸入。對于外部調(diào)用來說,同等重要。
因此,對于參數(shù)這塊,我們是希望能夠既減輕后臺(tái)開發(fā)對接口參數(shù)獲取、判斷、驗(yàn)證、文檔編寫的痛苦;又便于客戶端方便的、自由的調(diào)用;既利已又利他。
由此,我們引入了 參數(shù)解析 這一概念,即:通過配置參數(shù)的規(guī)則,即可自動(dòng)實(shí)現(xiàn)參數(shù)的獲取和驗(yàn)證。
熟悉Yii的同學(xué),對于以下的規(guī)則配置應(yīng)該倍感親切,但是不熟悉的同學(xué)也可以同樣快速上手。因?yàn)椋銜?huì)慢慢發(fā)現(xiàn),這樣的規(guī)則很符合我們PHP開發(fā)的規(guī)范,如果沒有,我們繼續(xù)努力改進(jìn)。
格式如下:
array(
'參數(shù)名' => array('name' => '接口參數(shù)名稱', 'type' => '類型', 'default' => '默認(rèn)值', ...),
... ...
)
假設(shè)這樣的業(yè)務(wù)場景,我們需要提供一個(gè)用戶登錄的接口,其中需要用戶名和密碼,因此:
<?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);
}
}
當(dāng)我們這樣調(diào)用接口時(shí):
/?service=User.Login&username=test&password=123456
就可以獲取到需要的參數(shù):
{"ret":0,"data":{"username":"test","password":"123456"},"msg":""}
從中,可以很容易理解:參數(shù)規(guī)則需要統(tǒng)一配置在接口實(shí)現(xiàn)類里面的 getRules() 函數(shù),隨后即可以通過類成員屬性方式獲取,如: $this->username 。
很多時(shí)候我們都會(huì)對用戶名和密碼作一些驗(yàn)證,如是否必須、長度、最值,以及默認(rèn)值等。
繼續(xù)上面的業(yè)務(wù)場景,我們登錄下用戶名和密碼必須,且密碼長度至少為6個(gè)字符,則可以調(diào)整參數(shù)規(guī)則:
'login' => array(
'username' => array('name' => 'username', 'require' => true),
'password' => array('name' => 'password', 'require' => true, 'min' => 6),
),
嘗試一下非法的參數(shù)請求,如無任何參數(shù)的情況下,訪問/?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"}
已被系統(tǒng)固定占有的參數(shù),目前只有一個(gè),即:service,為需要調(diào)用的服務(wù),類型為字符串,格式為:XXX.XXX,首字母不區(qū)分大小寫,建議統(tǒng)一以大寫開頭。
以下是一些示例:
#推薦寫法
/?service=User.GetBaseInfo
#正確寫法(開頭小寫)
/?service=user.getBaseInfo
#正確寫法(方法名小寫,但類名只能開頭小寫,否則會(huì)導(dǎo)致linux系統(tǒng)下文件加載失?。?/?service=user.getbaseinfo
#錯(cuò)誤寫法(缺少方法名)
/?service=User
#錯(cuò)誤寫法(缺少點(diǎn)號分割)
/?service=UserGetBaseInfo
#錯(cuò)誤寫法(默認(rèn)只支持點(diǎn)號分割)
/?service=User|GetBaseInfo
應(yīng)用參數(shù)是指在一個(gè)項(xiàng)目中,全部接口都需要的參數(shù),或者通用的參數(shù)規(guī)則。假如我們的項(xiàng)目中全部需要簽名sign參數(shù),且必須;以及非必須的版本號,則可以在./Config/app.php中的apiCommonRules配置:
//$vim ./Config/app.php
<?php
return array(
/**
* 應(yīng)用接口層的統(tǒng)一參數(shù)
*/
'apiCommonRules' => array(
//簽名
'sign' => array(
'name' => 'sign', 'require' => true,
),
//客戶端App版本號,如:1.0.1
'version' => array(
'name' => 'version', 'default' => '',
),
),
... ...
接口參數(shù)即為上面在各個(gè)接口子類中配置的規(guī)則,為特定接口所持有。同時(shí),為了方便同一套接口的規(guī)則重用,可以使用下標(biāo)為 '*' 表示是本接口通用規(guī)則,如我們?yōu)榱思訌?qiáng)安全性,為全部的用戶接口操作都加上4位的驗(yàn)證碼:
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),
),
);
}
在完成對上面的應(yīng)用參數(shù)規(guī)則、接口通用規(guī)則和指定規(guī)則的參數(shù)進(jìn)行配置后,對用戶登錄的接口進(jìn)行請求時(shí)就需要這樣訪問:
/?service=User.login&sign=77f81c17d512302383e5f26b99dae4d9&username=test&password=123456&code=abcd
溫馨提示:在Api類里面配置規(guī)則時(shí),下標(biāo)不區(qū)分大小寫。因?yàn)榭蚣軙?huì)自動(dòng)將請求的函數(shù)名和全部的規(guī)則下標(biāo)轉(zhuǎn)換成小寫進(jìn)行匹配。
這里,再小結(jié)一下,接口參數(shù)可以分為兩種: 通用接口參數(shù) 和 指定接口參數(shù) 。前者用 * 號下標(biāo)表示,后者則用函數(shù)名作為下標(biāo)表示。
當(dāng)同一個(gè)參數(shù)規(guī)則分別在應(yīng)用參數(shù)、接口通用參數(shù)及特定接口參數(shù)出現(xiàn)時(shí),后面的規(guī)則會(huì)覆蓋前面的,即具體化的規(guī)則會(huì)替換通用的規(guī)則,以保證接口在特定場合的定制性。
簡而言之,多個(gè)參數(shù)規(guī)則的優(yōu)先級從高到下,分別是(正如你想到的那樣):
為了便于理解上面全部的參數(shù)規(guī)則,對于具體接口調(diào)用的要求,這里可以使用在線接口參數(shù)查詢工具在瀏覽器訪問查看:
/demo/checkApiParams.php?service=User.Login
可以看到:
此工具同時(shí)也可以方便客戶端實(shí)時(shí)查看接口文檔時(shí),進(jìn)行輔助的接口規(guī)則說明。
這里值得一提的是,我們這里所定義的參數(shù)規(guī)則實(shí)際上也是自描述數(shù)據(jù)。即配置的代碼真實(shí)同步反映了參數(shù)的相關(guān)屬性。
系統(tǒng)下GET和POST皆可,但是推薦:
1、service參數(shù)以GET方式傳遞,接口統(tǒng)一以/?service=XXX.XXX鏈接請求,便于交流,更重要的是當(dāng)接口發(fā)生問題時(shí),可以快速在服務(wù)器上通過nginx日志定位問題;
2、其他參數(shù)以POST方式傳遞,特別對于敏感數(shù)據(jù),如密碼,以相對保護(hù)數(shù)據(jù)安全;
3、在編寫文檔,或者進(jìn)行調(diào)試時(shí),可以全部臨時(shí)使用GET方式,如本文檔的寫法,同時(shí)在瀏覽器時(shí)也可以使用GET;
類型type | 參數(shù)名稱 name | 是否必須require | 默認(rèn)值default | 最小值&最大值min&max | 更多 |
---|---|---|---|---|---|
字符串 | string | true/false,默認(rèn)false | 應(yīng)為字符串 | 可選 | regex下標(biāo)為正則匹配的規(guī)則;format下標(biāo)可用于定義字符編碼的類型,如utf8、gbk,gb2312 |
整數(shù) | int | true/false,默認(rèn)false | 應(yīng)為整數(shù) | 可選 | --- |
浮點(diǎn)數(shù) | float | true/false,默認(rèn)false | 應(yīng)為浮點(diǎn)數(shù) | 可選 | --- |
布爾值 | boolean | true/false,默認(rèn)false | true/false | --- | 以下值會(huì)轉(zhuǎn)換為true: ok, true, success, on, yes, 1 |
時(shí)間戳/日期 | date | true/false,默認(rèn)false | 會(huì)按格式轉(zhuǎn)換 | 可選,僅當(dāng)為timestamp時(shí)才判斷 | 格式:format 為timestamp時(shí)會(huì)將字符串的日期轉(zhuǎn)換 |
數(shù)組 | array | true/false,默認(rèn)false | 為非數(shù)組會(huì)自動(dòng)轉(zhuǎn)換/解析成數(shù)組 | 可選,判斷數(shù)組元素個(gè)數(shù) | 格式:format 為explode時(shí),會(huì)根據(jù)separator將字符串分割成數(shù)組, 為json時(shí),會(huì)json解析 |
枚舉 | enum | true/false,默認(rèn)false | 應(yīng)為range中的某個(gè)元素 | --- | 必須,range,以數(shù)組指定枚舉的范圍 |
文件 | file | true/false,默認(rèn)false | 數(shù)組類型 | min和max表示文件大小范圍 | range下標(biāo)表示允許上傳的文件類型,ext表示需要過濾的文件擴(kuò)展名 |
回調(diào) | callable | true/false,默認(rèn)false | --- | callback設(shè)置回調(diào)函數(shù),params為回調(diào)函數(shù)的第三個(gè)參數(shù),第一個(gè)為參數(shù)值,第二個(gè)為所配置的規(guī)則 |
溫馨提示:
全部的參數(shù)規(guī)則,都可以配置desc下標(biāo),對應(yīng)在線接口文檔的”說明“部分。
如: array('name' => 'username', 'desc' => '用戶名')
下面是對各類型的示例說明。
當(dāng)一個(gè)參數(shù)規(guī)則 未指定類型時(shí),默認(rèn)為string。一個(gè)完整的寫法可以為:
array('name' => 'username', 'type' => 'string', 'require' => true, 'default' => 'nobody', 'min' => 1, 'max' => 10)
若傳遞的參數(shù)長度過長,如&username=alonglonglonglongname,則會(huì)異常失敗返回:
{"ret":400,"data":[],"msg":"Illegal Param: username.len should <= 10, but now username.len = 21"}
但是當(dāng)需要驗(yàn)證的是類型是中文的話會(huì)出現(xiàn)一點(diǎn)問題一個(gè)中文字符會(huì)占用3個(gè)字節(jié)所以在min和max驗(yàn)證的時(shí)候會(huì)出現(xiàn)一些問題,PhalApi提供了format方式對你需要驗(yàn)證長度的string進(jìn)行指定格式可以排除此問題
array('name' => 'username', 'type' => 'string','format' => 'utf8', 'min' => 1, 'max' => 10)
對于正則表達(dá)式的驗(yàn)證,一個(gè)郵箱的例子是:
'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' => '郵箱',
),
如通常數(shù)據(jù)庫中的id,即可配置成:
array('name' => 'id', 'type' => 'int', 'require' => true, 'min' => 1 )
當(dāng)傳遞的參數(shù),不在其配置的范圍內(nèi)時(shí),如&id=0,則會(huì)異常失敗返回:
{"ret":400,"data":[],"msg":"Illegal Param: id should >= 1, but now id = 0"}
浮點(diǎn)型,類似整型的配置,此處略。
布爾值,主要是可以對一些字符串轉(zhuǎn)換成布爾值,如ok, true, success, on, yes, 以及會(huì)被PHP解析成true的字符串,都會(huì)轉(zhuǎn)換成true,方便調(diào)用。如通常的是否記住我:
array('name' => 'isRememberMe', 'type' => 'boolean', 'default' => true)
日期可以按自己約定的格式傳遞,當(dāng)需要將字符串的日期轉(zhuǎn)換成timestamp時(shí),可以這樣配置:
array('name' => 'registerData', 'type' => 'date')
對應(yīng)地,risterData=2015-01-31 10:00:00則會(huì)被獲取到為:"2015-01-31 10:00:00"。
如果是配置成:
array('name' => 'registerData', 'type' => 'date', 'format' => 'timestamp')
則上面的參數(shù)再請求時(shí),則會(huì)被轉(zhuǎn)換成:1422669600。
很多時(shí)候在接口進(jìn)行批量獲取時(shí),都需要提供一組參數(shù),所以這時(shí)可以使用數(shù)組來進(jìn)行配置。如:
array('name' => 'uids', 'type' => 'array', 'format' => 'explode', 'separator' => ',')
對應(yīng)&uids=1,2,3則會(huì)被轉(zhuǎn)換成:
array ( 0 => '1', 1 => '2', 2 => '3', )
又如接口需要使用JSON來傳遞整塊參數(shù)時(shí),可以這樣配置:
array('name' => 'params', 'type' => 'array', 'format' => 'json')
對應(yīng)¶ms={"username":"test","password":"123456"}則會(huì)被轉(zhuǎn)換成:
array ( 'username' => 'test', 'password' => '123456', )
特別地,當(dāng)配置成了數(shù)組,卻未指定格式format時(shí),會(huì)轉(zhuǎn)換成一個(gè)元素的數(shù)組,如:&name=test,會(huì)轉(zhuǎn)換成:array('test')。
在需要對接口參數(shù)進(jìn)行范圍限制時(shí),可以使用此枚舉型。如對于性別的參數(shù),可以這樣配置:
array('name' => 'sex', 'type' => 'enum', 'range' => array('female', 'male'))
當(dāng)傳遞的參數(shù)不合法時(shí),如&sex=unknow,則會(huì)被攔截,返回失?。?/p>
{"ret":400,"data":[],"msg":"Illegal Param: sex should be in female\/male, but now sex = unknow"}
關(guān)于枚舉類型的配置,這里需要特別注意配置時(shí),應(yīng)盡量使用字符串的值。
因?yàn)橥ǔ6?,接口通過GET/POST方式獲取到的參數(shù)都是字符串的,而如果配置規(guī)則時(shí)指定范圍用了整型,會(huì)導(dǎo)致底層規(guī)則驗(yàn)證時(shí)誤。如:
//接口參數(shù)為: &type=N
//接口參數(shù)規(guī)則為:
array('name' => 'type', 'type' => 'enum', 'range' => array(0, 1, 2))
//誤判,因?yàn)椋?var_dump(in_array('N', array(0, 1, 2))); //結(jié)果為true,因?yàn)?'N' == 0
為了避免這類情況發(fā)生,應(yīng)該這樣配置:
//接口參數(shù)規(guī)則為(使用字符串):
array('name' => '&type', 'type' => 'enum', 'range' => array(`0`, `1`, `2`))
在需要對上傳的文件進(jìn)行過濾、接收和處理時(shí),可以使用文件類型,如:
array(
'name' => 'upfile',
'type' => 'file',
'min' => 0,
'max' => 1024 * 1024,
'range' => array('image/jpeg', 'image/png') ,
'ext' => array('txt','xml')
)
其中,min和max分別對應(yīng)文件大小的范圍,單位為字節(jié);range為允許的文件類型,使用數(shù)組配置,且不區(qū)分大小寫。
如果成功,返回的值對應(yīng)的是$_FILES["upfile"],即會(huì)返回:
array(
'name' => '',
'type' => '',
'size' => '',
'tmp_name' => '',
)
對應(yīng)的是:
若需要配置默認(rèn)值default選項(xiàng),則也應(yīng)為一數(shù)組,且其格式應(yīng)類似如上。
其中,ext是對文件后綴名進(jìn)行驗(yàn)證,當(dāng)如果上傳文件后綴名不匹配時(shí)將拋出異常。文件擴(kuò)展名的過濾可以類似這樣進(jìn)行配置:
//單個(gè)后綴名 - 數(shù)組形式
'ext' => array('jpg')
//單個(gè)后綴名 - 字符串形式
'ext' => 'jpg'
//多個(gè)后綴名 - 數(shù)組形式
'ext' => array('jpg', 'jpeg', 'png', 'bmp')
//多個(gè)后綴名 - 字符串形式(以英文逗號分割)
'ext' => 'jpg,jpeg,png,bmp'
當(dāng)需要利用已有函數(shù)進(jìn)行自定義驗(yàn)證時(shí),可采用回調(diào)參數(shù)規(guī)則,如:
//配置規(guī)則
array('name' => 'version', 'type' => 'callable', 'callback' => array('Common_MyVersion', 'formatVersion'))
然后,回調(diào)時(shí)將調(diào)用下面這個(gè)函數(shù):
//新增一個(gè)自定義的版本檢測函數(shù)
class Common_MyVersion {
public static function formatVersion($value, $rule) {
if (count(explode('.', $value)) < 3) {
throw new PhalApi_Exception_BadRequest('版本號格式錯(cuò)誤');
}
}
}
溫馨提示:第一個(gè)為參數(shù)值,第二個(gè)為所配置的規(guī)則,第三個(gè)參數(shù)為配置規(guī)則中的params(可忽略)
使用$_REQUEST獲取參數(shù),便于在不同場合下GET/POST之間的切換,同時(shí)在初始化DI()->request服務(wù)時(shí),可以指定傳遞的參數(shù),以便于靈活的單元測試;
之所以沒把規(guī)則配置的下標(biāo)默認(rèn)成與客戶端傳遞的name一致,是為了更自由的名稱映射;
如可能我們PHP后臺(tái)開發(fā)喜歡用駝峰法來表示,但客戶端想用下劃線來分割,則通過這樣配置:
array(
'isRememberMe' => array('name' => 'is_remember_me', 'type' => 'boolean', 'default' => true),
)
更重要的是,有時(shí)我們希望能縮短客戶端請求的參數(shù)名稱以節(jié)省流量時(shí),可以這樣配置:
array(
'isRememberMe' => array('name' => 're', 'type' => 'boolean', 'default' => true),
)
對于客戶端參數(shù)不合法時(shí),以異常失敗返回,而不是隱性地轉(zhuǎn)換,是因?yàn)楹笈_(tái)接口往往需要手動(dòng)對傳遞的參數(shù)進(jìn)行人工的驗(yàn)證,而不是希望得到隱性轉(zhuǎn)換的值。即當(dāng)客戶端參數(shù)傳遞不對時(shí),我們需要明確提示說:參數(shù)非法。
當(dāng)PhalApi提供的參數(shù)規(guī)則不能滿足接口參數(shù)的規(guī)則驗(yàn)證時(shí),除了使用callable類型進(jìn)行擴(kuò)展外,還可以擴(kuò)展PhalApi_Request_Formatter接口來定制項(xiàng)目需要的類型。
一如既往,分兩步:
下面以大家所熟悉的郵件類型為例,說明擴(kuò)展的步驟。
首先,我們需要一個(gè)實(shí)現(xiàn)了郵件類型驗(yàn)證的功能類:
<?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('郵箱地址格式錯(cuò)誤');
}
return $value;
}
}
然后,注冊一下:
DI()->_formatterEmail = 'Common_MyFormatter_Email';
溫馨提示:在DI中手動(dòng)注冊服務(wù)時(shí),名稱的格式為: 下劃線("_") + 統(tǒng)一前綴("formatter") + 參數(shù)類型(全部小寫后,首字母大寫);
若需要實(shí)現(xiàn)自動(dòng)注冊,擴(kuò)展的類名格式須為:class PhalApi_Request_Formatter_{類型名稱} implements PhalApi_Request_Formatter { ...
系統(tǒng)已自動(dòng)注冊的格式化服務(wù)有:
至此,便可使用自己定制的類型規(guī)則了,
array('name' => 'user_email', 'type' => 'email')
更多建議: