How to use Laravel's hasManyThrough across 4 tables

Multi tool use
How to use Laravel's hasManyThrough across 4 tables
I have 4 tables with a structure and flow like this:
User
Accounts
Contacts
Orders
The relationship is as follows:
$user->hasMany('accounts')->hasMany('contacts')->hasMany('orders');
/** User Model **/
class User extend Eloquent {
public function accounts(){
return $this->hasMany('Accounts');
}
public function contacts(){
return $this->hasManyThrough('Contact', 'Account', 'owner_id');
}
//how do I get this?
public function orders(){
}
}
/** Account Model */
class Account extends Eloquent {
public function $user(){
return $this->belongsTo('User');
}
public function contacts(){
return $this->hasMany('Contact');
}
}
/** Contact Model **/
class Contact extends Eloquent {
public function account(){
return $this->belongsTo('Account');
}
public function orders(){
return $this->hasMany('Order');
}
}
/** Order Model **/
class Order extends Eloquent {
public function contact(){
return $this->belongsTo('contact');
}
}
A classic hasManyThrough
is not possible because we have 4 tables.
hasManyThrough
How can I create a relationship so that a single user can have it's orders accessed without method chaining each model, such as:
User::find(8)->orders;
3 Answers
3
It was quite difficult at first to identify exactly how the query builder slices things together. I knew how I wanted to format the query with a raw SQL statement, but I wanted to use Eloquent for this particular task given the other defined relationships.
I was able to resolve this by overloading the attribute and manually creating the relation. However, the actual query and hasMany needs to be built manually as well. Let's take a look at how I achieved this. Remember, the goal is to get Orders
off of the user through 2 has-Many relationships.
Orders
First, the overloading of the attribute.
public function getOrdersAttribute()
{
if( ! array_key_exists('orders', $this->relations)) $this->orders();
return $this->getRelation('orders');
}
The idea in the above function is to capture when a lazy loaded ->orders
is called. such as $user->orders
. This checks to see if the orders
method is in the relations
array for the existing User model
. If it's not, then it calls our next function in order to populate the relatioship, and finally returns the relationship we've just created.
->orders
$user->orders
orders
relations
User model
This is what the function that actually queries the Orders looks like:
public function orders()
{
$orders = Order::join('contacts', 'orders.contact_id', '=', 'contacts.id')
->join('accounts', 'contacts.account_id', '=', 'accounts.id')
->where('accounts.owner_id', $this->getkey())
->get();
$hasMany = new IlluminateDatabaseEloquentRelationsHasMany(User::query(), $this, 'accounts.owner_id', 'id');
$hasMany->matchMany(array($this), $orders, 'orders');
return $this;
}
In the above, we tell the Orders table to join it's contacts (which is the established route given the ownership of belongsTo()
in this scenario). Then from the contacts, we join the accounts, then from the accounts we can get there from our user by matching our owner_id
column against our existing $user->id
, so we don't need to do anything further.
belongsTo()
owner_id
$user->id
Next, we need to manually create our relationship by instantiating an instance of hasMany
from the Eloquent Relationship
builder.
hasMany
Eloquent Relationship
Given that the HasMany
method actually extends the HasOneOrMany
abstract class, we can reach the HasOneOrMany
abstract class by passing our arguments directly to HasMany
, like below:
HasMany
HasOneOrMany
HasOneOrMany
HasMany
$hasMany = new IlluminateDatabaseEloquentRelationsHasMany(User::query(), $this, 'accounts.owner_id', 'id');
The HasOneOrMany
expects the following to it's constructor:
HasOneOrMany
Builder $query,
Model $parent,
$foreignKey,
$localKey
So for our builder query, we've passed an instance of our model that we wish to establish the relationship with, the 2nd argument being an instance of our Model ($this), the 3rd argument being the foreign key constraint from our Current->2nd model, and finally the last argument being the column to match from our current model against the foreign key constraint on our Current->2nd model.
Once we've created our instance of Relation
from our HasMany
declaration above, we then need to match the results of the relationship to their many parents. We do this with the matchMany()
method which accepts 3 arguments:
Relation
HasMany
matchMany()
array $models,
Collection $results,
$relation
So in this case, the array of models would be an array instance of our current eloquent model (user) which can be wrapped in an array to achieve our effect.
The 2nd argument would be the result of our intitial $orders
query in our orders()
function.
$orders
orders()
Finally, the 3rd argument will be the relation string
that we wish to use to fetch our instance of this relationship; which for us is order
.
string
order
Now you can correct use either Eloquent or Lazy Loading to fetch our orders for our user.
User::find(8)->orders();
$user->orders;
Hopefully this is helpful for someone else facing a similar issue.
HasMany
addConstraints
This is a heroic solution that almost works, but for some reason the Order models will not have the correct IDs when instantiated. Instead their
id
will be the same as the contact_id
. At least it is when I tested it on Laravel 5.5. I can't for the life of me figure out why though.– Jeff Puckett
May 11 at 13:42
id
contact_id
@JeffPuckett It's been awhile since I implemented this. I almost built a package out of this, too, because there were some nuances, let me revisit this and see if I can figure it out why.
– Ohgodwhy
May 11 at 16:59
you can through from user to contact then join
with Orders
join
Orders
public function orders(){
return $this->hasManyThrough('Contact', 'Account', 'owner_id')->join('orders','contact.id','=','orders.contact_ID')->select('orders.*');
}
It works for me in same case, all feedback welcome
it work for me, thanks
– sadalsuud
Feb 16 at 6:07
The problem with this approach is that the model returned will be an instance of
Contact
with all the attributes of an Order
– Jeff Puckett
May 9 at 22:30
Contact
Order
I created a HasManyThrough
relationship with unlimited levels: Repository on GitHub
HasManyThrough
After the installation, you can use it like this:
class User extends Model {
use StaudenmeirEloquentHasManyDeepHasRelationships;
public function orders() {
return $this->hasManyDeep(Order::class, [Account::class, Contact::class]);
}
}
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
You could do this also by extending
HasMany
class and just overriding the methodaddConstraints
– Israel Ortuño
Mar 25 '16 at 23:44