0

ResourceCollection — pagination wrapping, custom meta

Intermediate5 min read·lv-24-003

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
<?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