skip to content

In Laravel, when do you write a ResourceCollection class instead of calling WorkoutResource::collection(), and how do with() and additional() add top-level meta?

level: middleimportance: should knowfreq 36%

answer

  1. anonymous versus named collection
  2. make:resource WorkoutCollection or --collection
  3. $this->collection holds mapped resources
  4. with() only on the outermost resource
  5. additional() set at the call site

basics

~20 s

WorkoutResource::collection() builds an anonymous collection that maps each item and is enough for plain lists and pages. Write a ResourceCollection when the list itself needs fields, meta or pagination hooks; with() adds class-defined meta, additional() adds per-call meta.

solid answer

~40 s

`WorkoutResource::collection($workouts)` returns an `AnonymousResourceCollection` that maps each model through `WorkoutResource`, and it handles paginators too. A named `WorkoutCollection extends ResourceCollection` (generated by `make:resource WorkoutCollection` or `--collection`) is worth writing when the collection has its own output: summary fields such as weekly totals, a `with()` method, `paginationInformation()`, or reuse across endpoints. It finds its item class by stripping `Collection` from its name, or from `#[Collects]` or `$collects`, and exposes the mapped items as `$this->collection`. `with($request)` lives in the class and is merged only when that resource is the outermost response; `additional([...])` is chained at the call site for request-specific meta. Both are merged recursively beside the `data` key.

code

php · 23 lines
php
<?php

namespace App\Http\Controllers;

use App\Http\Resources\WorkoutCollection;
use App\Http\Resources\WorkoutResource;
use App\Models\Workout;
use Illuminate\Http\Request;

class WeeklyWorkoutController
{
    public function index(Request $request)
    {
        $week = $request->user()->workouts()->thisWeek()->get();

        // Plain list: the anonymous collection is enough.
        // return WorkoutResource::collection($week);

        // List with its own fields and meta: a named collection.
        return (new WorkoutCollection($week))
            ->additional(['meta' => ['week' => now()->isoWeek()]]);
    }
}

go deeper

for a junior

Recall WorkoutResource::collection() for lists and make:resource WorkoutCollection for a named collection whose toArray() can add list-level fields.

for a middle

Explain how a ResourceCollection resolves the item class, what $this->collection holds, the difference between with() and additional(), and the 201 rule for freshly created models.

for a senior

Choose between anonymous and named collections deliberately, keep response meta on the outermost resource, and predict how custom meta merges with pagination output.

for a principal

Standardise list envelopes across an API: which meta every list carries, where it is defined, and how named collections stay consistent as endpoints multiply.

## Two ways to render a list An API resource (a class extending `JsonResource`) shapes one model. For a list there are two options. 1. **The anonymous collection.** `WorkoutResource::collection($workouts)` returns an `AnonymousResourceCollection`, a `ResourceCollection` that remembers which resource to apply and maps every item through it. It accepts a collection or a paginator, so `WorkoutResource::collection(Workout::paginate(20))` already produces `data`, `links` and `meta`. In Laravel 13, `$workouts->toResourceCollection()` and `$paginator->toResourceCollection()` do the same by convention. 2. **A named collection.** `php artisan make:resource WorkoutCollection` (the `Collection` suffix, or the `--collection` flag, selects the collection stub) creates a class extending `ResourceCollection`. You return it with `new WorkoutCollection($workouts)`. ## When a named collection earns its place The anonymous collection has no code of its own, so write a class when the **list** needs behaviour: - **List-level fields**: a fitness app's weekly screen wants `total_km` and `sessions` beside the workouts. - **A `with()` method** that always adds the same meta, such as the units system. - **`paginationInformation()`** to reshape `links` and `meta`, which the paginated response looks for on the collection instance. - **Reuse**: several endpoints return the same list shape. - **Convention discovery**: among its naming conventions, `toResourceCollection()` prefers a `WorkoutCollection` class over `WorkoutResource::collection()`, and `#[UseResourceCollection]` on the model names one explicitly. If none of these apply, the anonymous collection is simpler and adds no class to maintain. ## How a ResourceCollection maps its items The constructor stores the given items and maps each into the **collected resource class**, exposing the result as `$this->collection`. That class is resolved in this order: 1. a `#[Collects(WorkoutResource::class)]` attribute on the collection; 2. the public `$collects` property; 3. the collection's own name with `Collection` removed (`App\Http\Resources\Workout`), then with `Collection` replaced by `Resource` (`App\Http\Resources\WorkoutResource`). If the resolved class is not a `JsonResource`, a `LogicException` is thrown. Items that are already instances of the collected class are not wrapped a second time. When the input is a paginator, the mapped items are put back into it with `setCollection()`, so the page state behind `links` and `meta` survives. The collection is also countable and iterable, so `count()` and `foreach` work on it directly. In `toArray()` you usually return `parent::toArray($request)` or an array such as `['data' => $this->collection, 'total_km' => ...]`; a `data` key you write yourself is kept as is. By default the item keys are renumbered; the `#[PreserveKeys]` attribute on the item resource keeps them, for example when the list is keyed by date. ## with() versus additional() Both add keys beside the payload at the top level of the JSON, and both are merged with `array_merge_recursive`, together with pagination `links` and `meta` when the collection wraps a paginator. | | `with(Request $request)` | `additional(array $data)` | |---|---|---| | Defined | as a method in the resource class | chained where the resource is created | | Varies by | anything the class can compute | anything the controller knows | | Applied when | the resource is the outermost response | the resource is the outermost response | | Typical use | API version, units system | request-specific counts, feature flags | Neither applies to a resource nested inside another resource's array: only the outermost resource builds the response. If you add a `meta` key through either and the collection is paginated, your entries sit beside `current_page` and `total` rather than replacing them. ## Customising the response itself Two more hooks complete the picture: - `->response()` turns any resource into an `Illuminate\Http\JsonResponse`, so you can chain `->header('X-Units', 'metric')` or `->setStatusCode(...)`. - A `withResponse(Request $request, JsonResponse $response)` method on the class runs whenever that resource is the outermost response. The status defaults to 200, except that a single resource wrapping a model whose `wasRecentlyCreated` flag is true, such as one just returned from `Workout::create()`, is sent with **201 Created**. ## Example ```php <?php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\ResourceCollection; class WorkoutCollection extends ResourceCollection { public function toArray(Request $request): array { return [ 'data' => $this->collection, 'total_km' => round($this->collection->sum('distance_m') / 1000, 1), ]; } public function with(Request $request): array { return ['meta' => ['units' => 'metric']]; } } ``` A controller can then add request-specific meta: `(new WorkoutCollection($week))->additional(['meta' => ['week' => $isoWeek]])`, and both `meta` arrays merge into one.

  • What status code does returning new WorkoutResource(Workout::create($data)) send, and why?
    201 Created. The resource response checks whether the wrapped value is an Eloquent model whose `wasRecentlyCreated` flag is true, which `create()` sets after inserting. Any other single resource, and collections, default to 200. To send something else, chain `->response()->setStatusCode(...)`.
  • Why is with() on a nested ExerciseResource never seen in the JSON?
    `with()` is read only by the response builder, which runs for the resource returned from the controller. A resource nested in another resource's array is serialized through `jsonSerialize()`, which resolves its `toArray()` alone. Put list- or response-level meta on the outermost resource or pass it with `additional()`.
  • How does a named collection decide which resource to map each item into?
    It uses a `#[Collects]` attribute if present, else the `$collects` property, else its own class name with `Collection` stripped (`Workout`) or replaced by `Resource` (`WorkoutResource`) in the same namespace. A resolved class that is not a `JsonResource` raises a `LogicException`; if nothing resolves, items are output through their own `toArray()`.

saying these in an interview costs you the question

  • Every model needs its own ResourceCollection class to return a list.
  • WorkoutResource::collection() cannot handle a paginator.
  • with() meta also appears for every nested resource.
  • additional() replaces the pagination meta instead of merging with it.
  • Resource responses always use status 200, even after create().