344 words, 2 min read

When exposing related collections through Laravel API Resources, the default order is whatever the database returns — usually insertion order. For user-facing lists, that's rarely what you want.

The Problem

A resource with a belongsToMany relation would return items in an arbitrary order:

'items' => ItemResource::collection($this->whenLoaded('items')),

Items named "Item 2" and "Item 10" would sort lexicographically: Item 10, Item 2 — not what a user expects.

The Fix

whenLoaded accepts a callback that runs only when the relation is loaded. Use it to sort the collection before handing it off to the resource:

'items' => ItemResource::collection(
$this->whenLoaded('items', fn () => $this->items->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE))
),

SORT_NATURAL gives you human-friendly ordering (Item 1, Item 2, Item 10). SORT_FLAG_CASE makes it case-insensitive. The callback is only invoked when the relation is loaded, so lazy-loading is never triggered accidentally.

Testing It

The key assertion is that numeric suffixes sort numerically, not lexicographically:

$item10 = Item::factory()->create(['name' => 'Item 10']);
$item2 = Item::factory()->create(['name' => 'Item 2']);
$item1 = Item::factory()->create(['name' => 'Item 1']);
$model->items()->attach([$item10->id, $item2->id, $item1->id]);
$model->load('items');
$result = (new ModelResource($model))->toArray(Request::create('/'));
$names = collect($result['items'])->pluck('name')->values()->all();
$this->assertSame(['Item 1', 'Item 2', 'Item 10'], $names);

Attach them out of order, load, assert the right order comes out. Simple and robust.