Search code examples
phplaraveleloquentcollectionslaravel-query-builder

Laravel temp attributes inside 'with' and 'each' together


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

Solution

  • 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();
    }