Khi xây dựng API, bạn có thể cần một class chuyển đổi nằm giữa các model Eloquent và các response JSON được trả về cho người dùng application của bạn. Ví dụ: bạn có thể muốn hiển thị các thuộc tính nhất định cho một nhóm người dùng chứ không phải tất cả những người khác hoặc bạn có thể muốn luôn chứa các quan hệ nhất định trong định dạng JSON của các model của bạn. Các class resource của Eloquent cho phép bạn chuyển đổi một cách rõ ràng và dễ hiểu các model cũng như các collection model của bạn thành JSON.
Tất nhiên, bạn cũng có thể chuyển đổi các model hoặc collection Eloquent thành JSON bằng các phương thức toJson của chúng; tuy nhiên, các resource của Eloquent sẽ cung cấp nhiều khả năng kiểm soát mạnh mẽ và chi tiết hơn đối với quá trình chuyển hóa JSON của các model của bạn và các quan hệ của chúng.
Để tạo một class resource, bạn có thể sử dụng lệnh Artisan make:resource. Mặc định, resource sẽ được lưu vào trong thư mục app/Http/Resources của application của bạn. Các resource sẽ được extend từ class Illuminate\Http\Resources\Json\JsonResource:
php artisan make:resource UserResource
Ngoài việc tạo các resource dùng để chuyển đổi cho các model riêng biệt, bạn cũng có thể tạo các resource để chuyển đổi một collection của model. Điều này cho phép JSON response của bạn có thể thêm các liên kết hoặc các thông tin khác có liên quan đến toàn bộ collection của một resource.
Để tạo một resource collection, bạn hãy sử dụng cờ --collection khi tạo resource. Hoặc có từ Collection trong tên của resource cũng cho Laravel biết rằng nó cần tạo ra một resource collection. Resource collection được extend từ class Illuminate\Http\Resources\Json\ResourceCollection:
php artisan make:resource User --collection
php artisan make:resource UserCollection
[!NOTE] Đây là tổng quan về resource và resource collection. Bạn được khuyến khích đọc các phần khác của tài liệu này để hiểu sâu hơn về khả năng tùy biến và sức mạnh của các resource có thể cung cấp cho bạn.
Trước khi đi sâu vào tất cả các tùy chọn có sẵn cho bạn khi bạn viết resource, trước tiên chúng ta hãy xem về cách sử dụng resource trong Laravel. Một class resource sẽ đại diện cho một model cần chuyển đổi thành dạng JSON. Ví dụ, đây là một resource class UserResource đơn giản:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
Mọi class resource đều định nghĩa một phương thức toArray trả về mảng các thuộc tính sẽ được chuyển đổi thành JSON trước khi resource đó được trả về dưới dạng response từ một route hoặc một phương thức trong controller.
Lưu ý rằng chúng ta có thể truy cập vào các thuộc tính của model trực tiếp từ biến $this. Điều này là do một resource class sẽ tự động chuyển các thuộc tính và các phương thức xuống model để dễ dàng truy cập thuận tiện hơn. Sau khi resource đã được định nghĩa xong, nó có thể được trả về từ một route hoặc controller. Resource chấp nhận instance model thông qua hàm khởi tạo của nó:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user/{id}', function (string $id) {
return new UserResource(User::findOrFail($id));
});
Để thuận tiện, bạn có thể sử dụng phương thức toResource của model, phương thức này sẽ sử dụng các quy ước của framework để tự động tìm ra resource tương ứng của model đó:
return User::findOrFail($id)->toResource();
Khi gọi phương thức toResource, Laravel sẽ cố gắng tìm một resource giống với tên của model và có thể có hậu tố Resource nằm trong namespace Http\Resources gần nhất với namespace của model.
Nếu resource class của bạn không tuân theo quy ước đặt tên này hoặc nằm trong một namespace khác, bạn có thể chỉ định resource mặc định cho model bằng cách sử dụng attribute UseResource:
<?php
namespace App\Models;
use App\Http\Resources\CustomUserResource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResource;
#[UseResource(CustomUserResource::class)]
class User extends Model
{
// ...
}
Ngoài ra, bạn có thể chỉ định resource class bằng cách truyền nó vào phương thức toResource:
return User::findOrFail($id)->toResource(CustomUserResource::class);
Nếu bạn đang trả về một resource collection hoặc một response đang được phân trang, bạn nên sử dụng phương thức collection được cung cấp bởi resource class khi tạo instance resource trong route hoặc controller của bạn:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/users', function () {
return UserResource::collection(User::all());
});
Hoặc, để thuận tiện, bạn có thể sử dụng phương thức toResourceCollection của collection Eloquent, phương thức này sẽ sử dụng các quy ước của framework để tự động tìm ra resource collection tương ứng của model đó:
return User::all()->toResourceCollection();
Khi gọi phương thức toResourceCollection, Laravel sẽ cố gắng tìm kiếm một resource collection giống với tên của model và có hậu tố Collection bên trong namespace Http\Resources gần nhất với namespace của model.
Nếu resource collection class của bạn không tuân theo quy ước đặt tên này hoặc nằm trong một namespace khác, bạn có thể chỉ định resource collection mặc định cho model bằng cách sử dụng attribute UseResourceCollection:
<?php
namespace App\Models;
use App\Http\Resources\CustomUserCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResourceCollection;
#[UseResourceCollection(CustomUserCollection::class)]
class User extends Model
{
// ...
}
Ngoài ra, bạn có thể chỉ định resource collection class bằng cách truyền nó vào phương thức toResourceCollection:
return User::all()->toResourceCollection(CustomUserCollection::class);
Mặc định, resource collections sẽ không cho phép bạn thêm bất kỳ dữ liệu meta tùy chỉnh nào để có thể được trả về cùng với collection của bạn. Nếu bạn muốn tùy chỉnh response của resource collection, bạn có thể tạo một resource chuyên dụng để tạo collection:
php artisan make:resource UserCollection
Khi class resource collection đã được tạo, bạn có thể dễ dàng định nghĩa bất kỳ dữ liệu meta nào cần có trong response:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
}
Sau khi định nghĩa xong resource collection của bạn, nó có thể được trả về từ một route hoặc một controller:
use App\Http\Resources\UserCollection;
use App\Models\User;
Route::get('/users', function () {
return new UserCollection(User::all());
});
Hoặc, để thuận tiện, bạn có thể sử dụng phương thức toResourceCollection của collection Eloquent, phương thức này sẽ sử dụng các quy ước của framework để tự động tìm ra resource collection tương ứng của model đó:
return User::all()->toResourceCollection();
Khi gọi phương thức toResourceCollection, Laravel sẽ cố gắng tìm một resource collection giống với tên của model và có hậu tố là Collection bên trong namespace Http\Resources gần nhất với namespace của model đó.
Khi trả về một resource collection từ một route, Laravel sẽ reset lại các khóa của collection để chúng có thứ tự sắp xếp từ 0. Tuy nhiên, bạn có thể dùng thuộc tính PreserveKeys trên class resource của bạn để cho biết liệu khóa collection có được giữ nguyên hay không:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Attributes\PreserveKeys;
use Illuminate\Http\Resources\Json\JsonResource;
#[PreserveKeys]
class UserResource extends JsonResource
{
// ...
}
Khi thuộc tính preserveKeys được set thành true, các khóa của collection sẽ được giữ nguyên khi collection được trả về từ mmột route hoặc một controller:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/users', function () {
return UserResource::collection(User::all()->keyBy->id);
});
Thông thường, thuộc tính $this->collection của một resource collection sẽ được tự động nối với kết quả của việc ánh xạ của từng item của collection với class resource của nó. Class resource được giả định là tên class của collection mà không có chuỗi Collection ở cuối tên class. Ngoài ra, tùy thuộc vào sở thích cá nhân của bạn, resource class có thể có hoặc không có hậu tố Resource.
Ví dụ: UserCollection sẽ thử ánh xạ các instance user vào một resource có thể UserResource. Để tùy biến hành động này, bạn có thể dùng thuộc tính Collects trên resource collection của bạn:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Attributes\Collects;
use Illuminate\Http\Resources\Json\ResourceCollection;
#[Collects(Member::class)]
class UserCollection extends ResourceCollection
{
// ...
}
[!NOTE] Nếu bạn chưa đọc phần khái niệm tổng quan, bạn được khuyến khích đọc nó trước khi tiếp tục với phần này.
Resource chỉ cần chuyển đổi một model thành một mảng. Vì vậy, mỗi resource chứa một phương thức toArray để giúp chuyển các thuộc tính của model của bạn thành một mảng thân thiện với API để có thể được trả về từ các route hoặc controller của ứng dụng của bạn:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
Khi một resource đã được định nghĩa xong, nó có thể được trả về trực tiếp từ một route hoặc một controller:
use App\Models\User;
Route::get('/user/{id}', function (string $id) {
return new UserResource(User::findOrFail($id));
});
Nếu bạn muốn thêm các quan hệ vào trong một response của bạn, bạn có thể thêm chúng vào mảng được trả về trong phương thức toArray của resource của bạn. Trong ví dụ này, chúng ra sẽ sử dụng phương thức collection của resource PostResource để thêm các post trên blog của người dùng vào response của resource:
use App\Http\Resources\PostResource;
use Illuminate\Http\Request;
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'posts' => PostResource::collection($this->posts),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
[!NOTE] Nếu bạn chỉ thêm các quan hệ chỉ khi chúng đã được load, hãy xem tài liệu về điều kiện cho quan hệ.
Trong khi các resource sẽ chuyển một model thành một mảng, thì các resource collection sẽ chuyển một collection của model thành một mảng. Tuy nhiên, không nhất thiết phải định nghĩa một class resource collection cho từng loại model của bạn vì tất cả các collection eloquent model đều được cung cấp một phương thức toResourceCollection để tạo các resource collection "ad-hoc" một cách nhanh chóng:
use App\Models\User;
Route::get('/users', function () {
return User::all()->toResourceCollection();
});
Tuy nhiên, nếu bạn cần tùy chỉnh dữ liệu meta được trả về cùng với collection, bạn sẽ cần phải định nghĩa riêng một resource collection của chính bạn:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
}
Giống như resource, resource collection có thể được trả về trực tiếp từ các route hoặc controller:
use App\Http\Resources\UserCollection;
use App\Models\User;
Route::get('/users', function () {
return new UserCollection(User::all());
});
Hoặc, để thuận tiện, bạn có thể sử dụng phương thức toResourceCollection của collection Eloquent, phương thức này sẽ sử dụng các quy ước của framework để tự động tìm ra resource collection tương ứng của model đó:
return User::all()->toResourceCollection();
Khi gọi phương thức toResourceCollection, Laravel sẽ cố gắng tìm một resource collection giống với tên của model và có hậu tố là Collection trong namespace Http\Resources gần nhất với namespace của model đó.
Mặc định, resource ngoài cùng của bạn sẽ được bao bọc bởi một key data khi chúng được chuyển đổi thành JSON. Vì thế, một response resource collection có thể trông như sau:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
]
}
Nếu bạn muốn vô hiệu hóa việc bao bọc resource này, bạn nên gọi phương thức withoutWrapping trên class Illuminate\Http\Resources\Json\JsonResource. Thông thường, bạn nên gọi phương thức này từ AppServiceProvider hoặc từ một service provider khác để được load cho mọi request trong application của bạn:
<?php
namespace App\Providers;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// ...
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
JsonResource::withoutWrapping();
}
}
[!WARNING] Phương thức
withoutWrappingchỉ ảnh hưởng đến response ở ngoài cùng và sẽ không xóa các keydatamà bạn đã thêm vào bên trong resource collection.
Bạn có toàn quyền tự do định nghĩa các quan hệ của resource của bạn được bao bọc. Nếu bạn muốn tất cả các resource collection được bao bọc bởi một key data, kể cả việc chúng lồng nhau, bạn nên định nghĩa một class resource collection cho mỗi resource và trả về collection đó trong một key data.
Bạn có thể tự hỏi liệu rằng điều này có khiến resource ngoài cùng của bạn có bị bao bọc trong hai key data. Đừng lo lắng, Laravel sẽ không bao giờ để resource của bạn vô tình bị bao bọc lặp lại như vậy, vì vậy bạn không phải lo lắng về mức độ lồng nhau của resource collection mà bạn đang chuyển đổi:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CommentsCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return ['data' => $this->collection];
}
}
Khi trả về một collection được phân trang thông qua một response resource, Laravel sẽ bao bọc dữ liệu resource của bạn trong một key data ngay cả khi phương thức withoutWrapping đã được gọi. Điều này là do trong response được phân trang luôn chứa các key meta và links cùng với các thông tin về trạng thái của phân trang:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
],
"links":{
"first": "http://example.com/users?page=1",
"last": "http://example.com/users?page=1",
"prev": null,
"next": null
},
"meta":{
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://example.com/users",
"per_page": 15,
"to": 10,
"total": 10
}
}
Bạn có thể truyền một instance phân trang của Laravel cho phương thức collection của một resource hoặc một resource collection tùy biến:
use App\Http\Resources\UserCollection;
use App\Models\User;
Route::get('/users', function () {
return new UserCollection(User::paginate());
});
Hoặc, để thuận tiện, bạn có thể sử dụng phương thức toResourceCollection của paginator, phương thức này sẽ sử dụng các quy ước của framework để tự động tìm kiếm resource collection tương ứng của model được phân trang:
return User::paginate()->toResourceCollection();
Các response được phân trang luôn chứa các key meta và links cùng với các thông tin về trạng thái của phân trang:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
],
"links":{
"first": "http://example.com/users?page=1",
"last": "http://example.com/users?page=1",
"prev": null,
"next": null
},
"meta":{
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://example.com/users",
"per_page": 15,
"to": 10,
"total": 10
}
}
Nếu bạn muốn tùy chỉnh thông tin được chứa trong các key links hoặc meta của response phân trang, bạn có thể định nghĩa phương thức paginationInformation trên các resource. Phương thức này sẽ nhận vào dữ liệu $paginated và một mảng thông tin $default có chứa các key links và meta:
/**
* Customize the pagination information for the resource.
*
* @param \Illuminate\Http\Request $request
* @param array $paginated
* @param array $default
* @return array
*/
public function paginationInformation($request, $paginated, $default)
{
$default['links']['custom'] = 'https://example.com';
return $default;
}
Đôi khi bạn có thể chỉ muốn thêm một số thuộc tính vào trong một response resource nếu một điều kiện được đáp ứng. Ví dụ: bạn có thể chỉ muốn thêm một giá trị nếu người dùng hiện tại đang là "quản trị viên". Laravel cung cấp nhiều phương thức helper để hỗ trợ cho bạn trong những tình huống này. Phương thức when có thể được sử dụng để thêm một điều kiện cho một thuộc tính vào response resource:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'secret' => $this->when($request->user()->isAdmin(), 'secret-value'),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
Trong ví dụ này, khóa secret sẽ chỉ được trả về trong response resource nếu phương thức $this->isAdmin() của người dùng hiện tại trả về giá trị true. Nếu phương thức trả về giá trị false, thì khóa secret sẽ bị xóa khỏi response resource trước khi nó được gửi về cho client. Phương thức when cho phép bạn định nghĩa một resource mà không cần dùng đến các câu lệnh có điều kiện khi xây dựng một mảng.
Phương thức when cũng chấp nhận một closure là tham số thứ hai của nó, cho phép bạn tính toán giá trị trả về nếu điều kiện đã cho là true:
'secret' => $this->when($request->user()->isAdmin(), function () {
return 'secret-value';
}),
Phương thức whenHas có thể được sử dụng để chứa một thuộc tính nếu nó thực sự có trên model:
'name' => $this->whenHas('name'),
Ngoài ra, phương thức whenNotNull có thể được sử dụng để đưa một thuộc tính vào resource response nếu thuộc tính đó không rỗng:
'name' => $this->whenNotNull($this->name),
Thỉnh thoảng bạn có thể có một số thuộc tính chỉ được đưa vào trong một response resource dựa trên cùng một điều kiện nào đó. Trong trường hợp này, bạn có thể sử dụng phương thức mergeWhen để thêm các thuộc tính vào trong response chỉ khi một điều kiện là true:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
$this->mergeWhen($request->user()->isAdmin(), [
'first-secret' => 'value',
'second-secret' => 'value',
]),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
Một lần nữa, nếu điều kiện trả về giá trị là false, các thuộc tính này sẽ bị xóa ra khỏi response resource trước khi nó được gửi về client.
[!WARNING] Không nên sử dụng phương thức
mergeWhentrong các mảng mà có sử dụng cả khoá string và khóa numeric. Hơn nữa, nó cũng không nên được sử dụng trong các mảng với các khóa numeric không được sắp xếp theo tuần tự.
Ngoài các thuộc tính load có điều kiện, bạn cũng có thể thêm các điều kiện cho các quan hệ trong các response resource dựa trên việc quan hệ đó đã được load trên model hay chưa. Điều này cho phép controller của bạn quyết định xem những quan hệ nào sẽ được load trong model và resource của bạn có thể dễ dàng chứa nó chỉ khi nó đã được load. Cuối cùng, điều này giúp dễ dàng tránh được các sự cố truy vấn "N+1" trong resource của bạn.
Phương thức whenLoaded có thể được sử dụng để load một quan hệ theo điều kiện. Để tránh load các quan hệ không cần thiết, phương thức này chấp nhận tên của quan hệ thay vì chính quan hệ đó:
use App\Http\Resources\PostResource;
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'posts' => PostResource::collection($this->whenLoaded('posts')),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
Trong ví dụ này, nếu quan hệ chưa được load, thì khóa posts sẽ bị xóa bỏ ra khỏi response resource trước khi nó được gửi về client.
Ngoài điều kiện cho quan hệ, bạn có thể thêm "counts" quan hệ trên các resource response của bạn dựa trên việc count của quan hệ đó đã được load trên model hay chưa:
new UserResource($user->loadCount('posts'));
Phương thức whenCounted có thể được sử dụng để đưa count quan hệ vào resource response của bạn một cách có điều kiện. Phương thức này tránh việc chứa thuộc tính một cách không cần thiết nếu không có count quan hệ đó:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'posts_count' => $this->whenCounted('posts'),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
Trong ví dụ này, nếu count quan hệ posts chưa được load, khóa posts_count sẽ bị xóa khỏi resource response trước khi nó được gửi đến client.
Các loại tính toán khác, chẳng hạn như avg, sum, min và max cũng có thể được load có điều kiện bằng phương thức whenAggregated:
'words_avg' => $this->whenAggregated('posts', 'words', 'avg'),
'words_sum' => $this->whenAggregated('posts', 'words', 'sum'),
'words_min' => $this->whenAggregated('posts', 'words', 'min'),
'words_max' => $this->whenAggregated('posts', 'words', 'max'),
Ngoài việc thêm các thông tin quan hệ có điều kiện vào trong các response resource của bạn, bạn cũng có thể thêm các điều kiện cho dữ liệu từ các bảng trung gian của quan hệ nhiều-nhiều bằng cách sử dụng phương thức whenPivotLoaded. Phương thức whenPivotLoaded chấp nhận tên của bảng pivot làm tham số đầu tiên. Tham số thứ hai phải là một closure sẽ trả về giá trị được trả về nếu thông tin pivot đó tồn tại trên model:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'expires_at' => $this->whenPivotLoaded('role_user', function () {
return $this->pivot->expires_at;
}),
];
}
Nếu quan hệ của bạn đang sử dụng một model bảng trung gian tùy chỉnh, bạn có thể truyền một instance của model bảng trung gian làm tham số đầu tiên cho phương thức whenPivotLoaded:
'expires_at' => $this->whenPivotLoaded(new Membership, function () {
return $this->pivot->expires_at;
}),
Nếu bảng trung gian của bạn đang sử dụng một tên accessor khác không phải là pivot, bạn có thể sử dụng phương thức whenPivotLoadedAs:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () {
return $this->subscription->expires_at;
}),
];
}
Một số tiêu chuẩn API JSON sẽ yêu cầu thêm dữ liệu meta vào các response của resource và resource collection của bạn. Điều này thường chứa những thông tin như links đến resource hoặc resource quan hệ hoặc dữ liệu meta về chính resource đó. Nếu bạn cần trả về thêm dữ liệu meta cho một resource, hãy cho nó vào phương thức toArray của bạn. Ví dụ: bạn có thể chứa thông tin links khi chuyển đổi một resource collection:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
Khi trả về thêm một dữ liệu meta từ resource của bạn, bạn sẽ không phải lo lắng về việc vô tình ghi đè các key links hoặc meta được Laravel tự động thêm khi trả về các response để phân trang. Bất kỳ links nào mà bạn đã định nghĩa sẽ được merge với các link đã được cung cấp bởi paginator.
Thỉnh thoảng bạn có thể chỉ muốn thêm một số dữ liệu meta nhất định vào một response resource nếu resource đó là resource ngoài cùng được trả về. Thông thường, điều này sẽ chứa những thông tin meta về toàn bộ response. Để định nghĩa những dữ liệu meta như thế này, hãy thêm một phương thức with vào trong class resource của bạn. Phương thức này sẽ trả về một mảng dữ liệu meta sẽ được chứa trong response resource chỉ khi resource đó là resource ngoài cùng được chuyển đổi:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
/**
* Get additional data that should be returned with the resource array.
*
* @return array<string, mixed>
*/
public function with(Request $request): array
{
return [
'meta' => [
'key' => 'value',
],
];
}
}
Bạn cũng có thể thêm dữ liệu khi khởi tạo một instance resource trong route hoặc controller của bạn. Phương thức additional, có sẵn trên tất cả các resource, chấp nhận một mảng dữ liệu cần được thêm vào response resource:
return User::all()
->load('roles')
->toResourceCollection()
->additional(['meta' => [
'key' => 'value',
]]);
Laravel có chứa JsonApiResource, một resource class giúp tạo ra các response tuân thủ theo định dạng JSON:API. Nó kế thừa class JsonResource base và tự động xử lý đối tượng resource, quan hệ, lọc field, thêm đối tượng liên quan, lazy attribute evaluation, và tự động set header Content-Type thành application/vnd.api+json.
[!NOTE] Các JSON:API resource của Laravel sẽ xử lý việc serialization cho các response của bạn. Nếu bạn cũng cần xử lý các tham số truy vấn JSON:API đầu vào như filter và sắp xếp, thì Laravel Query Builder của Spatie là một package tuyệt vời cho việc đó.
Để tạo một JSON:API resource, hãy sử dụng lệnh Artisan make:resource với flag --json-api:
php artisan make:resource PostResource --json-api
Class được tạo ra sẽ extend Illuminate\Http\Resources\JsonApi\JsonApiResource và chứa các thuộc tính $attributes và $relationships để bạn định nghĩa:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
class PostResource extends JsonApiResource
{
/**
* The resource's attributes.
*/
public $attributes = [
// ...
];
/**
* The resource's relationships.
*/
public $relationships = [
// ...
];
}
JSON:API resource có thể được trả về từ các route và controller giống như các resource bình thường khác:
use App\Http\Resources\PostResource;
use App\Models\Post;
Route::get('/api/posts/{post}', function (Post $post) {
return new PostResource($post);
});
Hoặc để thuận tiện, bạn có thể sử dụng phương thức toResource của model:
Route::get('/api/posts/{post}', function (Post $post) {
return $post->toResource();
});
Điều này sẽ tạo ra một response tuân thủ theo JSON:API:
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World",
"body": "This is my first post."
}
}
}
Để trả về một collection các JSON:API resource, hãy sử dụng phương thức collection hoặc phương thức toResourceCollection:
return PostResource::collection(Post::all());
return Post::all()->toResourceCollection();
Có hai cách để định nghĩa các thuộc tính nào sẽ được đưa vào JSON:API resource của bạn.
Cách đơn giản nhất là định nghĩa vào thuộc tính $attributes ở trong resource của bạn. Bạn có thể liệt kê các tên thuộc tính dưới dạng các giá trị, chúng sẽ được đọc trực tiếp từ model tương ứng:
public $attributes = [
'title',
'body',
'created_at',
];
Nếu một thuộc tính tiêu tốn nhiều tài nguyên để tính toán, bạn có thể trả về nó từ toAttributes dưới dạng một closure để nó chỉ được tính toán khi thuộc tính đó thực sự cần thiết trong response.
Hoặc để toàn quyền kiểm soát các thuộc tính của resource, bạn có thể ghi đè phương thức toAttributes trong resource:
/**
* Get the resource's attributes.
*
* @return array<string, mixed>
*/
public function toAttributes(Request $request): array
{
return [
'title' => $this->title,
'body' => $this->body,
'is_published' => fn () => $this->published_at !== null,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
JSON:API resource hỗ trợ định nghĩa các quan hệ tuân theo định dạng JSON:API. Các quan hệ chỉ được serialize khi client yêu cầu thông qua tham số truy vấn include.
$relationships PropertyBạn có thể định nghĩa các quan hệ có thể được thêm vào resource thông qua thuộc tính $relationships có trong resource:
public $relationships = [
'author',
'comments',
];
Khi liệt kê tên một quan hệ dưới dạng giá trị, Laravel sẽ resolve quan hệ Eloquent tương ứng và tự động tìm class resource phù hợp. Nếu bạn cần chỉ định rõ class resource nào sẽ được thêm vào, thì bạn có thể định nghĩa quan hệ đó dưới dạng cặp key và class:
use App\Http\Resources\UserResource;
public $relationships = [
'author' => UserResource::class,
'comments',
];
Ngoài ra, bạn cũng có thể ghi đè phương thức toRelationships trong resource:
/**
* Get the resource's relationships.
*/
public function toRelationships(Request $request): array
{
return [
'author' => UserResource::class,
'comments' => fn () => CommentResource::collection(
$request->user()->is($this->resource)
? $this->comments
: $this->comments->where('is_public', true),
),
];
}
Sử dụng closure cung cấp cho bạn nhiều quyền kiểm soát hơn đối với payload của quan hệ, trong khi vẫn chỉ resolve quan hệ khi client yêu cầu.
Client có thể yêu cầu các resource liên quan bằng cách sử dụng tham số truy vấn include:
GET /api/posts/1?include=author,comments
Điều này tạo ra một response gồm các id đối tượng resource ở trong key relationships và các đối tượng resource thật ở trong mảng included cao hơn:
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World"
},
"relationships": {
"author": {
"data": {
"id": "1",
"type": "users"
}
},
"comments": {
"data": [
{
"id": "1",
"type": "comments"
}
]
}
}
},
"included": [
{
"id": "1",
"type": "users",
"attributes": {
"name": "Taylor Otwell"
}
},
{
"id": "1",
"type": "comments",
"attributes": {
"body": "Great post!"
}
}
]
}
Các quan hệ lồng nhau có thể được thêm vào bằng cách sử dụng cú pháp dấu chấm:
GET /api/posts/1?include=comments.author
Mặc định, các quan hệ lồng nhau được thêm vào sẽ bị giới hạn ở một độ sâu tối đa. Bạn có thể tùy chỉnh giới hạn này bằng phương thức maxRelationshipDepth, thường được set trong một service provider của ứng dụng:
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
JsonApiResource::maxRelationshipDepth(3);
Mặc định, type của resource sẽ được lấy từ tên class resource. Ví dụ, PostResource sẽ tạo ra type là posts và BlogPostResource sẽ tạo ra là blog-posts. Và id của resource sẽ được lấy từ khóa chính của model.
Nếu bạn cần tùy chỉnh các giá trị này, bạn có thể ghi đè các phương thức toType và toId trong resource của bạn:
/**
* Get the resource's type.
*/
public function toType(Request $request): string
{
return 'articles';
}
/**
* Get the resource's ID.
*/
public function toId(Request $request): string
{
return (string) $this->uuid;
}
Điều này đặc biệt hữu ích khi type của resource khác với tên class của nó, chẳng hạn khi một AuthorResource chứa một model User và nên output ra type là authors.
JSON:API resource có hỗ trợ lọc field, cho phép client chỉ yêu cầu các thuộc tính cụ thể cho từng loại resource thay vì lấy ra tất cả, bạn có thể thực hiện điều này bằng cách sử dụng tham số truy vấn fields:
GET /api/posts?fields[posts]=title,created_at&fields[users]=name
Điều này sẽ chỉ lấy ra các thuộc tính title và created_at cho các resource posts, và thuộc tính name cho các resource users.
Nếu bạn muốn tắt tính năng lọc field cho một resource response nhất định, bạn có thể gọi phương thức ignoreFieldsAndIncludesInQueryString:
return $post->toResource()
->ignoreFieldsAndIncludesInQueryString();
Mặc định, các quan hệ chỉ được đưa vào response khi được yêu cầu qua tham số truy vấn include. Nếu bạn muốn đưa vào tất cả các quan hệ đã được eager-load trước đó bất kể query string như thế nào, bạn có thể gọi phương thức includePreviouslyLoadedRelationships:
return $post->load('author', 'comments')
->toResource()
->includePreviouslyLoadedRelationships();
Bạn có thể thêm các link và thông tin meta vào các đối tượng JSON:API resource bằng cách ghi đè các phương thức toLinks và toMeta trong resource:
/**
* Get the resource's links.
*/
public function toLinks(Request $request): array
{
return [
'self' => route('api.posts.show', $this->resource),
];
}
/**
* Get the resource's meta information.
*/
public function toMeta(Request $request): array
{
return [
'readable_created_at' => $this->created_at->diffForHumans(),
];
}
Điều này sẽ thêm các key gồm có links và meta vào đối tượng resource trong response:
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World"
},
"links": {
"self": "https://example.com/api/posts/1"
},
"meta": {
"readable_created_at": "2 hours ago"
}
}
}
Như bạn đã đọc, resources có thể được trả về trực tiếp từ một route hoặc một controller:
use App\Models\User;
Route::get('/user/{id}', function (string $id) {
return User::findOrFail($id)->toResource();
});
Tuy nhiên, thỉnh thoảng bạn có thể cần tùy biến HTTP response trước khi nó được gửi về client. Có hai cách để thực hiện điều này. Đầu tiên, bạn có thể gắn thêm phương thức response vào trong resource. Phương thức này sẽ trả về một instance Illuminate\Http\JsonResponse, cho phép bạn toàn quyền kiểm soát các header của response:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user', function () {
return User::find(1)
->toResource()
->response()
->header('X-Value', 'True');
});
Ngoài ra, bạn cũng có thể định nghĩa một phương thức withResponse vào trong chính resource của bạn. Phương thức này sẽ được gọi khi resource được trả về là resource ngoài cùng nhất trong một response:
<?php
namespace App\Http\Resources;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
];
}
/**
* Customize the outgoing response for the resource.
*/
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('X-Value', 'True');
}
}
entry