0

I would like to know how to pass an argument to a model relationship function. Just to be clear, I'm NOT talking about the query callback.

Consider a model like so:

class UserRelationships extends Model
{
    // other stuff
    // dynamic scope:
        /**
     * Scope a query to only include users of a given type.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @param  mixed  $type
     * @return \Illuminate\Database\Eloquent\Builder
     */
    // $relationships = UserRelationships::at( Carbon::parse('2022-10-10') )->get();
    public function scopeAt($query, Carbon $date)
    {
        return $query->where('superseded_at', '>', $date )
                    ->where('created_at', '<=', $date );
    }
}

And a related model featuring the following relationships:

class User extends Authenticatable
{
    public function progenial_relation(Carbon $date=null) // returns this user record in the userRelationships table, which can be used to retrieve this users parent (directly lookup the sponsor_id)
    // when eager loading, this is useful for getting all users whose parent is x, hence the name
    {
        return $this->hasOne(UserRelationships::class, 'user_id', 'id')
                    ->at( @$date ?: Carbon::now() ) // local dynamic scope in userRelationships
                    ->orderByDesc('created_at')
                    ->limit(1);
    }
    public function parental_relation(Carbon $date=null) // returns records from the userRelationships table, of all the users which refer to this user as their sponsor
    // when eager loading, this is useful for getting the user whose child is x, hence the name
    {
        return $this->hasMany(UserRelationships::class, 'sponsor_id', 'id')
                    ->at( @$date ?: Carbon::now() ); // local dynamic scope in userRelationships
    }

}

As you can see my relationships accept an argument (the date).

Now, if you wanted to use those relationships straightforwardly like so, there's no issues:

$date = Carbon\Carbon::parse('2022-06-01');
$relations_at_date = User::find(1)->parental_relation( $date )->get();

But what happens if you need to use eager-loading methods such as has(), whereHas(), doesntHave(), whereDoesntHave()? How do you pass an argument to the relationship? For example, I wanted to add other relationships to my User model.

public function children(Carbon $date=null)
{
    $date = @$date ?: Carbon::now();
    return self::whereHas('progenial_relation', function($q) {
        $q->where('sponsor_id', $this->id);
    }, $date); // not working
}

I tried with these syntax, but it doesn't work:

whereHas( 'relationship_name', $callback, $argument )

whereHas( 'relationship_name', $argument, $callback )

whereHas( 'relationship_name', [$argument], $callback )

whereHas( 'relationship_name', $callback, [$argument] )

Is it somehow possible? Are there any alternatives?

For completeness I'm going to add what happens if I use a normal closure:

public function children(Carbon $date=null)
{
    $date = @$date ?: Carbon::now();
    return self::whereHas('progenial_relation', function($q) use ($date) {
        $q->at($date)->where('sponsor_id', $this->id);
    });
}

This is the resulting SQL. As you can see the constraints are applied twice. Once by the query callback and once by the relationship. But since I cannot pass the correct argument to the relationship, it gets the default one. The 2 constraints collide and the query does not work.

"select * from `users` 
where exists (
    select * 
    from `user_relationships` 
    where `users`.`id` = `user_relationships`.`user_id` 
    and `user_relationships`.`superseded_at` > ? 
    and `user_relationships`.`created_at` <= ? 
    and `sponsor_id` = ? 
    and `user_relationships`.`superseded_at` > ? 
    and `user_relationships`.`created_at` <= ?
)
and `users`.`deleted_at` is null"
Valentino
  • 465
  • 6
  • 17

1 Answers1

0

I don't think that its possible to pass variables to relationship methods when eager-loading like this.

But you can apply a sub-query to the wherehas:

 $date = @$date ?: Carbon::now();
 return self::whereHas('progenial_relation', function($q) use ($date) {
        $q
         ->where('sponsor_id', $this->id)
         ->at( @$date ?: Carbon::now() );
    }, $date);

Although I'm not sure what the ->at method/scope you added does.

Mtxz
  • 3,749
  • 15
  • 29
  • I already tried and it cannot work. I'm going to update the question with the results of doing this – Valentino Nov 25 '22 at 10:52
  • the scope of at() is shown in the first model – Valentino Nov 25 '22 at 10:56
  • Ah yes, I didn't specify it, but my solution implies removing the `->at` scope on the relationship. So you'd have to add it every time you need it of course. It'd prevent having it applied to time as you explained. Again I don't think that you can pass a parameter like you want to, so I'd do it that way. – Mtxz Nov 25 '22 at 16:40
  • that cannot work, think about it. By restraining the callback you are filtering the results, but the relationship is still producing those results. In a recursive relationship such as this, you end up with branches generated multiple times (one per each duplicate match, which should have been ignored by the relationship, but was not) – Valentino Nov 25 '22 at 19:14