Skip to main content

Command Palette

Search for a command to run...

Understanding `whenLoaded()` and `when()` in Laravel API Resources

Updated
5 min read
Summery
This blog post explains how to use whenLoaded() and when() in Laravel API Resources to conditionally include data in API responses. whenLoaded() only includes related data if it's been eager-loaded, preventing performance issues, while when() offers a more general way to include data based on any condition, including dynamically added attributes. The post also highlights that when(isset()) can be used as a fallback when whenLoaded() fails, especially when dealing with attributes added using setAttribute().

In Laravel, whenLoaded() is a useful method in API resources that ensures relationships are only included in the response if they have been eager-loaded. However, sometimes whenLoaded() may not work as expected. In this post, we'll explore why that happens, and how to fix it with a practical example of a Post with Comments.


1. Setting Up Our Models

Post Model (Post.php)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'content'];

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

Comment Model (Comment.php)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    use HasFactory;

    protected $fillable = ['post_id', 'body'];

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

2. Creating the API Resource (PostResource.php)

Now, let's create a resource to format our API response.

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'content' => $this->content,
            // Using whenLoaded() to include comments only if they are eager-loaded
            'comments' => $this->whenLoaded('comments', fn() => CommentResource::collection($this->comments)),
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Comment Resource (CommentResource.php)

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class CommentResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'body' => $this->body,
            'post_id' => $this->post_id,
            'created_at' => $this->created_at,
        ];
    }
}

3. Controller with and without Eager Loading

Using whenLoaded() Correctly

To make whenLoaded() work, we must eager-load the comments relationship.

Correct way:

use App\Models\Post;
use App\Http\Resources\PostResource;

public function show($id)
{
    $post = Post::with('comments')->findOrFail($id);
    return new PostResource($post);
}

Response (with eager loading)

{
    "id": 1,
    "title": "My First Post",
    "content": "This is the content of the post.",
    "comments": [
        {
            "id": 1,
            "body": "Great post!",
            "post_id": 1,
            "created_at": "2024-03-08T12:00:00.000000Z"
        }
    ],
    "created_at": "2024-03-08T11:00:00.000000Z",
    "updated_at": "2024-03-08T11:30:00.000000Z"
}

Using whenLoaded() Incorrectly

If you forget to eager-load comments, the whenLoaded() method will return null.

Incorrect way (no eager loading):

public function show($id)
{
    $post = Post::findOrFail($id); // No eager loading
    return new PostResource($post);
}

Response (without eager loading)

{
    "id": 1,
    "title": "My First Post",
    "content": "This is the content of the post.",
    "comments": null,
    "created_at": "2024-03-08T11:00:00.000000Z",
    "updated_at": "2024-03-08T11:30:00.000000Z"
}

4. Alternative Using when()

If whenLoaded() doesn’t work, you can use when() with isset() as a fallback.

'comments' => $this->when(
    isset($this->comments),
    CommentResource::collection($this->comments)
),

This works even if the comments are not eager-loaded because it checks whether the property exists.


5. Using setAttribute to Modify Attributes Dynamically

We can dynamically add attributes to our models using the setAttribute method and conditionally include them in our API responses using the when method in API resources. Here's how we can implement this:

Modifying the Model in the Controller

In our controller, after retrieving the Post model instance, we can use the setAttribute method to add a custom attribute. This attribute can then be conditionally included in the API resource response.

use App\Models\Post;
use App\Http\Resources\PostResource;

public function show($id)
{
    // Retrieve the post along with its comments
    $post = Post::with('comments')->findOrFail($id);

    // Dynamically add a custom attribute to the model
    $post->setAttribute('custom_message', 'This is a dynamically added attribute');

    // Return the post resource
    return new PostResource($post);
}

Updating the API Resource to Include the Custom Attribute

In our PostResource, we can use the when method to conditionally include the custom_message attribute in the response. The when method checks if the custom_message attribute exists and is not null before including it in the serialized output.

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'content' => $this->content,
            'comments' => CommentResource::collection($this->whenLoaded('comments')),
            'custom_message' => $this->when(
                $this->resource->getAttribute('custom_message') !== null,
                $this->resource->getAttribute('custom_message')
            ),
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

In this example, the when method checks if the custom_message attribute is not null before including it in the response. This ensures that the attribute is only included when it has been set, providing flexibility in our API responses.

Expected Response

When the custom_message attribute is set as shown above, the API response will include this attribute:

{
    "id": 1,
    "title": "My First Post",
    "content": "This is the content of the post.",
    "comments": [
        {
            "id": 1,
            "body": "Great post!",
            "post_id": 1,
            "created_at": "2024-03-08T12:00:00.000000Z"
        }
    ],
    "custom_message": "This is a dynamically added attribute",
    "created_at": "2024-03-08T11:00:00.000000Z",
    "updated_at": "2024-03-08T11:30:00.000000Z"
}

If the custom_message attribute is not set, it will be excluded from the response, ensuring a clean and efficient API output.

By following this approach, we can dynamically add attributes to our models and conditionally include them in our API responses using Laravel's setAttribute and when methods.


6. Summary

ScenariowhenLoaded()when(isset())
Relationship is eager-loaded (with('comments'))✅ Works✅ Works
Relationship is not eager-loaded❌ Returns null✅ Works
setAttribute is used❌ Not detected✅ Works

7. Best Practices

  1. Always eager-load relationships when using whenLoaded().

  2. Use when(isset()) when working with dynamically added attributes.

  3. Prefer whenLoaded() for performance when dealing with large datasets.