最近專案遇到一個情況,我們在遇到使用者訪問某個資訊沒有許可權的時候,希望提示詳細的原因,比如當訪問一個團隊資源時非成員訪問的場景下會提示一個:您不是 [xxxxxx] 團隊的成員,暫時無法檢視,可<申請加入>
,同時需要顯示打碼後的團隊名稱,以及加入按鈕,可是介面方的邏輯是當沒有許可權時直接 abort
了:
abort_if(!$user->isMember($resouce->team), 403, '您無權訪問該資源');
得到的響應結果如下:
HTTP/1.0 403 Forbidden
{
"message": "您無權訪問該資源"
}
我們不可能將 message 用 html 來完成前端提示頁的展示,這樣耦合性太強,違背了前後端分離的原則。我們的目標是返回如下的格式即可解決:
HTTP/1.0 403 Forbidden
{
"message": "您無權訪問該資源",
"team": {
"id": "abxT8sioa0Ms",
"name": "CoDesign****"
}
}
通過攜帶上下文的方法傳遞資料,方便了前端同學自由組合。
開始改造
當然這並不是什麼複雜的事情,直接修改原來的 abort_if
即可解決:
- abort_if(!$user->isMember($resouce->team), 403, '您無權訪問該資源');
+ if (!$user->isMember($resouce->team)) {
+ return response()->json([
+ 'message' => '您無權訪問該資源',
+ 'team' => [
+ 'id' => $resouce->team_id,
+ 'name'=> $resouce->team->desensitised_name,
+ ]
+ ], 403);
+ }
這樣看起來解決了問題,可是試想一下,如果是在閉包裡面檢測到異常想要退出,上面這種 return
式的寫法就會比較難搞了,畢竟 return
只會終止最近的上下文環境,我們還是希望像 abort
一樣能終止整個應用的執行,再進行另一番改造。
優化實現
看了 abort
原始碼,我發現它的第一個引數其實支援 \Symfony\Component\HttpFoundation\Response
例項,而上面?我們 return
的結果就是它的例項,所以我們只需要改成這樣就可以了:
if (!$user->isMember($resouce->team)) {
abort(response()->json([
'message' => '您無權訪問該資源',
'team' => [
'id' => $resouce->team_id,
'name'=> $resouce->team->desensitised_name,
]
], 403));
}
看起來實現了異常中斷,可是新的問題來了,如果需要複用的時候還是比較尷尬,這段程式碼將會重複出現在各種有此許可權判斷的地方,這並不是我們想要的。
邏輯複用
為了達到邏輯複用,看了 \App\Exceptions\Handler
的實現,發現父類的 render
方法還有這麼一個設計:
public function render($request, Throwable $e)
{
if (method_exists($e, 'render') && $response = $e->render($request)) {
return Router::toResponse($request, $response);
} elseif ($e instanceof Responsable) {
return $e->toResponse($request);
}
//...
所以,我們可以將這個邏輯抽離為一個獨立的異常類,實現 render 方法即可:
我們先建立一個異常類:
$ ./artisan make:exception NotTeamMemberException
實現程式碼如下:
<?php
namespace App\Exceptions;
use App\Team;
class NotTeamMemberException extends \Exception
{
public Team $team;
public function __construct(Team $team, $message = "")
{
$this->team = $team;
parent::__construct($message, 403);
}
public function render()
{
return response()->json(
[
'message' => !empty($this->message) ? $this->message : '您無權訪問該資源',
'team' => [
'id' => $this->team->id,
'name' => $this->team->desensitised_name,
],
],
403
);
}
}
這樣一來,我們的邏輯就變成了:
if (!$user->isMember($resouce->team)) {
throw new NotTeamMemberException($resouce->team, '您無權訪問該資源');
}
當然也可以簡寫為:
\throw_if(!$user->isMember($resouce->team), NotTeamMemberException::class, $resouce->team, '您無權訪問該資源');
問題到這裡總算以一個比較完美的方式解決了,如果你有更好的方案歡迎評論探討。
本作品採用《CC 協議》,轉載必須註明作者和本文連結