Bạn đã sử dụng đúng API Resources trong Laravel ?

Trong Laravel, API Resources giúp bạn cần chuyển đổi dữ liệu Eloquent model và JSON được trả về bởi ứng dụng của người dùng ( thêm, thay đổi giá trị hay loại bỏ thuộc tính của model ) khi dựng một API.
Phạm vi bài viết:
Tập trung làm rõ : " Các lỗi thường gặp phải và cách tối ưu code " ~ cũng là vấn đề mình gặp phải trong quá trình làm dự án công ty. Bởi vậy, trong bài viết mình xin loại bỏ các bước chuẩn bị file và dữ liệu nhé
Đơn giản, mình sẽ một loạt các yêu cầu bài toán với đầu vào dữ liệu được lấy từ 2 bảng Songs (id, title, artist, rating, album_id ) và Album (name, user) ( n-1 ).

1. Lỗi thường gặp

Lẫn lộn giữa Resouce và ResouceCollection

Yêu cầu bài toán: mong muốn lấy ra danh sách bài hát thông qua API và các thông tin tương ứng cần có

# routes/api.php
Route::get('/songs', function() {
    return new SongResource(Song::all());
});
class SongsResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'artist' => $this->artist
        ];
    }
}

Kết quả: Lỗi thông báo Property [id] does not exist on this collection instance. xuất hiện vì SongsResource chỉ cho phép nhận một giá trị đơn lẻ chứ không phải một tập giá trị collection =)).
Vậy đến đây bạn sẽ giải quyết như thế nào ?
Mình có đưa ra 2 giải pháp tùy thuộc vào dữ liệu mong muốn cần hiện thị

  • Chỉ hiện có các trường đã được định nghĩa trong SongResource:
    Đơn giản hãy chuyển !
# routes/api.php
// Lấy danh sách các bài hát 
Route::get('/songs', function() {
    return SongsResource::collection(Songs::all());
});
  • Thêm trường mới vào dữ liệu trả về
    Hãy sử dụng ResouceCollection để giải quyết nó !.
class SongsCollection extends ResourceCollection
{
    public function toArray($request)
    {
        return [
            'data' => SongsResource::collection($this->collection),
            'meta' => [
                'count' => $this->collection->count(),
                'link' => 'value-link',
            ],
        ];
    }
}
Route::get('/songs', function() {
    return new SongsCollection(Songs::all());
});

Kết quả: trường thêm mới countlink được thêm mới vào và với việc sử dụng SongsResource::collection($this->collection) bạn có thể tùy chỉnh các trường dữ liệu trả về từ SongsResource.

2. Tối ưu code

  • Giải quyết vấn đề n+1 query

Yêu cầu bài toán: thêm 1 trường mới album vào SongsResource để miêu tả thêm thông tin bài hát

class SongsResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            // new field which don't belongs to songs table
            'album' => new AlbumResource($this->album),
            // List of fields in songs table
            'title' => $this->title,
            'artist' => $this->artist
        ];
    }
}

Cách giải quyết thông thường:

Route::get('/songs', function() {
    return new SongsCollection(Songs::all());
});
class SongsCollection extends ResourceCollection
{
    public function toArray($request)
    {
        return [
            'data' => SongsResource::collection($this->collection),
            'meta' => [
                'count' => $this->collection->count(),
            ],
        ];
    }
}
class AlbumResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'name' => $this->name,
        ];
    }
}

Kết quả: N + 1 query xuất hiện

string(21) "select * from `songs`"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
{"data":[{"album":{"name":"Pray how."},"title":"Cat, 'if.","artist":"Cheshire."}],"meta":{"count":20}}

Chú ý: thêm đoạn mã nào vào routes\api.php để debug

\DB::listen(function($query) {
    var_dump($query->sql);
});

Vậy đến đây bạn sẽ giải quyết bằng cách nào ?
Với mình thì chỉ cần chỉ cần thêm Songs::with('album')->get() là xong ~

Route::get('/songs', function() {
    return new SongsCollection(Songs::with('album')->get());
});

Kết quả:

string(21) "select * from `songs`"
string(75) "select * from `album` where `album`.`id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"
{"data":[{"album":{"name":"Pray how."},"title":"Cat, 'if.","artist":"Cheshire."}],"meta":{"count":20}}
  • Code ngắn gọn

Yêu cầu bài toán: Thêm 2 trường mới songsupdated_at miêu tả vào rõ hơn về ngày tạo album và danh sách bài hát trong đó AlbumResource

class AlbumResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'name' => $this->name
    }
}
class SongsResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'title' => $this->title,
            'artist' => $this->artist
        ];
    }
}

Route::get('/songs', function() {
    return new SongsCollection(Songs::with('album')->get());
});

Cách giải quyết thông thường:

class AlbumResource extends JsonResource
{
    public function toArray($request)
    {
        $songs = collect($this->songs)->map(function ($item){
            return [$item->title,$item->artist];
        });
        $result = [
            'name' => $this->name,
            // other attributes
            'songs' => $songs,
        ];

        if($this->songs->count() > 3) {
            $result['updated_at'] = '02082021';
        }
        return $result;
    }
}

Hãy sử dụng whenLoadedmergeWhen để tối ưu code trong trường hợp này -> loại bỏ bớt if else và logic xử lý ngoài lề.
Chú ý: whenLoaded phải đi kèm với đầu vào là Songs::with('album')->get() là một Eager Loading.

class AlbumResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'name' => $this->name,
            // other attributes
            'songs' => SongsResource::collection($this->whenLoaded('songs')),
            $this->mergeWhen($this->songs->count() > 3

                , ['updated_at' => '02082021'])
        ];
    }
}

Rất mong bài viết có thể giúp ích cho mọi người và nhận được sự đóng góp từ mọi người !