Eloquent relation with two keys - laravel

I am working on a team sports statistics project in Laravel.
Here is a sketch of the main models regarding this question:
class Team
// belongsToMany matches
// belongsToMany people
class Match
// belongsToMany teams
// belongsToMany people
class Person
// belongsToMany matches
// belongsToMany teams
I have also made models for the pivot tables to work directly with them. I have some stuff like goals_scored, number, position etc. stored there and it's too clumsy to use methods for updating pivot.
class MatchTeam // Participation of a team in the match
// belongsTo match
// belongsTo team
class MatchPerson // Participation of a person in the match
// belongsTo match
// belongsTo team
// belongsTo person
Is there an Eloquent way to relate MatchTeam to MatchPerson? I don't want to introduce a direct match_team_id foreign key, I want to do this using the existing match_id and team_id fields on both of the tables.
Currently I have this in my MatchTeam model:
public function matchPeople() // the roster
{
return $this->hasMany('App\Stats\MatchPerson', 'team_id', 'team_id')->where('match_id', $this->match_id);
}
It works fine. However, I am very concerned that this code is not fair to the nature of the relation - they are not related based on team_id, they are related based on both fields. I could have also written it like this:
public function matchPeople() // the roster
{
return $this->hasMany('App\Stats\MatchPerson', 'match_id', 'match_id')->where('team_id', $this->team_id);
}
Is there a fair way in which I could specify relation based on two columns in Eloquent?
Of course, this question is more out of curiosity as the actual problem of making the method is conquered.
A usage example for the curious:
$teamA->goals_for = $teamA->matchPeople->sum('goals');

Related

Laravel: Load only pivot additional columns without relation

I have the following structure:
Charge
id
amount
created_at
..
Payment
id
amount
created_at
..
and a pivot table charge_payment, since one charge can be covered partially by multiple payments.
charge_payment
charge_id
payment_id
amount
The amount column on the pivot table defines the amount that the given Payment covers for the Charge.
And I need to get only the "paid" amount for a Payment.
I have the following models:
// Charge.php
class Charge extends Model
{
public function payments()
{
return $this->belongsToMany('App\Payment')->withPivot('amount');
}
}
// Payment.php
class Payment extends Model
{
public function charges()
{
return $this->belongsToMany('App\Charge')->withPivot('amount');
}
}
I want to be able to get only the paid amount with:
$payment = Payment::find(..);
$paid_amount = $payment->charges->sum('pivot.amount');
However, with this approach the related models are loaded from the database and I won't be needing them.
Is there a way to load only the additional pivot column without the related models?
Thank you very much!
Firstly, if one Payment can be always related only to one Charge, I would advise you to change the relationship from many-to-many to one-to-many (it will be just more correct and simpler).
Than you can simple use aggregate function (in your case withSum) which will return you only resulting values without relationship Models.
$payment = Payment::withSum('payments', 'amount')->find(..);
{{ $payment->payments_sum_amount }}

Struggling with Laravel's morphable relationships

In a project, let's say that we have Customers, and each customer can have one Voucher. The voucher, though, may be for a different thing for different customers - maybe for a Hote, a Car or a Flight.
We have a table of flight voucher codes, a table of hotel voucher codes and a table of car voucher codes.
When a customer is allocated a voucher, therefore, we allocated them the next code for the relevant thing that they're getting a voucher for. But rather than have multiple tables (customer_car_voucher, customer_hotel_voucher, and so on) I would rather have a Voucher table which is, in turn, linked to the relevant voucher type.
What I want to be able to do is just go $customer->voucher->code to get the relevant code, whether that be a flight, a hotel or a car. Other vouchers may be added at a later date, you see, for different things.
I think I can do this using morphable relationships - the voucher morphsTo car, hotel and flight, so within the the voucher table there is a "voucherable_type" and a "voucherable_id". But damned if I can get it to work.
Any help, please? Am I going about it wrong?
you arte right. and for a hint use:
public function customer()
{
return $this->belongsTo(Customer::class):
}
public function voucherable()
{
return $this->morphTo();
}
in voucher model.
and for each flight,car,hotel include:
public function voucher(){
return $this->morphOne(Voucher::class,'voucherable');
}
you can see Laravel morph relationship too for more help.
In Laravel's Eloquent ORM is used for morphable relationships.
First, create two Models AirVoucher and Voucher.
First, the AirVoucher model uses the following relationship.
public function voucher()
{
return $this->morphOne(Voucher::class, 'voucherable');
}
Second, the Voucher model uses the following relationship.
public function voucherable()
{
return $this->morphTo();
}
You can use the following Laravel official relationship document for more help.
Laravel Morph Relationships.
you must use laravel Polymorphic Relationships.
in Voucher model set this model as polymorph model(function name = modelname+"able"):
public function voucherable() \Illuminate\Database\Eloquent\Relations\MorphTo
{
return $this->morphTo();
}
then in Car model (or hotel/fight) set realation(function name= polymorph name):
if each car has one voucher, use morphOne:
public function files(): \Illuminate\Database\Eloquent\Relations\MorphOne
{
return $this->morphOne(Voucher::class, 'voucherable');
}
if each car has many voucher, use morphMany:
public function files(): \Illuminate\Database\Eloquent\Relations\MorphMany
{
return $this->morphMany(Voucher::class, 'voucherable');
}
Retrieving The Relationship
$car = Car::find(1);
$vocher = $car->voucher;
laravel docs

Laravel - Eloquent relation whereHas one or more other relations

I'm learning Laravel and Laravel eloquent at the moment and now I try to solve a problem using relations in Laravel.
This is what I want to archive:
The database holds many sport clubs. A sport club has a lot of teams. Each team has games. The teams table has a column named club_id. Now I want to create Eloquent relations to get all games of a club.
Here is what I got so far:
Club model
id => PRIMARY
public function games()
{
return $this->hasMany('App\Models\Games')->whereHas('homeTeam')->orWhereHas('guestTeam');
}
Game model
home_id => FOREIGN KEY of team ; guest_id => FOREIGN KEY of team
public function homeTeam()
{
return $this->belongsTo('App\Models\Team','home_id')->where('club_id','=', $club_id);
}
public function guestTeam()
{
return $this->belongsTo('App\Models\Team','guest_id')->where('club_id','=', $club_id);
}
Team model
id => PRIMARY ; club_id => FOREIGN
In my controller all I want to do is Club::findOrFail($id)->games()
Executing the code above returns a SQL error that the games table does not have a column named club_id.
What is the correct way to create this kind of relation?
Thanks!
EDIT
Thanks to Nikola Gavric I've found a way to get all Games - but only where club teams are the home or away team.
Here is the relation:
public function games()
{
return $this->hasManyThrough('App\Models\Game','App\Models\Team','club_id','home_id');
}
How is it possible to get the games where the home_id OR the guest_id matches a team of the club? The last parameter in this function does not allow an array.
There is method to retrieve a "distant relationship with an intermediary" and it is called Has Many Through.
There is also a concrete example on how to use it which includes Post, Country and User, but I think it will be sufficient to give you a hint on how to create games relationship inside of a Club model. Here is a link, but when you open it, search for hasManyThrough keyword and you will see an example.
P.S: With right keys naming you could achieve it with:
public function games()
{
return $this->hasManyThrough('App\Models\Games', 'App\Models\Teams');
}
EDIT #01
Since you have 2 types of teams, you can create 2 different relationships where each relationship will get you one of the type you need. Like this:
public function gamesAsHome()
{
return $this
->hasManyThrough('App\Models\Games', 'App\Models\Teams', 'club_id', 'home_id');
}
public function gamesAsGuests()
{
return $this
->hasManyThrough('App\Models\Games', 'App\Models\Teams', 'club_id', 'guest_id');
}
EDIT #02
Merging Relationships: To merge these 2 relationships, you can use merge() method on the Collection instance, what it will do is, it will append all the records from second collection into the first one.
$gamesHome = $model->gamesAsHome;
$gamesGuests = $model->gamesAsGuests;
$games = $gamesHome->merge($gamesGuests);
return $games->unique()->all();
Thanks to #HCK for pointing out that you might have duplicates after the merge and that unique() is required to get the unique games after the merge.
EDIT #03
sortBy also offers a callable instead of a attribute name in cases where Collection contains numerical indexing. You can sort your Collection like this:
$merged->sortBy(function($game, $key) {
return $game->created_at;
});
When you define that Club hasMany games you are indicating that game has a foreign key called club_id pointing to Club. belongsTo is the same but in the other way. These need to be coherent with what you have on your database, that means that you need to have defined those keys as foreign keys on your tables.
Try this...
Club model
public function games()
{
return $this->hasMany('App\Models\Games');
}
Game model
public function homeTeam()
{
return $this->belongsTo('App\Models\Team','home_id');
}
public function guestTeam()
{
return $this->belongsTo('App\Models\Team','guest_id');
}
Your Query like
Club::where('id',$id)->has('games.guestTeam')->get();

Laravel Relationship that returns just one of a belongsToMany

I have a two Models: a Deal is started on one system by customers, and a Sale is started on another system by a salesman. I have an intermediate table dms_matches that forms a many-to-many relationship between those models, and includes a confidence score.
Customers can start many deals, the confidence score measures how likely the customer Deal is the exact one that initiated the final Sale transaction. (e.g., one system says first name is "Joe" and the other says "Joseph", or customer got upsold so Sale contains a related but different product than Deal)
I have a working many-to-many relationship on Sale to return all matched Deals:
public function deals()
{
return $this->belongsToMany('App\Models\Deal', 'dms_matches', 'dms_sale_id')
->orderBy('dms_matches.confidence', 'DESC')
->withPivot('confidence');
}
Now I would like a relationship that returns the exactly one Deal with the highest confidence score. (I want a relationship so I can get eager loading and caching.)
This works, but returns a collection of length one:
public function best_deal()
{
return $this->deals()->limit(1);
}
This works, but isn't a relationship, so it doesn't work with eager loading.
public function getBestDealAttribute()
{
return $this->deals()->first();
}
How can I build something that behaves like a hasOne or belongsTo relationship, but uses an intermediate table?

Laravel / Eloquent - custom relation method

I have a class Report which has a belongsToMany relation to Metric. Report also additionally has a belongsTo relation to Metric.
Normally, the model returned by the belongsTo relation is the same as one of the models in the belongsToMany relation. When this is true I'd like it to be the case that each of the two relations actually looks at the same object instance (this also saves an extra trip to the db).
So, in basic terms - is there a way to get one relation to check another first, to see if a model has already been loaded, and if so, point to that object rather than creating a new one.
I tried putting some code in the belongsTo relation method for Metric but I can't get round the fact it needs to return an instance of belongsTo, which needs various things passed as constructor arguments (ie. a query object), which aren't relevant in that case that the model has already been loaded in the belongsToMany relation.
I thought of ditching the belongsTo relation and adding data horizontally in the pivot table for the belongsToMany relation, but it isn't a many-to-many relation required so that seems a bit wrong.
Thanks!
Geoff
The idea here is to write a function which would check if a relationship is loaded and return that relationship, otherwise it will return the belongsToMany. This would go in your Report class. This is also for Laravel 5. If you have 4, just remove the namespaces from the model names.
public function metric()
{
return $this->belongsTo('App\Metric');
}
public function metrics()
{
return $this->belongsToMany('App\Metric');
}
public function getMetric()
{
if(array_key_exists('metric', $this->getRelations())) {
return $this->metric;
}
return $this->metrics()->first();
}
If you do decide to just go with a belongsToMany only, I'd suggest putting a unique key on your pivot table for both ID's to keep from getting any duplicates in the pivot table.

Resources