I'm trying to use Laravel each method inside a with in my query builder but the attribute I'm adding inside my each method does not persist to the collection. My attribute does show when I dd($task);
inside the each method.
$projects =
Project::with(['tasks' => function($query) {
$query
->whereBetween('start_date', ['2024-04-21', '2024-04-28'])
->orWhereBetween('end_date', ['2024-04-21', '2024-04-28'])
->each(function($task, $key) {
//if statements here
$task->test = 'test';
$task->setAttribute('date', $task->end_date);
// dd($task); <- this returns the 'test' and 'date' attributes above
});
}])
->status(['Active', 'Scheduled'])->sortByDesc('last_status.start_date');
// dd($projects->first()->tasks->first()); <- this is missing the 'test' and 'date' attributes from above
TL;DR
You can't use each
inside a with
because it's not actually returning the models, it's just using the query and combining data later.
Jump to the workaround section below for a potential solution.
How "with" works
Eloquent uses with
to allow nested relationships to be returned as a hierarchy. This is different to a join
query where the attributes of both models are returned as one object containing all the attributes
Behind the scenes Eloquent performs separate queries for each of the with
relationships, then builds the object hierarchy in memory before returning the result.
In your case there will be a
select *
from projects
followed by
select *
from tasks
where project_id in (1,2,3 ...) -- all the project.id values
and ... -- your additional where clauses
The projects
collection is then merged with their related tasks
using the foreign key in the task objects.
In contrast, each
is a callback applied to all the models returned by the query after it has been executed. By using each
on the query inside a with
it's forces Eloquent to execute it immediately as well as return the query for later execution; but appears to ignore the each
when building the object hierarchy.
In testing this on my local project I see the second query - the one inside the with
closure - executed twice even though it's only needed once to get all the tasks
associated with the project
.
Workaround
To add attributes to the task
models you can perform separate queries and combine the results.
// Get all the projects
$projects = Project::all();
// Get tasks for these projects and add attributes.
$projectIds = $projects->pluck('id');
$tasks = Task::whereIn('project_id', $projectIds)
->get()
->each(function ($task) {
$task->test = 'test';
})->groupBy('project_id');
// Combine projects and tasks
foreach ($projects as $project) {
$project->tasks = $tasks[$project->id] ?? collect();
}