< BACK TO BLOG

Conditionally loading relations in Laravel resources

Published April 29, 2022

As your system gets bigger and bigger, and with each sprint and tight deadlines, we as developers will just append more data to the same response in the form of nested resources, this helps our API consumers by providing all the data they need, and will allow them to use 1 API instead of many, this is well and good but given enough time this will snowball into massive technical dept.

Before you know your simple listing response will take multiple seconds to return data, and your front end (regardless if it was web or mobile app) will build more and more features around this response, which will make refactoring much more complicated.

Not to mention that with more and more relations, and more and more nested resources you could forget to eager load something and you'll get an N+1 query problem 3 or 4 levels down which is very tough to debug.

Load resource when relation is loaded

Thankfully we have a clean built in solution for this problem.

Meet $this->whenLoaded(), this function will only return the key that its associated with if the model in the resource has the provided relation loaded.

Let's take this sample resource class:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'user_id' => $this->user_id,
            'user' => UserResource::make($this->user),
            'comments' => CommentResource::make($this->comments),
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Which calls these resources

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class CommentResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'body' => $this->body,
            'user_id' => $this->user_id,
            'user' => UserResource::make($this->user),
            'interactions' => CommentInteractionResource::collection($this->commentInteractions),
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

So as you can see using these resources without eager loading will cause an n+1 query, plus it will add a lot of redundant data which will slow down your response.

Let's define an index method for posts:

<?php

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return PostResource::collection(Post::paginate());
    }
}

We've seeded 2 users and 2 posts and 3 comments for each user in this example, so here's a sample response:

{
   "data":[
      {
         "id":1,
         "title":"Mr.",
         "body":"Dolorum voluptates quasi delectus autem aut voluptatem. Vitae qui voluptatem similique vel consequatur cumque itaque. Quia et dolores dolorum voluptatem ratione. Et eos ab quibusdam sunt.",
         "user_id":1,
         "user":{
            "id":1,
            "name":"Heather Marks",
            "email":"kenyon81@example.org",
            "email_verified_at":"2022-04-29T23:07:00.000000Z",
            "created_at":"2022-04-29T23:07:00.000000Z",
            "updated_at":"2022-04-29T23:07:00.000000Z"
         },
         "comments":[
            {
               "id":1,
               "body":"Qui est ut fuga qui odio ea qui omnis. Ut quia minima sed aperiam ut laudantium. Vero expedita et perspiciatis ratione quae.",
               "user_id":1,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            },
            {
               "id":2,
               "body":"Quia dolorem accusamus animi et eaque a laborum. In est earum aspernatur facere deleniti non sed. Dolor vitae et accusamus magni autem numquam voluptate. Consequatur unde eos nostrum qui necessitatibus et.",
               "user_id":1,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            },
            {
               "id":3,
               "body":"Sit saepe quisquam et vel id. Quis autem repellendus iusto dicta. In eum animi amet quasi illo.",
               "user_id":1,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            }
         ],
         "created_at":"2022-04-29T23:07:00.000000Z",
         "updated_at":"2022-04-29T23:07:00.000000Z"
      },
      {
         "id":2,
         "title":"Miss",
         "body":"Saepe aut dolor deleniti aut non velit officia. In aut consequatur qui omnis quia quisquam. Reiciendis dolor nulla non quod.",
         "user_id":1,
         "user":{
            "id":1,
            "name":"Heather Marks",
            "email":"kenyon81@example.org",
            "email_verified_at":"2022-04-29T23:07:00.000000Z",
            "created_at":"2022-04-29T23:07:00.000000Z",
            "updated_at":"2022-04-29T23:07:00.000000Z"
         },
         "comments":[
            {
               "id":4,
               "body":"Aperiam possimus sit nulla voluptas. Nobis vero maxime dolor in.",
               "user_id":2,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            },
            {
               "id":5,
               "body":"In natus dignissimos blanditiis quae fugiat. A consequuntur sint dolore reprehenderit iure consequatur sunt. Architecto unde nesciunt quis laboriosam distinctio aliquam.",
               "user_id":2,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            },
            {
               "id":6,
               "body":"Voluptas in facere laboriosam ab ratione voluptas amet. Et eum debitis laborum rerum laudantium. Iusto eum et doloribus eligendi ab labore. Quis officia et suscipit earum laborum aspernatur.",
               "user_id":2,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            }
         ],
         "created_at":"2022-04-29T23:07:00.000000Z",
         "updated_at":"2022-04-29T23:07:00.000000Z"
      },
      {
         "id":3,
         "title":"Ms.",
         "body":"Tenetur nihil voluptas maiores quo quia consequatur veritatis. Eos vitae aliquam ut repellat et. Fuga tempora rerum perferendis rerum qui sit. Dignissimos quaerat sunt aspernatur rerum harum harum.",
         "user_id":2,
         "user":{
            "id":2,
            "name":"Dr. Anibal Rohan III",
            "email":"delia.carroll@example.net",
            "email_verified_at":"2022-04-29T23:07:00.000000Z",
            "created_at":"2022-04-29T23:07:00.000000Z",
            "updated_at":"2022-04-29T23:07:00.000000Z"
         },
         "comments":[
            {
               "id":7,
               "body":"Quam aut quo odit et expedita ab. Qui quo dolores omnis omnis. Adipisci occaecati eaque necessitatibus non nisi quae beatae fugiat. Alias quo qui voluptatum ex ut amet.",
               "user_id":2,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            },
            {
               "id":8,
               "body":"Sunt nostrum aut quia sunt quo dolorem. Odit fuga incidunt et illum. Natus aut omnis magnam voluptatum.",
               "user_id":2,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            },
            {
               "id":9,
               "body":"Voluptas dignissimos consequuntur odio consectetur id est dicta. Quia inventore exercitationem voluptas nobis asperiores. Voluptatum error molestiae dolor iure autem illo nihil doloribus. Vero dolores dolores sed.",
               "user_id":2,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            }
         ],
         "created_at":"2022-04-29T23:07:00.000000Z",
         "updated_at":"2022-04-29T23:07:00.000000Z"
      },
      {
         "id":4,
         "title":"Mrs.",
         "body":"Perspiciatis est et voluptas. Molestiae quas porro repellat repudiandae.",
         "user_id":2,
         "user":{
            "id":2,
            "name":"Dr. Anibal Rohan III",
            "email":"delia.carroll@example.net",
            "email_verified_at":"2022-04-29T23:07:00.000000Z",
            "created_at":"2022-04-29T23:07:00.000000Z",
            "updated_at":"2022-04-29T23:07:00.000000Z"
         },
         "comments":[
            {
               "id":10,
               "body":"Rerum eos aperiam tenetur at voluptatem. Rem vero laboriosam tenetur autem id distinctio. Ut at minima rerum consectetur repellat nisi officia. Quidem deserunt quis non odio quam. Vel iure aut cum nihil voluptas delectus et.",
               "user_id":1,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            },
            {
               "id":11,
               "body":"Omnis eos quia et vel qui sed vel a. Provident quam at consequatur consequuntur nam repellat hic. Et cum omnis consequatur aut voluptatem non enim. Porro sunt quidem necessitatibus inventore modi.",
               "user_id":1,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            },
            {
               "id":12,
               "body":"Praesentium ut et quibusdam. Odio dolores et quidem similique consequatur quod ea nobis. Aut qui et et quis quisquam dolorum dolor.",
               "user_id":1,
               "user":null,
               "interactions":[
                  
               ],
               "created_at":"2022-04-29T23:07:00.000000Z",
               "updated_at":"2022-04-29T23:07:00.000000Z"
            }
         ],
         "created_at":"2022-04-29T23:07:00.000000Z",
         "updated_at":"2022-04-29T23:07:00.000000Z"
      }
   ],
   "links":{
      "first":"http://127.0.0.1:8000/api/posts?page=1",
      "last":"http://127.0.0.1:8000/api/posts?page=1",
      "prev":null,
      "next":null
   },
   "meta":{
      "current_page":1,
      "from":1,
      "last_page":1,
      "links":[
         {
            "url":null,
            "label":"&laquo; Previous",
            "active":false
         },
         {
            "url":"http://127.0.0.1:8000/api/posts?page=1",
            "label":"1",
            "active":true
         },
         {
            "url":null,
            "label":"Next &raquo;",
            "active":false
         }
      ],
      "path":"http://127.0.0.1:8000/api/posts",
      "per_page":15,
      "to":4,
      "total":4
   }
}

we get these queries:

[
   {
      "query":"select count(*) as aggregate from `posts`",
      "bindings":[
         
      ],
      "time":9.56
   },
   {
      "query":"select * from `posts` limit 15 offset 0",
      "bindings":[
         
      ],
      "time":0.55
   },
   {
      "query":"select * from `users` where `users`.`id` = ? limit 1",
      "bindings":[
         1
      ],
      "time":0.88
   },
   {
      "query":"select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null",
      "bindings":[
         1
      ],
      "time":0.66
   },
   {
      "query":"select * from `users` where `users`.`id` = ? limit 1",
      "bindings":[
         1
      ],
      "time":0.55
   },
   {
      "query":"select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null",
      "bindings":[
         2
      ],
      "time":0.68
   },
   {
      "query":"select * from `users` where `users`.`id` = ? limit 1",
      "bindings":[
         2
      ],
      "time":0.36
   },
   {
      "query":"select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null",
      "bindings":[
         3
      ],
      "time":0.42
   },
   {
      "query":"select * from `users` where `users`.`id` = ? limit 1",
      "bindings":[
         2
      ],
      "time":0.36
   },
   {
      "query":"select * from `comments` where `comments`.`post_id` = ? and `comments`.`post_id` is not null",
      "bindings":[
         4
      ],
      "time":0.37
   }
]

To fix it we can replace the direct relation calls with $this->whenLoaded():

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'user_id' => $this->user_id,
            'user' => $this->whenLoaded('user', fn () => UserResource::make($this->user)),
            'comments' => $this->whenLoaded('comments', fn () => CommentResource::collection($this->comments)),
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Which will return this response:

[
   {
      "id":1,
      "title":"Mr.",
      "body":"Dolorum voluptates quasi delectus autem aut voluptatem. Vitae qui voluptatem similique vel consequatur cumque itaque. Quia et dolores dolorum voluptatem ratione. Et eos ab quibusdam sunt.",
      "user_id":1,
      "user":{
         
      },
      "comments":{
         
      },
      "created_at":"2022-04-29T23:07:00.000000Z",
      "updated_at":"2022-04-29T23:07:00.000000Z"
   },
   {
      "id":2,
      "title":"Miss",
      "body":"Saepe aut dolor deleniti aut non velit officia. In aut consequatur qui omnis quia quisquam. Reiciendis dolor nulla non quod.",
      "user_id":1,
      "user":{
         
      },
      "comments":{
         
      },
      "created_at":"2022-04-29T23:07:00.000000Z",
      "updated_at":"2022-04-29T23:07:00.000000Z"
   },
   {
      "id":3,
      "title":"Ms.",
      "body":"Tenetur nihil voluptas maiores quo quia consequatur veritatis. Eos vitae aliquam ut repellat et. Fuga tempora rerum perferendis rerum qui sit. Dignissimos quaerat sunt aspernatur rerum harum harum.",
      "user_id":2,
      "user":{
         
      },
      "comments":{
         
      },
      "created_at":"2022-04-29T23:07:00.000000Z",
      "updated_at":"2022-04-29T23:07:00.000000Z"
   },
   {
      "id":4,
      "title":"Mrs.",
      "body":"Perspiciatis est et voluptas. Molestiae quas porro repellat repudiandae.",
      "user_id":2,
      "user":{
         
      },
      "comments":{
         
      },
      "created_at":"2022-04-29T23:07:00.000000Z",
      "updated_at":"2022-04-29T23:07:00.000000Z"
   }
]

Because we haven't eager loaded the relations, and when we do, then we'll have the results as before we used $this->whenLoaded()

Be aware of the 2nd parameter, you must use a callback, although it allows you to pass a value normally, then what happens is that the data will be loaded but not shown, but with a callback, it won't be executed until the relation is loaded.