How to compare related count with own column in Laravel Eloquent? - php

Assume we have an agents table with a quota column and a many-to-many relationship to tickets. With Laravel Eloquent ORM, how can I select only agents having less or equal number of 'tickets' than their 'quota'?
Eager-loading objects must be avoided.
class Agent extends Model {
public function tickets()
{
return $this->belongsToMany(Ticket::class, 'agent_tickets')->using(AgentTicket::class);
}
public function scopeQuotaReached($query)
{
// Does not work. withCount is an aggregate.
return $query->withCount('tickets')->where('tickets_count', '<=', 'quota');
// Does not work. Tries to compare against the string "quota".
return $query->has('tickets', '<=', 'quota');
}
}
Is there a more eloquent (pun intended) way to solve this than using a DB::raw() query with joining and grouping and counting manually?
EDIT
Works:
$query->withCount('tickets')->having('tickets_count', '<=', DB::raw('quota'))->get();
Works:
$query->withCount('tickets')->having('tickets_count', '<=', DB::raw('quota'))->exists();
Breaks: (throws)
$query->withCount('tickets')->having('tickets_count', '<=', DB::raw('quota'))->count();
RELATED
https://github.com/laravel/framework/issues/14492
Issue is closed, links to #9307, I have posted there. Will follow up.

Derived columns like tickets_count can only be accessed in the HAVING clause.
Since there is no havingColumn() method, you'll have to use a raw expression:
$query->withCount('tickets')->having('tickets_count', '<=', DB::raw('quota'));

At a database level I don't know how to achieve this, but you could do it at a Collection level.
// Get users
$agents = Agent::withCount('tickets')->get();
// filter
$good_agents = $agents->filter(function ($agent, $key) {
return $agent->tickets_count >= $agent->quota;
})
->all();
Of course you can inline it:
$good_agents = Agent
::withCount('tickets')
->get()
->filter(function ($agent, $key) {
return $agent->tickets_count >= $agent->quota;
})
->all();

Related

whereIn function within a sub query within eloquent doesnt filter any records

I have a Prize, Ticket and User model. A prize can have many tickets, and a ticket can only be associated to one User.
Each Prize will have one Winning Ticket, what I am trying to do is list all my Users that have a winning Ticket like so:
$winning_tickets = Prize::WinnerSelected()->get('ticket_winner_id')->pluck('ticket_winner_id');
$users = User::with(['tickets' => function($query) use ($winning_tickets) {
$query->whereIn('id', $winning_tickets);
}])->get();
$winning_tickets returns an array of winning ticket ids, but the $users collection returns ALL my users, even users that have no ticket records.
Can anyone explain what I am doing wrong?
with() doesn't actually filter the User Collection being returned. To do that, you need to use whereHas():
$winningTickets = Prize::WinnerSelected()->get('ticket_winner_id')->pluck('ticket_winner_id');
$users = User::whereHas('tickets', function($query) use ($winningTickets) {
$query->whereIn('id', $winningTickets);
})->get();
Now, the $users Collection will only contain User records that have one or more Ticket records matching the given ticket_winner_id in $winning_tickets.
If you need to, you can use both with() and whereHas() to filter and eager load the associated Ticket records:
$winningTickets = Prize::WinnerSelected()->get('ticket_winner_id')->pluck('ticket_winner_id');
$filterClause = function ($query) use ($winningTickets) {
return $query->whereIn('id', $winningTickets);
};
$users = User::with(['tickets' => $filterClause])
->whereHas('tickets', $filterClause)
->get();
Define the the function ($query) as a reusable clause to avoid repetition, and voila!
Sidenote, you don't need to chain ->get() into ->pluck(); both Builder and Collection classes have a ->pluck() method, so this is valid:
$winningTickets = Prize::WinnerSelected()->pluck('ticket_winner_id');

Dynamic Property call giving error (Property [game] does not exist on the Eloquent builder instance.)

I have two models: Game and Game_Assignment. Game_Assignment tells whose job it is to play a game.
I am trying to count the number of Game_Assignment's that a user has their id on that also have a specific value on the Game model that it relates to. I'll just get into the Models/the code
Game Model Relationship:
public function assignments() {
return $this->hasMany('App\Models\Game_Assignment', 'game_id');
}
Game_Assignment Relationship:
public function game() {
return $this->belongsTo('App\Models\Game', 'game_id');
}
Where things are going wrong (in a queue job, if that makes a difference)
$gamesDue = Game_Assignment::where('statistician_id', $statistician->id)->game->where('stats_done', '!=', 'yes')->count();
I have also tried the following two things, neither worked:
$gamesDue = Game_Assignment::where('statistician_id', $statistician->id)->game()->where('stats_done', '!=', 'yes')->count();
and...
$gamesDue = Game_Assignment::where('statistician_id', $defaultStatistician->id)->with(['games' => function($query) {
$query->where('stats_done', '!=', 'yes');
}])->count();
None of these work, and the first one I showed threw an error:
Property [game] does not exist on the Eloquent builder instance.
Anyone have an idea of where I am going wrong? I am using this link as my reference https://laravel.com/docs/5.8/eloquent-relationships#eager-loading
When using the query builder of your Game_Assignment model, you cannot simply switch context to the query builder of Game. You can only call ->game() or ->game after you retrieved one or many model instances of Game_Assignment with first() or get().
So, in your particular case, you were looking for whereHas('game', $callback) (where $callback is a function that applies constraints on the foreign table) in order to add a constraint on the foreign table:
use Illuminate\Database\Eloquent\Builder;
$gamesDue = Game_Assignment::query()
->where('statistician_id', $statistician->id)
->whereHas('game', function (Builder $query) {
$query->where('stats_done', '!=', 'yes');
})
->count();
Side note: a column (stats_done) that seems to hold a boolean value (yes/no) should be of boolean type and not string/varchar.

laravel eloquent sort by relationship

I have 3 models
User
Channel
Reply
model relations
user have belongsToMany('App\Channel');
channel have hasMany('App\Reply', 'channel_id', 'id')->oldest();
let's say i have 2 channels
- channel-1
- channel-2
channel-2 has latest replies than channel-1
now, i want to order the user's channel by its channel's current reply.
just like some chat application.
how can i order the user's channel just like this?
channel-2
channel-1
i already tried some codes. but nothing happen
// User Model
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved')
->with(['replies'])
->orderBy('replies.created_at'); // error
}
// also
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved')
->with(['replies' => function($qry) {
$qry->latest();
}]);
}
// but i did not get the expected result
EDIT
also, i tried this. yes i did get the expected result but it would not load all channel if there's no reply.
public function channels()
{
return $this->belongsToMany('App\Channel')
->withPivot('is_approved')
->join('replies', 'replies.channel_id', '=', 'channels.id')
->groupBy('replies.channel_id')
->orderBy('replies.created_at', 'ASC');
}
EDIT:
According to my knowledge, eager load with method run 2nd query. That's why you can't achieve what you want with eager loading with method.
I think use join method in combination with relationship method is the solution. The following solution is fully tested and work well.
// In User Model
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved');
}
public function sortedChannels($orderBy)
{
return $this->channels()
->join('replies', 'replies.channel_id', '=', 'channel.id')
->orderBy('replies.created_at', $orderBy)
->get();
}
Then you can call $user->sortedChannels('desc') to get the list of channels order by replies created_at attribute.
For condition like channels (which may or may not have replies), just use leftJoin method.
public function sortedChannels($orderBy)
{
return $this->channels()
->leftJoin('replies', 'channel.id', '=', 'replies.channel_id')
->orderBy('replies.created_at', $orderBy)
->get();
}
Edit:
If you want to add groupBy method to the query, you have to pay special attention to your orderBy clause. Because in Sql nature, Group By clause run first before Order By clause. See detail this problem at this stackoverflow question.
So if you add groupBy method, you have to use orderByRaw method and should be implemented like the following.
return $this->channels()
->leftJoin('replies', 'channels.id', '=', 'replies.channel_id')
->groupBy(['channels.id'])
->orderByRaw('max(replies.created_at) desc')
->get();
Inside your channel class you need to create this hasOne relation (you channel hasMany replies, but it hasOne latest reply):
public function latestReply()
{
return $this->hasOne(\App\Reply)->latest();
}
You can now get all channels ordered by latest reply like this:
Channel::with('latestReply')->get()->sortByDesc('latestReply.created_at');
To get all channels from the user ordered by latest reply you would need that method:
public function getChannelsOrderdByLatestReply()
{
return $this->channels()->with('latestReply')->get()->sortByDesc('latestReply.created_at');
}
where channels() is given by:
public function channels()
{
return $this->belongsToMany('App\Channel');
}
Firstly, you don't have to specify the name of the pivot table if you follow Laravel's naming convention so your code looks a bit cleaner:
public function channels()
{
return $this->belongsToMany('App\Channel') ...
Secondly, you'd have to call join explicitly to achieve the result in one query:
public function channels()
{
return $this->belongsToMany(Channel::class) // a bit more clean
->withPivot('is_approved')
->leftJoin('replies', 'replies.channel_id', '=', 'channels.id') // channels.id
->groupBy('replies.channel_id')
->orderBy('replies.created_at', 'desc');
}
If you have a hasOne() relationship, you can sort all the records by doing:
$results = Channel::with('reply')
->join('replies', 'channels.replay_id', '=', 'replies.id')
->orderBy('replies.created_at', 'desc')
->paginate(10);
This sorts all the channels records by the newest replies (assuming you have only one reply per channel.) This is not your case, but someone may be looking for something like this (as I was.)

Eloquent - where has pivot users orWhere has none

I'm trying to write a function that will get all "buckets" that are assigned to the auth'd user and/or buckets that have NO USERS assigned.
Relations and such, work as they should. Unless I'm missing something?
How can I get all buckets user is assigned too - and also include buckets where no users (including the auth user) are assigned.
Buckets user is assigned to
Buckets where NO users have been assigned. i.e. pivot table contains no rows for bucket, etc.
My issue very likely stems from the orWhere query...
$buckets = Team::currentTeam()->buckets()->with('user')->whereHas('user', function($query) {
$query->where('user_id', Auth::user()->id)
->orWhere('user_id', function() {
$query->count();
}, '<', 0);
})->get();
Didn't tested this but I think this should work. You're looking to remove that orWhere query and add orHas('user', '=', 0).
$buckets = Team::currentTeam()->buckets()->with('user')->whereHas('user', function($query) {
$query->where('user_id', Auth::user()->id);
})->orHas('user', '=', 0)->get();
Another possible solution you might consider is using a left join.
Heads-up: this might not be accurate as I don't know your db schema.
Team::currentTeam()
->buckets()
->leftJoin('users', 'users.bucket_id', '=', 'buckets.id')
->where(function($query) {
$query->where('users.id', $user_id)
->orWhereNull('user.id');
});
Please also check this blog post
AND-OR-AND + brackets with Eloquent
https://laraveldaily.com/and-or-and-brackets-with-eloquent/

Laravel Multiple Models Eloquent Relationships Setup?

I have 3 models
User
Pick
Schedule
I'm trying to do something like the following
$picksWhereGameStarted = User::find($user->id)
->picks()
->where('week', $currentWeek)
->first()
->schedule()
->where('gameTime', '<', Carbon::now())
->get();
This code only returns one array inside a collection. I want it to return more than 1 array if there is more than 1 result.
Can I substitute ->first() with something else that will allow me to to return more than 1 results.
If not how can I set up my models relationship to allow this to work.
My models are currently set up as follow.
User model
public function picks()
{
return $this->hasMany('App\Pick');
}
Schedule model
public function picks()
{
return $this->hasMany('App\Pick');
}
Pick model
public function user()
{
return $this->belongsTo('App\User');
}
public function schedule()
{
return $this->belongsTo('App\Schedule');
}
Since you already have a User model (you used it inside you find method as $user->id), you can just load its Pick relationship and load those Picks' Schedule as follows:
EDIT:
Assuming you have a schedules table and your picks table has a schedule_id column. Try this.
$user->load(['picks' => function ($q) use ($currentWeek) {
$q->join('schedules', 'picks.schedule_id', '=', 'schedules.id')
->where('schedules.gameTime', '<', Carbon::now()) // or Carbon::now()->format('Y-m-d'). See what works.
->where('picks.week', $currentWeek);
}])->load('picks.schedule');
EDIT: The code above should return the user's picks which have a schedules.gameTime < Carbon::now()
Try it and do a dump of the $user object to see the loaded relationships. That's the Eloquent way you want.
Tip: you may want to do $user->toArray() before you dump $user to see the data better.
EDIT:
The loaded picks will be in a form of Collections so you'll have to access it using a loop. Try the following:
foreach ($user->picks as $pick) {
echo $pick->schedule->gameTime;
}
If you only want the first pick from the user you can do: $user->picks->first()->schedule->gameTime
I think a foreach loop may be what you're looking for:
$picks = User::find($user->id)->picks()->where('week', $currentWeek);
foreach ($picks as $pick){
$pickWhereGameStarted = $pick->schedule()->where('gameTime', '<', Carbon::now())->get();
}
Try this and see if it's working for you

Categories