I feel like there should be a way to build up relationship objects, in a sequence, one join at a time.
Create a Custom Relationship Method
This has been driving me mad, because this is a question asked over and over, but the answers all seem to miss the point and provide a different solution than was asked.
What I would like to do, is to create a custom relationship method so that I can get a bunch of related models from a model instance, given an arbitrary relationship between those models.
We are used to this kind of thing out of the box:
// OrderLine
public function order
{
return $this->belongsTo('App\Order');
}
Then we can get the order model from an orderline like this:
$order = $orderline->order;
Simple. Magic stuff happens in belongsTo() - that method generates a relationship object. The relationship object is quite hard to understand, and I have not got my head around it. So my use case is that I want to do this:
$orders = $product->orders;
A product can be on many orderlines (many-to-many, with pivot table). An orderline is for an order. I would like to do this in one simple query, so the query will include a distinct or group by so I don't get orders multiple times (a product may be on several order lines in a single order).
So, how can I do this? What would the order() method look like? Now, I could create a getMeTheOrders() method that looks up the orderlines, gets the orders for each, throws away any duplicate orders, then returns the result. I don't want to do all those queries when this can be done in one query. Similarly, I want to avoid a sub-select query, as they can be very slow in MySQL since they don't get optimised well at all.
Any examples out there? Is this actually a lot more complex than I am hoping it is (considering that I cannot see this question really cracked yet, often asked wrt being able to find related records via a pivot table.
It looks like I would need to provide a concrete instance of Illuminate\Database\Eloquent\Relations\Relation which supports the relationship I would like to implement. And then extend my model with a method long the lines of the built-in relationship methods, such as belongsToMany https://github.com/illuminate/database/blob/master/Eloquent/Model.php#L932
Maybe I could create a generic relationship method that just exposes the underlying query to the model through a callback? That could be an interesting approach. Anyone here done that before?
Unless there is a way to chain the relationship methods that return multiple records together? For example, $orderline->order->user works because there is just one result at each stage. But $user->orders->orderlines is not happy at all, because there are multiple results at each stage.
A product can be on many orderlines (many-to-many, with pivot table)
No..a orderline only contains one product, right? So there would be no pivot table.
An orderline belongs to an order, and belongs to a product. Then I think you can use a hasManyThrough relation to get all the products in an order.
Probably not a clear enough example of mine - my orderline does have a single product, but that product purchase is customisable with a multiple number of additional model items. It is actually for a dog show - a product is an entry to a particular class, and that entry can include the user linking up multiple dogs they owm to that orderline to make up a team. So I am starting with the sub-product (a dog) and trying to get a list of shows it is booked onto. Once that is working, the relationship can be further qualified, such as giving me only upcoming (future) shows the dog is in:
$dog->shows; // all shows
$dog->shows()->where('show_start-date', '>', $today)->get(); // upcoming shows
(that type of thing)
A custom query in a helper function can probably do it, but I was just trying to work it in as an eloquent relationship for consistency and the experience of doing it.
And suddenly, we went from Orders, orderlines and products to teams, dogs and shows :P
Same thing though. A dog belongs to a team, and a show has many teams, no? :)
Okay, I seem to be confusing people by putting real names to models. I'll go more abstract.
We have this relationship:
A >--< B >-- C
which expands to this given the pivot table AB:
A --< AB >-- B >-- C
Given:
$a = A::find(1);
I would like to find all the C models (cs) that $a is connected to, as a relationship method:
$all_cs = $a->cs;
I actually want to get a little further to D, but we'll stop at C in this example, just accepting that there is an arbitrary query involved here.
A --< AB >-- B >-- C >-- D
(In case it helps the previous descriptions make more sense, A=dog; B=orderline, C=order, D=show)
@consil The relation classes are based on relationship types offered by a relational database system, so it doesn’t really make sense to create a new one.
It seems there are going to be a lot of joins involved and not something that can be expressed simply as a “relation”. Instead, what I’d do is write the raw SQL query that gives me the results I want, and then wrap that up in a method in my Eloquent model class:
$show = Dog::findFutureShows();
I’m a bit confused on your relationship set-ups though. Can you explain the relations between a Dog, Show, Order, and OrderLine? You seem to have additional models that are complicating things, too, i.e. a “team” aggregate or some sort?
Yes, there will be a number of joins, but nothing too complicated. The built-in laravel relationship methods cover some 90% of common queries that you may want. Adding further relationship queries does happen in core eloquent from time-to-time, but the more they add, the more they walk into the law of diminishing returns. But what I have here is just a standard relationship and nothing too special, but something that can be done efficiently in one query, but could involve a very large number of queries and models if not.
I suspect I will and up with the approach you suggest, but was just exploring how easy it would be to turn that custom query into a relationship method.
===========
Here is the run-down of the real-world relationships. It is not really necessary to understand the issue, but it's probably interesting as a bit of background:-
There is a dog show, and that has a number of show classes - sessions where single dogs, pairs of dogs (braces) or teams of dogs (three or more) are judged against other singles/braces/teams. A competitor can book dogs onto one or more of these sessions. Most sessions require one dog to the specified when booking. Some sessions require more than one dog to be booked on - so I can make a booking for the "terrier brace" show session and I MUST link TWO of my dogs to that booked session. Those dogs don't form a team or brace in any other context - they are just brought together for that purchased session.
That purchased session is an orderline. I can book multiple sessions for a single show in one order.
Working through this project, I now know FAR more about dogs and dog shows than I every thought I would need to know, but it's interesting stuff.
===========
Just as an aside, this system is kind of e-commerce, but has some differences to most (all!) baskets and carts that I have found to work from. Firstly, most of the products are customisable - when you buy a product, you need to set additional metadata to that orderline (a name, or one or more dogs, a handler's age etc). Secondly, the interaction of what products are available are very complex - they vary for a show, and significantly vary depending on what you have already purchased, and what ages, breeds and sexes your dogs are. That's a fun thing to implement! Lastly, unlike many carts, the same product (being customisable) can be purchased multiple times. Instead of just incrementing the quantity, each purchased instance is added as a separate orderline, because its metadata will be different.
That stuff is definitely not required for this question, but may be useful for someone looking to do something similar in the future.
I can actually get almost there:
// Dog
public function orders()
{
$relation = $this->belongsToMany('App\OrderLine');
$relation->getQuery()
->join('orders', 'order_lines.order_id', '=', 'orders.id')
->select('orders.*')
->groupBy('orders.id');
return $relation;
}
The query in a relation can be adjusted, and in this case I slip in another table, and select from that table, making sure I bring back unique orders only. The downside is that the retrieved data populates OrderLine models and not Order models. So if I wanted to make this a generic solution, I could extend the eloquent model to have a relatedToMany() method for example, with a callback to adjust the query (just in case the query becomes a value object in the future, which is probably unlikely).
// MyModel
public function someDistantModels()
{
return $this->relatedToMany('app\ModelToReturn', function($query) {
$query -> ... // extend the query to go find the model data
});
}
The default query would start with a simple select to the current model's table, and you can adjust the query to extend out from there in whatever direction you like.
If I get this implemented as a solution, I'll post back here.
a nice idea is to override and custom your own relationship method
@consil Did you ever figure out how to create Relation instances with arbitrary queries? I just went down a similar rabbit hole here https://laracasts.com/discuss/channels/eloquent/how-can-i-define-these-kinds-of-relationships-in-eloquent?page=1 and ended up doing the same sort of thing as you.
@matiascx I've tried googling for stuff like that but can't find anything useful. Any tips?
Using your example I was able to do this by using setQuery on the relation
public function events()
{
$relation = $this->hasMany('App\Events');
... get $ids from a non db source
$relation->setQuery(
Events::whereIn('id', $ids)->getQuery()
);
return $relation;
}
Not sure if anyone needs this, but i used the other answers around here and created this successfully:
App\UserRead
public function groupRoles()
{
$relation = $this->morphedByMany(GroupRead::class, "roles", "user_roles", "user_uuid", "related_uuid", "uuid", "uuid")
->withPivot(["role_uuid"]);
$relation->getQuery()
->join("group_roles", function($join){
$join->on("group_roles.uuid", "=", "user_roles.role_uuid");
})
->select(
'user_roles.related_uuid AS uuid',
'user_roles.related_uuid AS groupUuid',
'groups.name AS groupName',
'group_roles.name As roleName',
'group_roles.uuid AS roleUuid',
"group_roles.project_create AS projectCreate",
"group_roles.project_edit AS projectEdit",
"group_roles.asset_create AS assetCreate",
"group_roles.asset_edit AS assetEdit",
"group_roles.revision_create AS revisionCreate",
"group_roles.revision_edit AS revisionEdit"
);
return $relation;
}
though i'm sure its not 100% optimized. In my case, i needed to get some extra pivot table data "role_uuid" then get that role record and the group its associated with. I didn't want laravel to instantiate a bunch of things and go through classes after classes.
Hi, I had a similar issue. Basically I was looking for some way to relate to distant models. In my case it is:
File <- hasMany - FileVersion - belongsTo -> User - belongsTo -> Person. Basically, I wanted to relate File to the Person whom created the most recent version.
Essentially (using Laravel 10) I was able to solve it by creating a createdBy-relation on the File model:
Instead of calling $this->hasOne(...), I called $this->newHasOne(...) which allows me to configure the fields which are used for mapping.
// these lines are from Illuminate/Database/Eloquent/Concerns/HasRelationships.php
$instance = $this->newRelatedInstance(Person::class);
$relation = $this
->newHasOne($instance->newQuery(), $this, 'v.file_id', $this->getKeyName())
->latest('v.created_at');
$relation->getQuery()
->join('users as u', 'u.person_id', '=', 'people.id')
->join('file_versions as v', 'v.created_by_id', '=', 'u.id')
->select(['v.file_id', 'people.firstname', 'people.lastname', 'u.id as user_id', 'people.id as person_id']);
return $relation;
Consider this:
- The relation query starts at the people table (i.e. the most distant one). This is also why I do not have an alias for it ;)
- the foreign key column is
v.file_id, make sure to have it in the relation's result set (as it will be used to map the entity)
Hope this helps someone
Please or to participate in this conversation.