Cách tạo tài nguyên API hùng hồn cho Laravel để chuyển đổi mô hình thành JSON
Khi tạo API, ta thường cần làm việc với các kết quả database để lọc, diễn giải hoặc định dạng các giá trị sẽ được trả về trong phản hồi API. Các lớp tài nguyên API cho phép bạn chuyển đổi các mô hình và bộ sưu tập mô hình của bạn thành JSON, hoạt động như một lớp chuyển đổi dữ liệu giữa database và bộ điều khiển.Tài nguyên API cung cấp một giao diện thống nhất được dùng ở mọi nơi trong ứng dụng. Các mối quan hệ hùng hồn cũng được quan tâm.
Laravel cung cấp hai lệnh artisan
để tạo tài nguyên và bộ sưu tập - ta sẽ hiểu sự khác biệt giữa hai lệnh này sau. Nhưng đối với cả tài nguyên và bộ sưu tập, ta có phản hồi được bao bọc trong một thuộc tính dữ liệu: tiêu chuẩn phản hồi JSON.
Ta sẽ xem xét cách làm việc với các tài nguyên API trong phần tiếp theo bằng cách thử với một dự án demo.
Yêu cầu :
Để làm theo hướng dẫn này, bạn cần đáp ứng các yêu cầu sau:
- Một môi trường phát triển Laravel hoạt động. Để cài đặt điều này, bạn có thể làm theo hướng dẫn của ta về Cách cài đặt và cấu hình ứng dụng Laravel trên Ubuntu 18.04 .
Lấy mã Demo
Sao chép repo này và làm theo hướng dẫn trong README.md
để bắt đầu và chạy mọi thứ.
Với cài đặt dự án, bây giờ ta có thể bắt đầu làm bẩn tay. Ngoài ra, vì đây là một dự án rất nhỏ, ta sẽ không tạo bất kỳ bộ điều khiển nào và thay vào đó sẽ kiểm tra các phản hồi bên trong các tuyến đóng.
Hãy bắt đầu bằng cách tạo một lớp SongResource
:
- php artisan make:resource SongResource
Nếu ta nhìn vào bên trong file tài nguyên mới được tạo, tức là SongResource (Các file tài nguyên thường nằm bên trong folder App\Http\Resources
), nội dung sẽ giống như sau:
[...] 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); } }
Theo mặc định, ta có parent::toArray($request)
bên trong phương thức toArray()
. Nếu ta để mọi thứ ở mức này, tất cả các thuộc tính của mô hình hiển thị sẽ là một phần trong phản hồi của ta . Để điều chỉnh phản hồi, ta chỉ định các thuộc tính mà ta muốn được chuyển đổi thành JSON bên trong phương thức toArray()
này.
Hãy cập nhật phương thức toArray()
để trùng với đoạn mã bên dưới:
public function toArray($request) { return [ 'id' => $this->id, 'title' => $this->title, 'rating' => $this->rating, ]; }
Như bạn thấy, ta có thể truy cập các thuộc tính của mô hình trực tiếp từ biến $this
vì một lớp tài nguyên tự động cho phép phương thức truy cập vào mô hình bên dưới.
Bây giờ hãy cập nhật các routes/api.php
bằng đoạn mã dưới đây:
# routes/api.php [...] use App\Http\Resources\SongResource; use App\Song; [...] Route::get('/songs/{song}', function(Song $song) { return new SongResource($song); }); Route::get('/songs', function() { return new SongResource(Song::all()); });
Nếu ta truy cập URL /api/songs/1
, ta sẽ thấy phản hồi JSON chứa các cặp key-value mà ta đã chỉ định trong lớp SongResource
cho bài hát có id là 1
:
{ data: { id: 1, title: "Mouse.", rating: 3 } }
Tuy nhiên, nếu ta thử truy cập URL /api/songs
, thì một Ngoại lệ được ném Property [id] does not exist on this collection instance.
Điều này là do việc khởi tạo lớp SongResource yêu cầu một cá thể tài nguyên được chuyển cho hàm tạo chứ không phải một bộ sưu tập. Đó là lý do tại sao ngoại lệ được ném.
Nếu ta muốn một tập hợp được trả về thay vì một tài nguyên đơn lẻ, thì có một phương thức static collection()
có thể được gọi trên một lớp Resource truyền vào một tập hợp làm đối số. Hãy cập nhật kết thúc lộ trình bài hát của ta :
Route::get('/songs', function() { return SongResource::collection(Song::all()); });
Truy cập lại URL /api/songs
sẽ cung cấp cho ta phản hồi JSON chứa tất cả các bài hát.
{ data: [{ id: 1, title: "Mouse.", rating: 3 }, { id: 2, title: "I'll.", rating: 0 } ] }
Tài nguyên hoạt động tốt khi trả về một tài nguyên đơn lẻ hoặc thậm chí là một bộ sưu tập nhưng có những hạn chế nếu ta muốn đưa metadata vào phản hồi. Đó là nơi Collections
đến để giải cứu ta .
Để tạo một lớp tập hợp, ta chạy:
php artisan make:resource SongsCollection
Sự khác biệt chính giữa tài nguyên JSON và tập hợp JSON là tài nguyên mở rộng lớp JsonResource
và mong đợi một tài nguyên duy nhất được chuyển khi được khởi tạo trong khi một tập hợp mở rộng lớp ResourceCollection
và mong đợi một tập hợp là đối số khi được khởi tạo.
Quay lại bit metadata . Giả sử ta muốn một số metadata như tổng số bài hát là một phần của phản hồi, đây là cách thực hiện khi làm việc với lớp ResourceCollection
:
class SongsCollection extends ResourceCollection { public function toArray($request) { return [ 'data' => $this->collection, 'meta' => ['song_count' => $this->collection->count()], ]; } }
Nếu ta cập nhật việc đóng tuyến đường /api/songs
của ta tới đây:
[...] use App\Http\Resources\SongsCollection; [...] Route::get('/songs', function() { return new \SongsCollection(Song::all()); });
Và truy cập URL /api/songs
, bây giờ ta thấy tất cả các bài hát bên trong thuộc tính dữ liệu cũng như tổng số bên trong bit meta:
{ data: [{ id: 1, title: "Mouse.", artist: "Carlos Streich", rating: 3, created_at: "2018-09-13 15:43:42", updated_at: "2018-09-13 15:43:42" }, { id: 2, title: "I'll.", artist: "Kelton Nikolaus", rating: 0, created_at: "2018-09-13 15:43:42", updated_at: "2018-09-13 15:43:42" }, { id: 3, title: "Gryphon.", artist: "Tristin Veum", rating: 3, created_at: "2018-09-13 15:43:42", updated_at: "2018-09-13 15:43:42" } ], meta: { song_count: 3 } }
Nhưng ta gặp sự cố, mỗi bài hát bên trong thuộc tính dữ liệu không được định dạng theo đặc điểm kỹ thuật mà ta đã xác định trước đó bên trong SongResource và thay vào đó có tất cả các thuộc tính.
Để khắc phục điều này, bên trong phương thức toArray()
, hãy đặt giá trị của data
thành SongResource::collection($this->collection)
thay vì có $this->collection
.
Phương thức toArray()
của ta bây giờ sẽ giống như sau:
public function toArray($request) { return [ 'data' => SongResource::collection($this->collection), 'meta' => ['song_count' => $this->collection->count()] ]; }
Bạn có thể xác minh ta nhận được dữ liệu chính xác trong phản hồi bằng cách truy cập lại vào URL /api/songs
.
Điều gì sẽ xảy ra nếu một người muốn thêm metadata vào một tài nguyên duy nhất chứ không phải một bộ sưu tập? May mắn là lớp JsonResource
đi kèm với một phương thức additional()
cho phép bạn chỉ định bất kỳ dữ liệu bổ sung nào mà bạn muốn trở thành một phần của phản hồi khi làm việc với một tài nguyên:
Route::get('/songs/{song}', function(Song $song) { return (new SongResource(Song::find(1)))->additional([ 'meta' => [ 'anything' => 'Some Value' ] ]); })
Trong trường hợp này, phản hồi sẽ giống như sau:
{ data: { id: 1, title: "Mouse.", rating: 3 }, meta: { anything: "Some Value" } }
Tạo mối quan hệ kiểu mẫu
Trong dự án này, ta chỉ có hai mô hình là Album
và Song
. Mối quan hệ hiện tại là mối quan hệ một one-to-many
, nghĩa là một album có nhiều bài hát và một bài hát thuộc một album.
Bây giờ ta sẽ cập nhật phương thức toArray()
bên trong lớp SongResource để nó tham chiếu đến album:
class SongResource extends JsonResource { public function toArray($request) { return [ // other attributes 'album' => $this->album ]; } }
Nếu ta muốn cụ thể hơn về những thuộc tính album nào sẽ có trong phản hồi, ta có thể tạo một Nguồn Album tương tự như những gì ta đã làm với các bài hát.
Để tạo AlbumResource
, hãy chạy:
- php artisan make:resource AlbumResource
Khi lớp tài nguyên đã được tạo, sau đó ta chỉ định các thuộc tính mà ta muốn đưa vào phản hồi.
class AlbumResource extends JsonResource { public function toArray($request) { return [ 'title' => $this->title ]; } }
Và bây giờ bên trong lớp SongResource
, thay vì thực hiện 'album' => $this->album
, ta có thể sử dụng lớp AlbumResource
mà ta vừa tạo.
class SongResource extends JsonResource { public function toArray($request) { return [ // other attributes 'album' => new AlbumResource($this->album) ]; } }
Nếu ta truy cập lại URL /api/songs
, bạn sẽ nhận thấy một album sẽ là một phần của phản hồi. Vấn đề duy nhất với cách tiếp cận này là nó đưa ra vấn đề truy vấn N + 1
.
Với mục đích demo , hãy thêm đoạn mã bên dưới vào bên trong file api/routes
:
# routes/api.php [...] \DB::listen(function($query) { var_dump($query->sql); });
Truy cập lại URL /api/songs
. Lưu ý đối với mỗi bài hát, ta thực hiện thêm một truy vấn để lấy thông tin chi tiết của album? Điều này có thể tránh được bằng các mối quan hệ tải nhanh. Trong trường hợp của ta , hãy cập nhật mã bên trong đóng tuyến /api/songs
thành:
return new SongsCollection(Song::with('album')->get());
Reload trang và bạn sẽ nhận thấy số lượng truy vấn đã giảm. Comment về đoạn mã \DB::listen
vì ta không cần nó nữa.
Sử dụng các điều kiện khi làm việc với các tài nguyên
Thỉnh thoảng, ta có thể có một điều kiện xác định loại phản hồi sẽ được trả về.
Một cách tiếp cận mà ta có thể thực hiện là giới thiệu các câu lệnh if bên trong phương thức toArray()
của ta . Tin tốt là ta không cần phải làm điều đó vì có một đặc điểm ConditionallyLoadsAttributes
yêu cầu bên trong lớp JsonResource có một số phương thức để xử lý các điều kiện. Chỉ cần đề cập đến một số ít, ta có các phương thức when()
, whenLoaded()
và mergeWhen()
.
Ta sẽ chỉ lướt qua một vài phương pháp này, nhưng tài liệu này khá toàn diện.
Phương thức whenLoaded
Phương pháp này ngăn dữ liệu chưa được tải sẵn sàng được tải khi truy xuất các mô hình liên quan do đó ngăn vấn đề truy vấn (N+1)
.
Vẫn làm việc với tài nguyên Album như một điểm tham chiếu (một album có nhiều bài hát):
public function toArray($request) { return [ // other attributes 'songs' => SongResource::collection($this->whenLoaded($this->songs)) ]; }
Trong trường hợp ta không háo hức tải các bài hát khi truy xuất album, ta sẽ kết thúc với một bộ sưu tập bài hát trống.
Phương pháp mergeWhen
Thay vì có câu lệnh if ra lệnh xem một số thuộc tính và giá trị của nó có phải là một phần của phản hồi hay không, ta có thể sử dụng phương thức mergeWhen()
trong điều kiện để đánh giá là đối số đầu tiên và một mảng chứa cặp key-value nghĩa là một phần của phản hồi nếu điều kiện đánh giá là true:
public function toArray($request) { return [ // other attributes 'songs' => SongResource::collection($this->whenLoaded($this->songs)), this->mergeWhen($this->songs->count > 10, ['new_attribute' => 'attribute value']) ]; }
Điều này trông gọn gàng và thanh lịch hơn thay vì có các câu lệnh if bao bọc toàn bộ khối trả về.
Tài nguyên API kiểm tra đơn vị
Bây giờ ta đã học cách chuyển đổi các phản hồi của bạn , làm thế nào để ta thực sự xác minh phản hồi mà ta nhận lại có phải là những gì ta đã chỉ định trong các lớp tài nguyên của bạn ?
Bây giờ ta sẽ viết các bài kiểm tra xác minh phản hồi có chứa dữ liệu chính xác cũng như đảm bảo các mối quan hệ hùng hồn vẫn được duy trì.
Hãy tạo thử nghiệm:
- php artisan make:test SongResourceTest --unit
Lưu ý cờ --unit
khi tạo bài kiểm tra: điều này sẽ cho Laravel biết rằng đây phải là bài kiểm tra đơn vị.
Hãy bắt đầu bằng cách viết bài kiểm tra đảm bảo phản hồi của ta từ lớp SongResource
chứa dữ liệu chính xác:
[...] use App\Http\Resources\SongResource; use App\Http\Resources\AlbumResource; [...] class SongResourceTest extends TestCase { use RefreshDatabase; public function testCorrectDataIsReturnedInResponse() { $resource = (new SongResource($song = factory('App\Song')->create()))->jsonSerialize(); } }
Ở đây, trước tiên ta tạo một tài nguyên bài hát, sau đó gọi jsonSerialize()
trên SongResource để chuyển đổi tài nguyên thành định dạng JSON, vì đó là những gì sẽ được gửi đến giao diện user của ta .
Và vì ta đã biết các thuộc tính bài hát nên là một phần của phản hồi, nên bây giờ ta có thể đưa ra khẳng định của bạn :
$this->assertArraySubset([ 'title' => $song->title, 'rating' => $song->rating ], $resource);
Trong ví dụ này, ta đã đối sánh hai thuộc tính: title
và rating
. Bạn có thể liệt kê bao nhiêu thuộc tính tùy thích.
Nếu bạn cần đảm bảo các mối quan hệ mô hình của bạn được duy trì ngay cả sau khi chuyển đổi mô hình thành tài nguyên, bạn có thể sử dụng:
public function testSongHasAlbumRelationship() { $resource = (new SongResource($song = factory('App\Song')->create(["album_id" => factory('App\Album')->create(['id' => 1])])))->jsonSerialize(); }
Ở đây, ta tạo một bài hát với album_id
là 1
sau đó chuyển bài hát đó vào lớp SongResource trước khi chuyển đổi tài nguyên sang định dạng JSON.
Để xác minh mối quan hệ bài hát-album vẫn được duy trì, ta đưa ra xác nhận về thuộc tính album của $resource
mà ta vừa tạo. Như vậy:
$this->assertInstanceOf(AlbumResource::class, $resource["album"]);
Tuy nhiên, lưu ý nếu ta đã thực hiện $this->assertInstanceOf(Album::class, $resource["album"])
thì thử nghiệm của ta sẽ không thành công vì ta đang chuyển thể hiện album thành tài nguyên bên trong lớp SongResource.
Tóm lại, trước tiên ta tạo một cá thể mô hình, chuyển cá thể đó cho lớp tài nguyên, chuyển đổi tài nguyên thành định dạng JSON trước khi cuối cùng đưa ra các xác nhận.
Kết luận
Ta đã xem xét các tài nguyên API Laravel là gì, cách tạo chúng cũng như cách kiểm tra các phản hồi JSON khác nhau. Vui lòng khám phá lớp JsonResource
và xem tất cả các phương thức có sẵn.
Nếu bạn muốn tìm hiểu thêm về tài nguyên API của Laravel, hãy xem tài liệu chính thức .
Các tin liên quan