Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

user947's avatar

Custom eloquent relationship?

Hello,

Suppose that i have a Foo model and its table has (id, name, child_id_offset) columns. I want to define 'childs' method and when i call Foo::with('childs') it would load all childs whose id is higher than their parent's child_id_offset.

So i would define relationship like this but it is invalid:

public function childs ()

{

// instead of return $this->hasMany(Foo::class, child_id_offset)

return $this->hasMany(Foo::class, child_id_offset, id, '<')

// would mean 'where foo2.id > foo.child_id_offset'

}

I think this cannot be done with eloquent, so i want an alternative way to eager load those childs and access them as $foo->childs.

Example: Table : foo, Model: Foo, Relation name: childs

id | name | child_id_offset

1 | A | 2

2 | B | 3

3 | C | 2

4 | D | 4

5 | E | 1

if i would do Foo::find(1)->with(['childs']) it would return me a model "A" which has relations that consists of models ("C", "D", "E"). Because C, D and E has id higher than A's offset.

0 likes
22 replies
bugsysha's avatar

Everything can be done with Eloquent 😁

return $this->hasMany(Foo::class, 'id', 'child_id_offset')->where('child_id_offset', '>', $this->child_id_offset);

// or

return $this->hasMany(Foo::class, 'id', 'child_id_offset')->where('child_id_offset', '>', $this->id);

I do not understand what you are doing so there you go, two possible solutions.

1 like
user947's avatar

What you wrote just adds a where condition to already joined table. If i would do what your code does with plain SQL, the query would be:

SELECT * FROM foo INNER JOIN foo as foo2 ON foo.child_id_offset = foo2.id WHERE foo2.child_id_offset > foo.id

which also would translate to:

SELECT * FROM foo INNER JOIN foo as foo2 ON foo.child_id_offset = foo2.id AND foo2.child_id_offset > foo.id

which would return no results.

I need

"SELECT * FROM foo INNER JOIN foo as foo2 ON foo.child_id_offset < foo2.id" equivalent

Also, your example throws an exception. InvalidArgumentException. Illegal operator and value combination.

Seems like you cant pass $this to where method like that.

I edited the question and added example with data.

bugsysha's avatar

Also, your example throws an exception. InvalidArgumentException Illegal operator and value combination.

That is just a pseudo code. As I said I do not fully understand what you are trying to do. It was just supposed to give you an idea how to solve this. So on your relationship just chain a where method to specify what you need.

Seems like you cant pass $this to where method like that.

Yes you can.

user947's avatar

I have added an example. hasMany gets related records on primary_key = foreign_key. I need something that gets me primary_key < foreign_key. If i add where condition, it does not override default behaviour of hasMany, just filters the result of hasMany..

Example: Table : foo, Model: Foo, Relation name: childs

id | name | child_id_offset

1 | A | 2

2 | B | null

3 | C | null

4 | D | null

5 | E | null

if i would do Foo::find(1)->with(['childs']) it would return me a model "A" which has relations that consists of models ("C", "D", "E"). Because C, D and E has id higher than A's offset.

bugsysha's avatar

Have you tried what I've said? Chaining where?

Also wouldn't C cause a loop cause offset is 2 and ID of C is 3?

You would then need additional where statement to exclude itself.

->where('id', '!=', $this->id)
user947's avatar

C would just return relation that consist of (C, D, E), it is not problem since it can be treated as its own child. It would cause loop if i appended "->with("childs")" to hasMany for recursion.

As i said your solution throws an exception not allowing $this inside where method.

bugsysha's avatar

As i said your solution throws an exception not allowing $this inside where method.

Put dd($this->id) in your relationship method and see if you get a value.

user947's avatar

it is null, although when i return object it has all attributes set.

when i call $foo->childs, it works tho. it seems that model instance are not initialized when you call "with" and "load" methods.

bugsysha's avatar

Then there lies the solution. Try replacing $this->id with foo.id?

user947's avatar

or i can put just 'id' since it is self referencing.

->where('child_id_offset', '<', 'id')

it only compares the record's id with its own child_id offset and will always return false unless they are equal... Pretty sure appending where method will not work here in any condition. Because the purpose of appending where method to relationship is just adding scope, so it extracts data from what hasMany returns.

bugsysha's avatar
->where('child_id_offset', '<', $this->id ?? 'id')

You can probably use something like this to make it work when you have a loaded model and when you do not.

user947's avatar

Man, it just compares one record's id to its own child_offset_id. So for above example, it will no relationship. Because hasMany will fetch B as A's child, and where scope will see that B's id is not greater than its own child_id_offset which will result empty collection. However, it is supposed to return [C, D, E]. So i guess there is no way other than overriding hasMany method or smth.

bugsysha's avatar

We are not talking about the same thing.

user947's avatar

well, relationship childs is defined as you proposed

$this->hasMany(Foo::class, child_offset_id, id)->where('child_id_offset', '<', 'id');

Foo::find(1)->with('childs') returns empty relationship, while i want it to return ['C', 'D', 'E']. Test it yourself, i dont know what you are talking about.

bugsysha's avatar

It works fine for me.

class User extends Model
{
    public function children(): HasMany
    {
    return $this->hasMany(User::class, 'offset', 'id')->where('id', '>=', $this->offset ?? 'offset');
    // or
        return $this->hasMany(User::class, 'offset', 'id')->whereRaw('id >= ?', $this->offset ?? 'offset');
    }
}
user947's avatar

When i put your code, it returns no children with my data. Because you have (User::class, 'offset', 'id'). you should treat id as foreign key and offset as primary key for this case (User::class, 'id', 'offset'). I have converted the result to array then pass to dd to make it readable https://imgur.com/pYzWocc . When i change the order, it still returns what i expected, not what is intended. It returns only B. https://i.imgur.com/wj7BpOs

Can someone confirm that his code doesn't return desired result?

bugsysha's avatar

I've received correct results from your example with the code I've posted.

user947's avatar

Can you screenshot the result executing this?

dd(User:with('children')->find(1)->toArray());

Also your function return type is Illuminate\Database\Eloquent\Relations\HasMany, right?

bugsysha's avatar

Can you screenshot the result executing this?

Install fresh Laravel and do what I suggested.

Also your function return type is Illuminate\Database\Eloquent\Relations\HasMany, right?

I guess. As far as I know there is only one class named like that.

user947's avatar
Install fresh Laravel and do what I suggested.

I did that and posted my proofs. But you did not post any screenshots.

I consider above replies useless, unless someone else confirms i am wrong.

bugsysha's avatar

Like screenshots prove anything. Best luck.

Please or to participate in this conversation.