ResourceCollection — pagination wrapping, custom meta
Concept
ResourceCollection and custom pagination wrapping give you control over the entire JSON response envelope when returning collections of resources.
JsonResource::collection(): The automatic way. Creates an AnonymousResourceCollection. Automatically wraps paginator results with data, links, meta. Works for 90% of use cases.
Custom ResourceCollection: Create with php artisan make:resource UserCollection --collection. Extends ResourceCollection. Lets you customize the top-level response structure — add custom meta fields, modify data key, add response headers.
Pagination metadata: When you pass a Paginator to a collection, links and meta are automatically added to the response.
paginationInformation(): Override in your collection to customize the links and meta structure.
additional(array $data): Add extra top-level keys to any resource or collection: UserResource::collection($users)->additional(['version' => 'v1']).
withResponse(Request $request, JsonResponse $response): Override in a Resource to customize the response object — add headers, set status codes.
Wrapping: By default, JsonResource wraps output in a data key. Disable with JsonResource::withoutWrapping() (e.g., in a test or when wrapping is provided elsewhere). Don't disable globally in service providers — it affects all resources.
Code Example
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Request;
// Custom UserCollection with extra metadata
class UserCollection extends ResourceCollection
{
public string $collects = UserResource::class; // which resource to use per item
public function toArray(Request $request): array
{
return [
'data' => $this->collection, // transformed items
'meta' => [
'total' => $this->total(), // from paginator
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'per_page' => $this->perPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
// Custom meta
'active_count' => User::where('active', true)->count(),
'admin_count' => User::where('role', 'admin')->count(),
],
];
}
// Add response headers
public function withResponse(Request $request, \Illuminate\Http\JsonResponse $response): void
{
$response->header('X-Total-Users', $this->total());
}
}
// Controller
public function index(): UserCollection
{
return new UserCollection(User::paginate(15));
}
// Additional data (works on any resource or collection)
public function index(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection
{
return UserResource::collection(User::paginate(15))
->additional([
'meta' => [
'version' => 'v2',
'generated_at' => now()->toISOString(),
'filters' => request()->only(['role', 'search']),
],
]);
}
// Disable wrapping for a specific response (not globally)
return (new UserResource($user))->response()->setData(
(new UserResource($user))->resolve()
); // returns without 'data' wrapper
// withoutWrapping for test context
UserResource::withoutWrapping();
// ... test assertions expecting no 'data' key ...
UserResource::$wrap = 'data'; // restore