How to use whenLoaded in Laravel API Resources

Picture

An Eloquent API resource in Laravel is a class that allows you to transform your Eloquent models and model collections into a JSON format suitable for API responses. It provides a convenient way to shape the output of your model data, making it easier to control what data is exposed and how it's structured in your API responses.

Here's an example:

<?php
 
namespace App\Http\Resources;
 
use Illuminate\Http\Resources\Json\JsonResource;
 
class UserResource extends JsonResource
{
	public function toArray($request)
	{
		return [
			'id' => $this->id,
			'name' => $this->name,
			'email' => $this->email,
			'created_at' => $this->created_at,
			'updated_at' => $this->updated_at,
		];
	}
}

Resources are highly customizable, as you can see in the Laravel documentation. Among all the available methods, I stumbled upon the whenLoaded method, whose explanation in the documentation was rather confusing, at least to me.

The whenLoaded method may be used to conditionally load a relationship. In order to avoid unnecessarily loading relationships, this method accepts the name of the relationship instead of the relationship itself:

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,
	];
}

Example provided by the documentation

In this example, if the relationship has not been loaded, the posts key will be removed from the resource response before it is sent to the client.

It's not clear what is supposed to load the posts collection, as the documentation doesn't explicitly state it at any point. To make the example more understandable, I've tweaked it a bit, as it's a bit confusing to have a PostResource check if a posts collection is loaded.

<?php
 
namespace App\Http\Resources;
 
use Illuminate\Http\Resources\Json\JsonResource;
 
class PostResource extends JsonResource
{
	public function toArray($request)
	{
		return [
			'id' => $this->id,
			'title' => $this->title,
			'content' => $this->content,
			'comments' => CommentResource::collection($this->whenLoaded('comments')),
		];
	}
}

Now the relationship makes more sense, since it's a Post with optional Comments attached. However, the question of what loads the comments collection remains unanswered. After reading the documentation for Eloquent: Relationships and doing some testing, you can figure out that we should load the Post collection from the PostController as follows:

<?php
 
namespace App\Http\Controllers;
 
use App\Models\Post;
use App\Http\Resources\PostResource;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
 
class PostController extends Controller
{
	public function index(Request $request)
	{
		$posts = Post::with('comments')->get();
 
		return PostResource::collection($posts);
	}
}

In the example above, , thanks to the whenLoaded method, the comments property of the JSON should contain all of the comments associated with each post. Conversely, if we change the $posts = Post::with('comments')->get(); for $posts = Post::all();, the comments property will be empty because we haven't loaded the comments associated with each post.

Now that we know where the relationship comes from, we might come up with the following question: how can we use whenLoaded for relationships deeper than one level? The answer for this in recent versions of Laravel is quite simple, as you can just do Post::with('comments.replies'), or even pass a nested array to the with method, which can be convenient when eagerly loading multiple nested relationships:

$books = Book::with([
	'author' => [
		'contacts',
		'publisher',
	],
])->get();

Example taken from the nested eager loading section of the documentation.

As you can see, whenLoaded is indeed a powerful method "to avoid unnecessarily loading relationships", as the docs describe.