vincent15000's avatar

Eloquent eager loading

Hello,

Hmmm ... I have this code.

public function index()
{
    return Inertia::render('Creator/Quests/Index', [
        'quests' => fn () => Quest::orderBy('name')->get()->toResourceCollection(),
    ]);
}

When I check in the backend (with dd()) the content of the query, it returns only the quests without any step as the steps aren't loaded.

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'code' => $this->code,
        'qrcode' => $this->qrcode,
        'synopsis' => $this->synopsis,
        'location' => $this->location,
        'coordinates' => $this->coordinates,
        'duration' => $this->duration,
        'participants_min' => $this->participants_min,
        'participants_max' => $this->participants_max,
        'level' => $this->level,
        'state' => [
            'value' => $this->state->value,
            'label' => $this->state->label(),
            'color' => $this->state->color(),
        ],
        'calculated_distance' => $this->calculated_distance,
        'calculated_duration' => $this->calculated_duration,
        'user_id' => $this->user_id,
        'steps' => StepResource::collection($this->whenLoaded('steps')),
        'medias' => MediaResource::collection($this->whenLoaded('medias')),
    ];
}

But in the frontend, I see all steps for each quest.

I have checked everywhere in my code and I never load the steps, I don't call the steps with $quest->steps, ...

It's strange.

Why do I retrieve the steps without loading them explicitely ?

Is there any implicit eager loading for the relationships in API mode ?

Thanks for your help to understand what happens.

V

0 likes
10 replies
vincent15000's avatar

I have found the problem.

protected $appends = [
    'qrcode',
    'calculated_distance',
    'calculated_duration',
];

protected function calculatedDistance(): Attribute
{
    return Attribute::make(
        get: fn () => Geography::totalDistance($this->steps->pluck('coordinates')->toArray()),
    );
}

protected function calculatedDuration(): Attribute
{
    return Attribute::make(
        get: fn () => Geography::totalDuration($this->steps->pluck('coordinates')->toArray(), $this->level),
    );
}

Is there any way to remove the appended properties just for one specific query ?

aleahy's avatar

Call ->setAppends([]) on the instance and it won't go and get the appended properties when hydrating the model for the front end.

1 like
Jsanwo64's avatar

Option 1 way to override $appends per query:

Quest::query()
    ->orderBy('name')
    ->get()
    ->each
    ->setAppends([]);

or

$quests = Quest::orderBy('name')->get();

$quests->each(function ($quest) {
    $quest->setAppends([]);
});

Another option is to use withoutAppends()

Quest::orderBy('name')
    ->withoutAppends()
    ->get();
1 like
vincent15000's avatar

In an API resource, I get some custom properties needing to load the steps.

Is it possible to add these properties to the API resource only if they are appended ?

In other words, is it possible to check if a property is appended or not ?

Jsanwo64's avatar

Of cos, you can handle this in your API Resource and avoid triggering lazy loading for properties that aren’t actually appended. Essentially, you want to conditionally include data only if the property exists in $appends or was explicitly requested.

  1. Check if a property is appended
$quest->isAppended('calculated_distance'); // true/false

So inside your Resource:

'calculated_distance' => $this->when(
    $this->resource->isAppended('calculated_distance'),
    fn() => $this->calculated_distance
),

when() will only include the key if the first argument is truthy. isAppended() ensures you don’t trigger $this->steps if calculated_distance isn’t in $appends.

  1. multiple attributes
public function toArray($request)
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'steps' => StepResource::collection($this->whenLoaded('steps')),

        'calculated_distance' => $this->when(
            $this->resource->isAppended('calculated_distance'),
            fn() => $this->calculated_distance
        ),

        'calculated_duration' => $this->when(
            $this->resource->isAppended('calculated_duration'),
            fn() => $this->calculated_duration
        ),
    ];
}
1 like
vincent15000's avatar

Oh I didn't know this function ... where is it documented ?

vincent15000's avatar

I don't know where you found this function or if you have tested it, but I get an error saying that this function doesn't exist.

Jsanwo64's avatar

Sorry must definately be a typo (typed that while feeling sleepy)

You are correct because by default in the docs, Laravel does not expose any public method like:

$quest->isAppended('calculated_distance'); // This function does not exist

Try this and see what happens

Access $appends via getAppends()

$quest->getAppends(); // This should returns array

So in your Resource:

'calculated_distance' => $this->when(
    in_array('calculated_distance', $this->resource->getAppends()),
    fn() => $this->calculated_distance
),

Or Explicit API control

Instead of relying on $appends, pass intent from the controller:

return Inertia::render('Creator/Quests/Index', [
    'quests' => fn () => Quest::query()
        ->orderBy('name')
        ->get()
        ->each
        ->setAppends(['calculated_distance']),
]);

Then in Resource:

'calculated_distance' => $this->when(
    in_array('calculated_distance', $this->resource->getAppends()),
    fn() => $this->calculated_distance
),
1 like
Thunderson's avatar

Yes, the issue is with how you wrote StepResource::collection($this->whenLoaded('steps')). You should use $this->whenLoaded('steps', fn() => StepResource::collection($this->steps)) instead.

1 like

Please or to participate in this conversation.