Eloquent API Resources
PHP:7.2
Laravel:5.8
當我們利用 Laravel 在設計 API 時,通常都會透過 Eloquent Model
取得資料,並將資料轉換成 JSON
格式輸出。
比如在 Controller
中就會像這樣回傳:
return response()->json([
'status' => 200,
'data' => [
'meta' => SongModel::find($songID)
]
]);
然而,這裡隱藏著幾個缺點:
- 重複的格式定義: 類似的
endpoint
可能都有類似的格式定義,可是卻散落在Controller
各處。 - format 不固定: 因為是直接將
model
輸出,如果資料表有更動的話,API 會直接受到衝擊。
為了因應以上問題,Laravel 在 5.5
之後提供了 API Resources 讓大家更方便的自訂輸出格式,建立 Model
與 Output
之間的橋樑。
Basic Usage
Artisan
提供了命令列可以直接建立 Resource
。
php artisan make:resource SongResource
產生後便可以在 app\Http\Resources
看到 SongResource
檔案。
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class SongResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}
由於 Resource
是繼承自 JsonResource
,因此輸出就會是 JSON
的格式內容,而我們要做的就是修改 toArray()
method 當中的格式:
public function toArray($request)
{
return [
'song_id' => $this->song_id,
'song_name' => $this->song_name,
'artist_id' => $this->artist_id,
'album_id' => $this->album_id,
];
}
接著只要在 Route
或 Controller
中直接輸出就可以了。
Controller
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Song;
use App\Http\Resources\SongResource;
class TestController extends Controller
{
public function test()
{
$model = SongModel::find($songID);
// 直接 return Resource
return new SongResource($model);
// 也可以自行設定 HTTP code
return (new SongResource($model))
->response()
->setStatusCode(200);
}
}
Route
use App\Song;
use App\Http\Resources\SongResource;
Route::get('/songs/{song}', function(Song $song) {
return new SongResource($song);
});
Output
{
"data":{
"song_id":301231008,
"song_name":"TEST",
"artist_id":3182,
"album_id":30497178,
}
}
Collection
接下來問題來了,很多時候我們會取得資源的清單,比如說 SongModel::all()
,直接使用 Resource
會出現錯誤訊息,因為丟進去的是一個 Collection
,並不是物件本身。
此時需要搭配 Resource::collection()
使用,它會將 Collection
中的所有 Model
物件自動套用 Resource
的格式。
Controller Exmaple
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Song;
use App\Http\Resources\SongResource;
class TestController extends Controller
{
public function test()
{
$collection = SongModel::all();
return SongResource::collection($collection);
}
}
當然,也可以定義一些 Collection
的資料在 Resource
之中:
app/Http/Resources/SongResource
public function toArray($request)
{
return [
// 傳入 collection
'data' => $this->collection,
'count' => $this->collection->count()
];
}
Relationships
既然用到了 Eloquent Model
,必定會用到一些 relation
去取得不同張表的資料;對於 Resource
來說,我們必須將這些資源都一一拆開,一來具備開發彈性、二來這些資源也可重複使用。
舉例來說,如果透過 SongModel
關聯出 Album
跟 Artist
,可以設計成以下這樣:
app/Http/Resources/ArtistResource
public function toArray($request)
{
return [
'artist_id' => $this->artist_id,
'artist_name' => $this->artist_name
];
}
app/Http/Resources/AlbumResource
public function toArray($request)
{
return [
'album_id' => $this->album_id,
'album_name' => $this->album_name
];
}
app/Http/Resources/SongResource
public function toArray($request)
{
return [
'song_id' => $this->song_id,
'song_name' => $this->song_name,
'artist_id' => $this->artist_id,
'album_id' => $this->album_id,
'artist' => new Artist($this->artist),
'album' => new Album($this->album)
];
}
Controller Exmaple
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Song;
use App\Http\Resources\SongResource;
class TestController extends Controller
{
public function test()
{
$collection = SongModel::with(['album', 'artist'])->all();
return SongResource::collection($collection);
}
}