Laravel hasOne()->orderBy() vs hasOne()->ofMany() - laravel

One bit of "magic" in the Laravel codebase is here in HasOneOrMany.php where if the underlying query behind a hasOne relationship technically fetches multiple rows, it will just return the first row instead.
protected function getRelationValue(array $dictionary, $key, $type)
{
$value = $dictionary[$key];
return $type === 'one' ? reset($value) : $this->related->newCollection($value);
}
This got me thinking that, given a related model that is normally hasMany, an oldestOfMany() relation can be simulated with just an orderBy, e.g.
public function models()
{
return $this->hasMany(Model::class);
}
public function firstModel()
{
return $this->hasOne(Model::class)->orderBy('created_at');
}
Are there any downsides to this shortcut approach?
As pointed out in the comments, calling ->first() on the many relationship works just fine for actually retrieving a model. In our app it's common to build relations from other relations, and I want to do that here. e.g.
public function firstActiveModel()
{
return $this->firstModel()->active();
}

Related

Laravel Model request with several joins

While I am able to make simple requests with Model, I can't say the same for more complicated ones.
I know I don't necessarily have to use Model and can use DB facade but still, I want to know how it's supposed to be done.
Here's a request I made using DB :
DB::table('relationships')
->Join('users','users.id','=','relationships.user_id')
->Join('roles','roles.id','=','relationships.role_id')
->Join('bundles','bundles.id','=','relationships.related_id')
->Join('pools','bundles.id','=','pools.bundle_id')
->whereIn('pools.name',$pools)
->whereIn('roles.name',$roles)
->select('users.first_name','users.last_name','users.mail_address','roles.name AS role_name','bundles.name AS bundle_name', 'pools.name AS pool_name')
->get();
On a first attempt, I tried this:
User::whereHas('relationships', function($req) use($roles) {
$req->whereHas('bundle', function($req){
$req->whereIn('name', $pools);
});
$req->whereHas('role', function ($req){
$req->whereIn('name', $roles);
});
})
->with('relationships', 'relationships.role:id,name', 'relationships.bundle:id,name')
->get();
}
Problem is, using "with" just select everything unconditionally, ignoring previous tests you made earlier (whereHas, whereIn).
So I'd have to again filter on each table in the with statement.
Then I ended up doing this:
$pools = request()->input('pools.*.name');
return $prepReq = User::whereHas('relationships', function($req) use($pools, $roles) {
$req->whereHas('bundle', function($req) use ($pools){
$req->whereHas('pools', function($req) use ($pools){
$req->whereIn('name', $pools);
});
});
$req->whereHas('role', function ($req){
$req->whereIn('name', $roles);
});
})
->with(['relationships' => function ($query) use($pools, $roles){
$query->whereHas('role', function ($query){
$query->whereIn('name', $roles)
->select('id','name');
})->select('id','name');
}])
->get(['id', 'first_name', 'last_name', 'mail_address']);
Then I got lost into this and gave up.
Another thing that made me sweat is that when you go nested using "with", you can select columns only on the last table.
For example: "relations.bundle.pools" => I can select columns on pools but not on relationships or bundles, does that mean i have to imbricate with statements for each table ?
As you can see, I am a bit clueless on how things are supposed to be done
I would like any advice or help regarding this matter
Thanks in advance for your time
When using Laravel, you should be setting up the eloquent relationships for each model
Based on your select statement from above, and assuming you want to get the user, bundle, role, and pool, then I would do the following, may be off depending on how your actual DB and models are set up
// Relationship.php
public function user()
{
return $this->belongsTo(User::class);
}
public function role()
{
return $this->belongsTo(Role::class);
}
public function bundle()
{
return $this->belongsTo(Bundle::class, 'id', 'related_id');
}
// Bundle.php
public function relationship()
{
return $this->hasOne(Relationship::class, 'related_id');
}
// Role.php
public function relationship()
{
return $this->hasOne(Relationship::class);
}
// Pool.php
public function bundle()
{
return $this->belongsTo(Bundle::class);
}
Then you could do something like
Pool::with('bundle.relationship.user')->whereIn('name', $pools);
Role::with('relationship.user')->whereIn('name', $roles);

Best way to query model relationship in Eloquent

I am working within a controller in a Laravel application. I am returning a table to the view. The table is based on my PlanSubmission model. I am receiving parameters through a GET request and using those parameters to return a filtered set of rows to my view.
The first part of my controller looks like this and is working fine:
public function index()
{
//Used for filter. The request is received in the URL
if (request()->has('status')) {
$plans = PlanSubmission::where('status', request('status'))->paginate(25)->appends('status', request('status'));
}
elseif (request('employer_name')) {
$plans = PlanSubmission::where('employer_name', request('employer_name'))->paginate(25)->appends('employer_name', request('employer_name'));
}
I have run into a problem because now I need to use a model relationship in the controller. I am receiving 'advisor_name' from the request. The 'advisor_id" column is the foreign key on the PlanSubmission model. The 'advisor_name' column exists in the Advisor model. I have a function on my PlanSubmission model that looks like this:
public function advisor()
{
return $this->belongsTo(Advisor::class);
}
Initially, I thought there was a way I could do this easily with something like:
$plans = PlanSubmission::where(advisor->name, request('advisor_name'))->paginate(25)->appends('advisor_name', request('advisor_name'));
Of course, this will not work because I cannot enter a relationship into the first parameter in the Where Clause.
I do not know where to go from here. My other thought is to return all the advisors first from the Advisor model like this:
$advisors = Advisor::where('name', request('advisor_name'));
Then, I imagine I would have to somehow loop through that and get the id (primary key) for each of the objects in $advisors and somehow get that into the PlanSubmission where clause. I'm totally lost.
Like Victor mentions in his answer you can use whereHas like so:
PlanSubmission::whereHas('advisor', function ($query) {
$query->where('name', request('advisor_name'));
});
You didn't asked this directly, but I noticed that you use conditionals to make different queries. Eloquent provides a few way to make this a bit nicer to deal with.
The first which is kind of obvious is that that whatever method you call a builder (query) is returned that you can just add on to. It could be there were some common restrictions in your two cases:
public function index()
{
$query = PlanSubmission::where('something', 42);
if (request()->has('status')) {
$query = $query->where('status', request('status'));
} elseif (..) {
...
}
return $query->paginate(25);
}
Another way to do conditional queries in Laravel is using when. E.g. for status:
$query = $query->when(request->has('status'), function ($query) {
// note that you don't have to return the query
$query->where('status', request('status'));
});
// or PlanSubmission::>when(..)
In your example you cannot both filter by status AND advisor_name, but lets assume that would be okay, then you can combine everything like so:
public function index()
{
return PlanSubmission::query()
//->where('something', 42)
->when(request->has('status'), function ($query) {
$query->where('status', request('status'));
})
->when(request->has('advisor_name'), function ($query) {
$query->whereHas('advisor', function ($query) {
$query->where('name', request('advisor_name'));
});
})->paginate(25);
}
This approach may seem verbose for simple queries and then it is fine to use if conditions, but for complex queries when can be useful. Also the idea of "building up a query" also works nice in those situation. You can pass the query builder around and continuously build it up.
You can use whereHas for that
docs

How to retrieve multiple relations with multiple tables in laravel eloquent

I'm using Laravel 5.8 to build a babysitting site. I have 4 tables with different relationships as below:
please see this image
The relationships are:
Babysitter->hasMany(session)
Sessions->hasOne(Review)
Sessions->hasOne(Kids)
Sessions->hasOne(Babysitter)
Sessions->hasOne(Parent)
I want to achieve 2 things:
First one
I want to show this result when listing all babysitters. I'm showing this information for each babysitter:
plsease see this image
See here what I couldn't achieve
plsease see this image
This is my code
Sitters::where('Status', 'active')->where('Verified', 1)->get();
Second one
Also, I've tried to show kids name with parent review as shown here:
plsease see this image
This is what i'm using
Sessions::select('Reviews.*', 'Sessions.Parent_id')->join('Reviews', 'Reviews.Session_id', '=', 'Sessions.id')->with('owner')->where('Trainer_id', session('user')->Id)->where('Status', '=', 'complete')->with('owner')->orderBy('Sessions.id', 'DESC')->get();
Here is Session.php Model
public function owner(){
return $this->belongsTo('App\Models\Parents', 'Parent_id');
}
As discussed change the relations:
Babysitter->hasMany(sesstion)
Sessions->hasOne(Review)
Sessions->belongsTo(Kids)
Sessions->belongsTo(Babysitter)
Sessions->belongsTo(Parent)
First one
in Babysitter.php declare the following attributes
class Babysitter extends Model
{
public function reviews()
{
$this->hasManyThrough(Review::class, Session::class);
}
public function getAverageReviewAttribute()
{
return $this->reviews()->avg('Rating');
}
}
Then you just need to call it on the model instance.
$babysitter = Babysitter::first();
return $babysitter->average_review;
Second one
Just use the relation
$babysitter = BabySitter::with(['sessions' => public function ($session) {
$session->with(['review','parent','kids']);
})->where('trainer_id', '=', session('user')->Id) //did not understand this condition
->first();
This assumes you have parent, kids and review relation declared on Session::class. (change the names if needed)
After a few days of searching & testing, this is what worked for me:
Inside (Sitters) Model, put this relation
public function sessions()
{
return $this->hasMany(Sessions::class, 'sitter_id')
->withCount('reviews')
->withCount(['reviews as review_avg' => function($query){
$query->select(DB::raw('AVG(Rating)'));
}]);
}
Also, inside (Sessions) Model, put this relation
public function reviews()
{
return $this->hasOne(Reviews::class, 'Session_id');
}
Now you query like this
return $sitters = Sitters::with('sessions')->get();
I hope this can help someone :)

Laravel Eloquent Sort By Relationship Column

I have several relationships, one specifically that I would like to use for ordering a list, but I can't seem to find the right way to do it.
Below are my relationships:
public function date(){
return $this->hasOne(agent_billings_dates::class,'id','dateID');
}
public function carrier(){
return $this->hasOne(customer::class,'id','carrierID');
}
As well as two attributes which I have added as appends:
public function getItemCountAttribute(){
return $this->items->count();
}
public function getItemMissingAttribute(){
return $this->itemIssues->count();
}
public function getBatchSumAttribute(){
return $this->items->sum('amount');
These show up all fine when I have the following in my function:
$batches = agent_billings_batches::with(['date','carrier','carrier.carrierDetails'])
->where('status',$request->status)
->get();
But the attributes and the with's fall off when I do this (however the date is sorted appropriately):
$batches = agent_billings_batches::with(['carrier','carrier.carrierDetails'])
->join('agent_billings_dates', 'agent_billings_dates.id', '=', 'agent_billings_batches.dateID')
->orderBy('agent_billings_dates.date','desc')
->where('status',$request->status)
->get();
Am I doing something wrong? I'd appreciate any help anyone could give.
Thanks!
Eloquent does not use Joins when loading relationships. It loads them in a separate query, therefore you cannot order the main result using a relationship at query time, you need to do it after the data is collected:
$batches = agent_billings_batches::with(['date','carrier','carrier.carrierDetails'])
->where('status',$request->status)
->get()
->sortBy(function ($batch) {
return $batch->date->date;
});

Laravel oneToMany accessor usage in eloquent and datatables

On my User model I have the following:
public function isOnline()
{
return $this->hasMany('App\Accounting', 'userid')->select('rtype')->latest('ts');
}
The accounting table has activity records and I'd like this to return the latest value for field 'rtype' for a userid when used.
In my controller I am doing the following:
$builder = App\User::query()
->select(...fields I want...)
->with('isOnline')
->ofType($realm);
return $datatables->eloquent($builder)
->addColumn('info', function ($user) {
return $user->isOnline;
}
})
However I don't get the value of 'rtype' for the users in the table and no errors.
It looks like you're not defining your relationship correctly. Your isOnline method creates a HasMany relation but runs the select method and then the latest method on it, which will end up returning a Builder object.
The correct approach is to only return the HasMany object from your method and it will be treated as a relation.
public function accounts()
{
return $this->hasMany('App\Accounting', 'userid');
}
Then if you want an isOnline helper method in your App\User class you can add one like this:
public function isOnline()
{
// This gives you a collection of \App\Accounting objects
$usersAccounts = $this->accounts;
// Do something with the user's accounts, e.g. grab the last "account"
$lastAccount = $usersAccounts->last();
if ($lastAccount) {
// If we found an account, return the rtype column
return $lastAccount->rtype;
}
// Return something else
return false;
}
Then in your controller you can eager load the relationship:
$users = User::with('accounts')->get(['field_one', 'field_two]);
Then you can do whatever you want with each App\User object, such as calling the isOnline method.
Edit
After some further digging, it seems to be the select on your relationship that is causing the problem. I did a similar thing in one of my own projects and found that no results were returned for my relation. Adding latest seemed to work alright though.
So you should remove the select part at very least in your relation definition. When you only want to retrieve certain fields when eager loading your relation you should be able to specify them when using with like this:
// Should bring back Accounting instances ONLY with rtype field present
User::with('accounts:rtype');
This is the case for Laravel 5.5 at least, I am not sure about previous versions. See here for more information, under the heading labelled Eager Loading Specific Columns
Thanks Jonathon
USER MODEL
public function accounting()
{
return $this->hasMany('App\Accounting', 'userid', 'userid');
}
public function isOnline()
{
$rtype = $this->accounting()
->latest('ts')
->limit(1)
->pluck('rtype')
->first();
if ($rtype == 'Alive') {
return true;
}
return false;
}
CONTROLLER
$builder = App\User::with('accounting:rtype')->ofType($filterRealm);
return $datatables->eloquent($builder)
->addColumn('info', function (App\User $user) {
/*
THIS HAS BEEN SUCCINCTLY TRIMMED TO BE AS RELEVANT AS POSSIBLE.
ARRAY IS USED AS OTHER VALUES ARE ADDED, JUST NOT SHOWN HERE
*/
$info[];
if ($user->isOnline()) {
$info[] = 'Online';
} else {
$info[] = 'Offline';
}
return implode(' ', $info);
})->make();

Resources