Laravel Eloquent: ordering Many-to-Many query- user favorites first - php

My Laravel app returns a list of ships for users to manage. I want to return ships that are "favorited" by the active user before the rest of the ships. eg:
Best ship (*)
Ship I manage often (*)
Another ship
Beta ship
Cornelia
I have a many-to-many relationship between User and Ship, where ship->userFavorite returns the users who have "favorited" the ship.
I can imagine 2 solutions, but I'm not sure if they are possible and how to implement them if they are.
An orderby query that sorts based on favorite being equal to active user
$ships = Ship::orderby([ship->userfavorite being equal to Auth::User()], 'ASC')
->orderby('name', 'ASC')->get();
My current solution: I use a wherehas query to return the favorited ships first ($favships), and use another query to return all the ships ($ships). For this solution I would like to remove the favoried ships from the second query. But how can I elegantly remove these ships from the results?
$user = Auth::user();
$favships = Ship::wherehas('userFavorite', function($q) use($user)
{
$q->where('id', $user->id);
})->orderBy('name', 'ASC')->get();
$ships = Ship::orderBy('name', 'ASC')->get();
Any help to increase my understanding of this problem would be greatly appreciated!

you can use
$normalShips= $ships->diff($favships);
But I think you can reduce from 2 queries to 1 query:
//Ship.php
public function currentUserFavorite() {
$this->userFavorite()->where('id', Auth::user()->id);
}
// get all ships
$ships = Ship::with('currentUserFavorite')->orderBy('name', 'ASC')->get();
// finally
$favships = $ships->where('currentUserFavorite.0.id', Auth::user()->id);
$normalShips = $ships->diff($favships);
// or do a loop
foreach ($ships as $ship) {
if ($ship->currentUserFavorite->count() > 0) {
$favships[] = $ship;
} else {
$normalShips[] = $ship;
}
}

Related

Laravel advanced query

Affiliate has many affiliatesHistory, affiliatesHistory belongs to affiliate, how to make the following query?
Take affiliates, where has affiliatesHistory, if affiliatesHistory records count is equal to 1, then do not take affiliatesHistory, which has status of uninstalled.
$affiliates = $user->affiliates()
->whereDoesntHave('affiliatesHistory', function ($q) {
$q->where('affiliates_histories.status', 'Installed earlier')
->orWhere('affiliates_histories.status', 'Uninstalled / Installed earlier');
The following query works, but I need to not take those affiliates, where affiliatesHistory count is equal to 1 and the status is uninstalled.
Any help will be appriaciated.
So, for what I understand you want to get the affiliates which affiliatesHistory status is Installed earlier. If this is the case then try this:
$user_affiliates = $user->affiliates();
$affiliates = $user_affiliates->whereHas('affiliatesHistory', function($q){
$q->where('status', 'Installed earlier');
})->get();
dd($affiliates);
For your case if there are more than one affiliatesHistory items then return else if there is only one affiliatesHistory then it should not contain Uninstalled status, I guess you can use conditional count to get desired results as
$affiliates = Affiliate::withCount([
'affiliatesHistory',
'affiliatesHistory as affiliatesHistoryUninstalled_count' => function ($query) {
$query->where('status', 'Uninstalled');
}
])->where('user_id', $user->id)
->havingRaw('affiliatesHistory_count > 1 OR (affiliatesHistory_count = 1 AND affiliatesHistoryUninstalled_count = 0)')
->get();

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

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();

Get relation for multiple objects with laravel

A User has many Phones. I want to get all the phones from active users. I can do:
$phones = [];
$users = User::with('phones')->where('active', 1)->get();
foreach($users as $user) {
$phones = array_merge($phones, $user->phones->toArray());
}
dd($phones); // <- Here are all the phones.
But I'm not sure it's the more elegant or laravel-ish way. Is there a built in magic function for such a case? Can I get all the phones from active users without writing a loop and insert in an Array?
You should use whereHas() method:
$phones = Phone::whereHas('user', function($q) {
$q->where('active', 1);
})->get();
You can also use whereExists, which can be more efficient if you're not using data from the users table:
Phone::whereExists(function($query) {
$query->select(\DB::raw('NULL'))
->from('users')
->whereRaw('users.id = phones.user_id AND users.active = 1')
})->get();

Laravel: ordering a many-many relation

I have a many-many relation between Ingredient and Recipe, with a pivot table (ingredient_recipe).
I'd like to get ingredients ordered by how many recipes have them. Example, if I use salt in 2 recipes and meat in 3 recipes, I'll have meat before salt.
This is what I have. It works but it doesn't order correctly, even though the resulting query executed directly on my DB works as expected, so Laravel is doing something internally, I guess.
//Ingredient model
public function recipesCount()
{
return $this->belongsToMany('Recipe')->selectRaw('count(ingredient_recipe.recipe_id) as aggregate')->orderBy('aggregate', 'desc')->groupBy('ingredient_recipe.ingredient_id');
}
public function getRecipesCountAttribute()
{
if ( ! array_key_exists('recipesCount', $this->relations)) $this->load('recipesCount');
$related = $this->getRelation('recipesCount')->first();
return ($related) ? $related->aggregate : 0;
}
//controller
$ingredients = Ingredient::with('recipesCount')->whereHas('recipes', function($q)
{
$q->where('user_id', Auth::id());
})->take(5)->get();
//outputting the last query here and executing it on my db returns correctly ordered results.
How can I fix it?
In order to order by related table you need join. There's no way to achieve that with eager loading whatsoever.
Ingredient::with('recipesCount')
->join('ingredient_recipe as ir', 'ir.ingredient_id', '=', 'ingredients.id')
->join('recipes as r', function ($j) {
$j->on('r.id', '=', 'ir.recipe_id')
->where('r.user_id', '=', Auth::id());
})
->orderByRaw('count(r.id) desc')
->groupBy('ingredients.id')
->take(5)
->get(['ingredients.*']);
There's no need for whereHas anymore, for inner joins will do the job for you.

Complex joins in Eloquent

I have a table of itineraries. An itinerary belongs to a customer and has multiple days. A package is assigned to each of these days. I want to be able to produce a manifest showing which customers are allocated to a package and on which days.
I'm struggling with Eloquent, because you can't do queries beyond a one-to-Many relationship
What i want to do is this:
return $this->package->where('PackageID, $id)->itineraryDay->itinerary->customer->select('CustomerID', 'Date')
But can only really achieve it using the query builder:
return DB::connection($this->connection)
->table('t_package as PA')
->join('t_itinerary_day_map as IDM', 'IDM.PackageID', '=', 'PA.PackageID')
->join('t_itinerary_day as ID', 'IDM.ItineraryDayID', '=', 'ID.ItineraryDayID')
->join('t_itinerary as IT', 'IT.ItineraryID', '=', 'ID.ItineraryID')
->join('t_customer as CC', 'CC.ItineraryID', '=', 'IT.ItineraryID')
->where('PA.PackageID', $id)
->select('CC.CustomerID', 'ID.Date')
->distinct()
->get();
I really want to use Eloquent as I hate hardcoding table names and i've already created relationships for these models, but can't see any way around it
I believe you could do something like this to find customers that have a package with the given ID:
$packageId = 42;
$customers = $customer->whereHas('packages', function($q) use($packageId){
return $q->where('package_id', $packageId);
})->get();
How would that work for what you want?
I'll have to make few assumptions on your relationship but it seems doable.
If one ItineraryDay belongs to one Itinerary. And one Itinerary belongs to one Customer. And one ItineraryDay may have more than one Package.
$packageID = 111;
$itineraryDays = ItineraryDay::with('itinerary.customer')
->whereHas('package', function($q) use($packageID) {
$q->where('PackageID', $packageID);
})
->get();
foreach($itineraryDays as $itineraryDay) {
var_dump($itineraryDay);
var_dump($itineraryDay->itinerary->customer);
}
I'm not sure if i get your relationship method naming correct, but hopefully this works.

Categories