概述
Laravel 是一個(gè)非常流行的 PHP 框架,我們可以使用它快速構(gòu)建一個(gè) WEB 應(yīng)用程序。而現(xiàn)在 WEB 應(yīng)用中多會(huì)采用前后端分離技術(shù),所以我們經(jīng)常會(huì)遇到使用 Laravel 搭建 API 項(xiàng)目的需求。
Laravel 在提供 API 這方面,很多地方都只是提供了一個(gè)規(guī)范,并沒(méi)有告訴我們?nèi)绾稳?shí)現(xiàn)它。這樣帶來(lái)的好處是 Laravel 放開(kāi)了限制,使大家可以按照自己的習(xí)慣去使用它。
但這樣做也給剛接觸 Laravel 不久的同學(xué)帶來(lái)了一些困擾:到底怎樣使用這個(gè)框架才更優(yōu)雅些呢,有沒(méi)有例子可以參考下呢。
本項(xiàng)目就是為了給大家提供一個(gè)參考而建立的,這是一個(gè)使用 Laravel 框架實(shí)現(xiàn)的 API 項(xiàng)目,項(xiàng)目中提供了一些常見(jiàn)功能的示例。
本項(xiàng)目從零開(kāi)始,在重要修改部分末尾都會(huì)添加 [commit] 鏈接, 可自行查看變更記錄。
需要注意的是:
- 這并不是一個(gè) Laravel 的新手教程,文中很多地方需要你了解 Laravel 的基礎(chǔ)知識(shí)。 Laravel官網(wǎng)文檔
- 如果你已經(jīng)有自己的實(shí)現(xiàn)方式,可以略過(guò)此教程,這個(gè)項(xiàng)目的實(shí)現(xiàn)方式可能也沒(méi)比你的實(shí)現(xiàn)方式更優(yōu)雅。
- Laravel 各個(gè)版本之前實(shí)現(xiàn)方式還是有一些區(qū)別的,要注意區(qū)分,但整體思路是一樣的。
安裝
這里使用 composer 安裝 Laravel ,下面命令會(huì)安裝最新版本的 Laravel 。此項(xiàng)目創(chuàng)建時(shí)的 Laravel 版本為 v8.21.0
composer create-project --prefer-dist laravel/laravel laravel-api-example
安裝好之后需要自行配置項(xiàng)目使其可以對(duì)外訪問(wèn),當(dāng)在瀏覽器中輸入項(xiàng)目地址進(jìn)入到 Laravel 的歡迎頁(yè)時(shí),就可以繼續(xù)向下閱讀了。
路由
在歡迎頁(yè)我們可以看到,Laravel 返回的信息是一個(gè) web 頁(yè)面,也就是 html 代碼。這個(gè)默認(rèn)的路由是在 routes/web.php 中定義的,我們需要把它給移除掉。[commit]
我們所有的路由都要定義到 routes/api.php 中,這個(gè)是專門用來(lái)定義 API 路由的文件,當(dāng)然如果你的路由特別多你也可以在 routes 中定義其他路由文件,然后在 RouteServiceProvider 中按照同樣的方式去加載它們。
在 RouteServiceProvider 中我們可以看到,我們?cè)?routes/api.php 中定義的路由會(huì)默認(rèn)加上 api 前綴,這對(duì) WEB 和 API 混寫(xiě)在同一個(gè)項(xiàng)目中很有必要,但單獨(dú)的 API 項(xiàng)目一般也會(huì)單獨(dú)域名。如:
https://api.apihubs.cn/holiday/get
所以我們要移除這個(gè) api 前綴或換成其他前綴如接口版本號(hào) V1 [commit]
在 routes/api.php 中默認(rèn)的路由是一個(gè)需要身份驗(yàn)證的 /user
我們使用瀏覽器或 postman 訪問(wèn)的時(shí)候,會(huì)得到一個(gè)錯(cuò)誤頁(yè)面,其中的主要信息為:Route [login] not defined.
我們使用 ajax 或者在 postman 的 header 中添加 X-Requested-With:XMLHttpRequest 頭信息后又會(huì)得到一個(gè) JSON 的錯(cuò)誤信息:{"message": "Unauthenticated."}
這實(shí)際上都是未登錄的原因,在未登錄訪問(wèn)需要鑒權(quán)的接口時(shí) Laravel 會(huì)拋出一個(gè) AuthenticationException ,而在響應(yīng)類 Response 中會(huì)根據(jù)請(qǐng)求的 header 頭自動(dòng)做出響應(yīng)。
也就是如果是以頁(yè)面形式調(diào)用的,就會(huì)跳轉(zhuǎn)到登錄頁(yè)面,因?yàn)轫?xiàng)目中還沒(méi)定義登錄頁(yè)面的路由就出現(xiàn)我們上面看到的那個(gè)錯(cuò)誤。如果是以接口形式訪問(wèn)的就會(huì) 401 狀態(tài)碼并返回 JSON 信息。
但我們這是一個(gè) API 項(xiàng)目,提供出去的都是接口地址,在接口地址中一會(huì)返回 JSON 一會(huì)又返回一個(gè)頁(yè)面這是不是顯得很尷尬。
這里我們需要新增一個(gè) Middleware 來(lái)解決這個(gè)問(wèn)題。然后在 Kernel 中注冊(cè)這個(gè) Middleware 使其全局生效。 [commit]
php artisan make:middleware JsonApplication
這樣我們已經(jīng)配置好一個(gè) JSON 應(yīng)用了,在 Laravel 拋出任何異常時(shí),無(wú)論我們以什么方式訪問(wèn)都會(huì)始終得到 JSON 響應(yīng)信息。
需要注意的是我們依然不能在路由的閉包或控制器中使用 return view($view) ,因?yàn)檫@會(huì)強(qiáng)制返回一個(gè) html 頁(yè)面響應(yīng)。
我們應(yīng)該在路由的閉包或控制器中始終 return 一個(gè)對(duì)象或數(shù)組,這兩種格式會(huì)使 Laravel 自動(dòng)為我們返回正確的 JSON 信息。
錯(cuò)誤碼
通過(guò)上面的配置,我們的應(yīng)用可以始終返回 JSON 信息了。比如在出現(xiàn)異常的時(shí)候:
- 鑒權(quán)失敗會(huì)返回 http 狀態(tài)碼 401 的 {"message": "Unauthenticated."}
- 請(qǐng)求的 method 不正確會(huì)返回 http 狀態(tài)碼 405 的 {"message":"The POST method is not supported for this route. Supported methods: GET, HEAD.","exception":"Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException",...}
- 請(qǐng)求的 URL 地址不存在會(huì)返回 404 的 {"message":"","exception":"Symfony\Component\HttpKernel\Exception\NotFoundHttpException",...}
- ...
但這樣的返回信息也有問(wèn)題:
- 返回信息的格式并不統(tǒng)一,JSON 中的 key 時(shí)有時(shí)無(wú),接口調(diào)用方在很多時(shí)候找不到要以什么作為依據(jù)進(jìn)行判斷
- 許多異常信息為服務(wù)端敏感信息,會(huì)直接報(bào)漏給用戶,存在安全隱患
- 異常信息都是通過(guò) HTTP 狀態(tài)碼拋出的,會(huì)導(dǎo)致許多錯(cuò)誤的 HTTP 狀態(tài)碼相同,比如 500
而通常的做法是需要根據(jù)不同的業(yè)務(wù)場(chǎng)景定義不同的錯(cuò)誤代碼和錯(cuò)誤信息, 然后始終返會(huì) http 狀態(tài)碼 200 的 {"code":"","msg":"","data":""}
在這里我們需要引入一個(gè)第三方的庫(kù)(這個(gè)庫(kù)會(huì)在項(xiàng)目中許多地方使用,也是本教程的核心,當(dāng)然這個(gè)庫(kù)的功能是仿照 Java 枚舉而來(lái)的) phpenum 。 [commit]
composer require phpenum/phpenum
這是一個(gè)枚舉庫(kù),在這里用來(lái)定義和管理錯(cuò)誤碼和錯(cuò)誤信息,錯(cuò)誤碼的位數(shù)應(yīng)該是固定的,至少一個(gè)模塊下的錯(cuò)誤碼位數(shù)是固定的,這里使用 5 位錯(cuò)誤碼,你可以根據(jù)實(shí)際使用場(chǎng)景來(lái)定義。
我們?cè)?app 目錄下新建一個(gè) Enums 目錄,然后添加 ErrorEnum 為不同的錯(cuò)誤和異常定義不同的錯(cuò)誤碼。 [commit]
定義好錯(cuò)誤碼后,我們還需要借助 Laravel 的 渲染異常 來(lái)渲染自定義異常類 ApiException 。 [commit]
php artisan make:exception ApiException
在上面這個(gè) commit 中,我們對(duì)常見(jiàn)的異常都做了處理,使他們返回固定的錯(cuò)誤碼和錯(cuò)誤信息,尤其對(duì)數(shù)據(jù)驗(yàn)證失敗在 data 中返回了詳細(xì)的錯(cuò)誤信息,你也可以在 Handler 中添加一些其他需要處理的異常。
而未特殊定義狀態(tài)碼的異常會(huì)統(tǒng)一返回錯(cuò)誤碼 99999 的 未知錯(cuò)誤,生產(chǎn)環(huán)境中是不應(yīng)該出現(xiàn)這個(gè)錯(cuò)誤碼的,這個(gè)異常一般出現(xiàn)在調(diào)試階段,我們需要解決掉它。
由于接口不再返回任何錯(cuò)誤信息了,我們排查問(wèn)題的方式也只能通過(guò)日志來(lái)排查 默認(rèn)的日志在你本地的這個(gè)目錄下 Laravel的日志也是非常強(qiáng)大,你可以隨意更改存儲(chǔ)的位置和介質(zhì),這里就不展開(kāi)介紹了。
到這里我們就配置好統(tǒng)一錯(cuò)誤碼了,接下來(lái)無(wú)論在項(xiàng)目中出現(xiàn)什么錯(cuò)誤,拋出什么異常,接口返回的信息始終保持為http狀態(tài)碼200的 {"code":"","msg":"","data":""}
但這些狀態(tài)碼都是系統(tǒng)產(chǎn)生異常時(shí)返回的,我們要自己返回自定義狀態(tài)碼要怎么做呢? 非常簡(jiǎn)單,你只需要在任何你想返回自定義狀態(tài)碼的地方拋出自定義異常就可以了(但除了 controller 層,其他層可能會(huì)以非 web 的方式掉用,比如 console ,它不應(yīng)該捕獲到 ApiException ,所以盡量保證在 controller 拋出 ApiException)
throw new ApiException(ErrorEnum::UNKNOWN_ERROR()); // {"code":99999,"msg":"服務(wù)器繁忙,請(qǐng)稍后再試","data":""}
throw new ApiException(ErrorEnum::UNKNOWN_ERROR(), 'This is an data'); // {"code":99999,"msg":"服務(wù)器繁忙,請(qǐng)稍后再試","data":"This is an data"}
那要是返回成功信息要怎么辦呢,這個(gè)實(shí)現(xiàn)方式有很多,可以用官方文檔示例中的 響應(yīng)宏 , 也可以使用幫助類,還可以使用...
這里我們選 Laravel 的 Resource , 新建一個(gè) Resource 類 JsonResponse, 在里面處理了常見(jiàn)的 Laravel 對(duì)象和添加分頁(yè)處理。 [commit]
這樣當(dāng)我們想返回成功信息時(shí)只需要 return 這個(gè)實(shí)例就可以了。
return new JsonResponse($mixed);
配置信息
Laravel 的配置信息都保存在 config 中,你也可以自定義自己的配置信息,獲取時(shí)只需要使用 config('filename.array_key') (支持多層級(jí))就能輕松的獲取到配置信息。
Laravel 的配置信息多是搭配了各個(gè)服務(wù)的門面模式來(lái)定義的,就比如 cache ,你只需要在 config 中修改 default driver ,就可以輕松的在 file 或 redis 或 database 等等等諸多存儲(chǔ)介質(zhì)之間切換,你還可以自定義存儲(chǔ)介質(zhì)。關(guān)于門面模式你可以自行查看 文檔 和 源碼 ,這里不展開(kāi)介紹了。
這里還有個(gè)問(wèn)題,就是比如數(shù)據(jù)庫(kù)配置,一般我們都會(huì)區(qū)分開(kāi)發(fā)、測(cè)試、生產(chǎn)環(huán)境,但是 config 中的配置只能在倉(cāng)庫(kù)中保存一份,這里我們就要用到另外一個(gè)特殊配置文件 .env
所有配置文件中以類似于 env('DB_HOST', '127.0.0.1') 這種方式定義的都會(huì)讀取 .env 文件,第一個(gè)參數(shù)作為配置名稱,如果未在 .env 文件中定義,則使用第二個(gè)參數(shù)默認(rèn)值返回
一般我們會(huì)把所有區(qū)分環(huán)境的配置都定義在這個(gè)文件中
注意這個(gè)文件是不能提交到倉(cāng)庫(kù)的,所以你拉去代碼后很有可能看不到這個(gè)文件,只需要將 .env.example copy 一份為 .env 即可 (首次安裝 Laravel 會(huì)自動(dòng)執(zhí)行 copy) 然后配置好正確的數(shù)據(jù)庫(kù)連接信息
數(shù)據(jù)庫(kù)遷移(生產(chǎn)環(huán)境使用需謹(jǐn)慎)
大多數(shù)框架都有數(shù)據(jù)庫(kù)遷移功能,它對(duì)保持?jǐn)?shù)據(jù)庫(kù)結(jié)構(gòu)的一致性起到非常大的作用,但這個(gè)功能如果沒(méi)有合理使用則風(fēng)險(xiǎn)非常大,我之前就有同事使用了這個(gè)功能不小心把所有表都給重置了,還好數(shù)據(jù)是有備份的,及時(shí)進(jìn)行了恢復(fù)。
Laravel 的數(shù)據(jù)遷移文件在 database/migrations 中,由于 Laravel 框架是國(guó)外開(kāi)發(fā)者開(kāi)發(fā)的,他們對(duì)用戶的信息是以 email 為主,我們要在 user 表中增加手機(jī)號(hào)碼字段。[commit]
這里我們是直接對(duì)表遷移文件進(jìn)行修改,是因?yàn)槲覀冞€沒(méi)有進(jìn)行數(shù)據(jù)庫(kù)遷移,當(dāng)你執(zhí)行過(guò)數(shù)據(jù)庫(kù)遷移后,Laravel 會(huì)在數(shù)據(jù)庫(kù)中記錄你已經(jīng)遷移過(guò)的文件,這時(shí)如果再想修改應(yīng)使用 更新表 的操作
接下來(lái)就可以執(zhí)行數(shù)據(jù)庫(kù)遷移操作了 (開(kāi)發(fā)環(huán)境)
php artisan migrate
驗(yàn)證規(guī)則
Laravel 提供了非常多的 驗(yàn)證規(guī)則 ,這些驗(yàn)證規(guī)則可以滿足大數(shù)據(jù)的驗(yàn)證場(chǎng)景,部分特殊驗(yàn)證需要我們自定義驗(yàn)證規(guī)則,比如手機(jī)號(hào)碼
我們通過(guò)以下命令來(lái)創(chuàng)建一個(gè) 手機(jī)號(hào)碼的驗(yàn)證規(guī)則 [commit]
php artisan make:rule PhoneNumber
添加好規(guī)則后我們可以在 ServiceProvider 中為規(guī)則配置別名 [commit]
參數(shù)驗(yàn)證
驗(yàn)證規(guī)則一般可以直接寫(xiě)在 controller 中,也可以單獨(dú)定義 Requests 進(jìn)行管理,這里我們使用第二種方式統(tǒng)一在 Requests 中定義管理驗(yàn)證邏輯。
我們先來(lái)創(chuàng)建一個(gè)獲取驗(yàn)證碼的 Request [commit]
php artisan make:request GetSmsCodeRequest
Request 中一般我們要在 messages 方法中重新定義錯(cuò)誤信息,添加好 Request 后,我們就可以直接在 controller 中使用,結(jié)合之前我們的配置,當(dāng)驗(yàn)證不通過(guò)時(shí)會(huì)返回以下信息
{
"code": 10004,
"msg": "數(shù)據(jù)驗(yàn)證失敗",
"data": {
"phone_number": [
"請(qǐng)輸入您的手機(jī)號(hào)碼"
]
}
}
獲取驗(yàn)證碼
到這里我們的基礎(chǔ)配置就完成了,讓我們來(lái)實(shí)際的添加一個(gè)接口吧,首先在 routes/api.php 中添加一條獲取驗(yàn)證碼的路由(限制訪問(wèn)頻次 30 分鐘 100 次) [commit]
Route::middleware('throttle:100,30')->post('getSmsCode', [AuthController::class, 'getSmsCode']);
然后在添加 Controller Contract Service 以及在 ServiceProvider 中綁定 Contract 和 Service 的關(guān)系 [commit]
php artisan make:controller AuthController
這種實(shí)現(xiàn)方式是參考了 Laravel 的 自動(dòng)注入 和 Laravel 的 將接口綁定到實(shí)現(xiàn) 而實(shí)現(xiàn)的
這里為了方便測(cè)試,驗(yàn)證碼直接在接口中返回,實(shí)際使用需要修改為短信發(fā)送
注冊(cè)
按照上面的教程,我們先來(lái)添加一個(gè)注冊(cè)的路由 [commit]
Route::middleware('throttle:100,30')->post('register', [AuthController::class, 'register']);
然后添加一個(gè)驗(yàn)證碼的 rule 并為其添加別名 [commit]
使用驗(yàn)證碼規(guī)則創(chuàng)建注冊(cè)的 request [commit]
最后在添加 Controller Contract Service 中的方法 [commit]