Order by count in many to many polymorphic relation in Laravel - laravel

Let's take the example from the doc : https://laravel.com/docs/5.7/eloquent-relationships#many-to-many-polymorphic-relations it's easy to get all posts with their tags count doing Post::withCount('tags')->get().
But how to get all tags with their usage count ? To have them ordered by most used / less used.
If I do Tag::withCount(['video', 'post'])->get() I will have 2 attributes videos_count and posts_count. In my case I would like a unique taggables_count that will be the sum of the two. In a perfect world by adding a subselect querying the pivot table.

I would suggest simply doing the call you already did, which is Tag::withCount(['video', 'post'])->get(), and add this to your Tag model:
// Tag.php
class Tag
{
...
// Create an attribute that can be called using 'taggables_count'
public function getTaggablesCountAttribute()
{
return $this->videos_count + $this->posts_count;
}
...
}
and then in your loop (or however you use the items in the collection):
#foreach($tags as $tag)
{{ $tag->taggables_count }}
#endforeach
This setup requires you to get the Tags with the withCount['video', 'post'] though. If you do not, you will likely get 0in return for $tag->taggables_count.
If you're really concerned about speed, you would have to create the query manually and do the addition in there.

So after more searching I find out there is no way to do it with only in one query due to the fact that in mysql we can't do a select on subselet results. So doing Tag::withCount(['videos', 'posts']) and trying to sum in the query the videos_count and posts_count will not work. My best approach was to create a scope that read results in the pivot table :
public function scopeWithTaggablesCount($query) {
if (is_null($query->getQuery()->columns)) {
$query->select($query->getQuery()->from . '.*');
}
$query->selectSub(function ($query) {
$query->selectRaw('count(*)')
->from('taggables')
->whereColumn('taggables.tag_id', 'tags.id');
}, 'taggables_count');
return $query;
}
To use it :
$tags = Tag::withTaggablesCount()->orderBy('name', 'ASC')->get();
So now we have a taggables_count for each tag and it can be used to order by. Hope it can help others.

Related

Query Builder filter for multi level deep relationship in Laravel

I have a selection of plots which each belong to a development by a hasManyThrough relationship through housetypes. I want to filter these by development on their overview page. Plots has a housetype_id column and housetypes has a development_id column.
public function plots()
{
return $this->hasManyThrough(Plot::class, Housetype::class);
}
When I use my filter it returns the developments ID number as $development, I then need this to only show plots which are linked to that development.
I have looked into using whereHas or Join methods but have been unable to figure this out. Current filter scope is below. Thanks
public function scopeFilterDevelopment($query)
{
$development = request()->input('filter_development');
if ($development == "") {
return;
}
if(!empty($development)){
$query->where('development_id', $development);
}
}
If I can understand it right you wish to assert a condition on other Model, HasMany will load all the objects to the related model once the query is completed. Eloquent then binds the related model objects to each.
Try joins from Laravel instead. I feel this is what you exactly want: https://laravel.com/docs/5.8/queries#joins
I would use whereHas to filter the relationship:
YourModel::whereHas('plots', function($query) {
$query->filterDevelopment();
})->get();
I would also edit the query scope not to rely on the request global function, but instead pass the development of value as a parameter.
you have make a leftjon and then use when, you dont have to use
if(!empty($development)){
$query->where('development_id', $development);
}
this any more, you can use
->when($development=="" ? false : true, function($query) use ($development){
return $query->where('development_id', $development);
})
this is a full example
$queryBuilder = DB::table('facturas')->
leftJoin('clientes','clientes.id','=','facturas.clientes_id')->
select('facturas.estados_id as estado','facturas.numero as
numero',DB::raw('concat(clientes.nombre," ",clientes.apellido) as cliente'))->
when($estados===null ? false: true,function($query) use ($estados){
return $query->whereIn('facturas.estados_id', $estados);
})
It was a whereHas that solved this in the end! (another developer at work walked me through this)
Relationship -
public function housetype()
{
return $this->belongsTo(Housetype::class);
}
Function -
public function scopeFilterDevelopment($query)
{
if (request()->input('filter_development') == "") {
return;
}else{
$query->whereHas('housetype', function($housetype){
$housetype->where('development_id', request()->input('filter_development'));
});
}
}
This then returns any plot where its housetype has a matching development_id for the filter_development from the request.
Thanks for everyone's input

Laravel / Eloquent: Is it possible to select all child model data without setting a parent?

I have various parent/child relationships, drilling down a few levels. What I want to know is if its possible to do something like this:
$student = Student::find(1);
$student->bursaries()->enrolments()->courses()->where('course','LIKE','%B%');
(With the end goal of selecting the course which is like '%B%'), or if I would have to instead use the DB Query builder with joins?
Models / Relationships
Student:
public function bursaries() {
return $this->hasMany('App\StudentBursary');
}
StudentBursary:
public function enrolments() {
return $this->hasMany('App\StudentBursaryEnrolment');
}
If what you want is to query all courses, from all enrollments, from all bursaries, from a students, then, unfortunately, you are one table too many from getting by with the Has Many Through relationship, because it supports only 3 tables.
Online, you'll find packages that you can import / or answers that you can follow to provide you more though of solutions, for example:
1) How to use Laravel's hasManyThrough across 4 tables
2) https://github.com/staudenmeir/eloquent-has-many-deep
Anyhow, bellow's something you can do to achieve that with Laravel alone:
// Eager loads bursaries, enrolments and courses, but, condition only courses.
$student = Student::with(['bursaries.enrolments.courses' => function($query) {
$query->where('course','LIKE','%B%');
}])->find(1);
$enrolments = collect();
foreach($student->bursaries as $bursary) {
$enrolments = $enrolments->merge($bursary->enrolments);
}
$courses = collect();
foreach ($enrolments as $enrolment) {
$courses = $courses->merge($enrolment->courses);
}
When you do $student->bursaries() instead of $student->bursaries, it returns a query builder instead of relationship map. So to go to enrolments() from bursaries() you need to do a bursaries()->get(). It should look like this.
$student->bursaries()->get()[0]->enrolments(), added the [0] because im using get(), you can use first() to avoid the [0]
$student->bursaries()->first()->enrolments()
But I'm not sure if it will suffice your requirement or not.

Laravel eloquent query with sum of related table

I have a table users and posts with columns user_id and post_views.
In post_views I keep information how many times post was display.
And now, in query I would like to get user with sum of post_views all his posts.
I tried do something like this:
User::where(['id'=>$id])->with('posts')->get();
And in model I defined:
public function posts()
{
return $this->hasMany('App\Models\Post')->sum('post_views','AS','totalViews');
}
But without success.
How to do it?
Thank you
You can use a modified withCount():
public function posts()
{
return $this->hasMany('App\Models\Post');
}
$user = User::withCount(['posts as post_views' => function($query) {
$query->select(DB::raw('sum(post_views)'));
}])->find($id);
// $user->post_views
You can use
User::withCount('posts')->find($id)
to get the user with the id $id and a posts_count attribute in the response
I'm not fully sure what the intention of ->sum('game_plays','AS','totalVies'); is - you would need to add more context if you want this
Just something to add with regards to your shown code: No need to query by id using where + the get() at the end will make you query for a collection. If you want to get a single result use find when searching by id
As always laravel has a method for that : withSum (Since Laravel v8)
Note : I know that at the time of the message was posted, the method did not exist, but since I came across this page when I was looking for the same result, I though it might be interesting to share.
https://laravel.com/docs/9.x/eloquent-relationships#other-aggregate-functions
In your case it should be :
$user = User::withSum('posts as total_views', 'post_views')->find($id);
Then you can access to the result :
$user->total_views

Putting eloquent results in another table to doing where queries in Laravel 5.4

For some special reasons I used append attributes in my model and now when I want to do where queries on custom attributes, for example "category", I face an error with this meaning that eloquent could not found column with "category" name!
To solve this problem I guess if I put my query's result into a temp table, I could do what I want!
Have someone any Idea about that? If it's useful to me, How can I transfer my results to the temp table?
You won't be able to limit the database query using a Model accessor's dynamic field, since that field obviously doesn't exist in the database.
However, the Collection object has fairly robust filtering capabilities, so you could filter the Collection results using the dynamic fields after the database has been queried. This is not as performant as filtering out the results before they are retrieved from the database, but you may be a situation where the performance isn't that critical or the code cleanliness/maintenance cost outweighs the performance cost.
As an example, given the following Model:
class Book extends Model
{
public function getCategoryAttribute()
{
if ($this->targetAge < 13) {
return 'child';
}
if ($this->targetAge < 18) {
return 'teen';
}
return 'adult';
}
}
The following query will not work because the category field doesn't actually exist in the table:
$childrenBooks = Book::where('category', 'child')->get(); // error: no category field
However, the following will work, because you're calling where() on the Collection of Models returned from the database, and the Models do have access to the dynamic field:
$childrenBooks = Book::get()->where('category', 'child');
The problem in this case is that, while it does work, it will get all the books from the database and create a Model instance for each one, and then you filter through that full Collection. The benefit, however, is that you don't have to duplicate the logic in your accessor method. This is where you need to weigh the pros and cons and determine if this is acceptable in your situation.
An intermediate option would be to create a Model scope method, so that your accessor logic is only duplicated in one place (if it can be duplicated for a query):
class Book extends Model
{
public function getCategoryAttribute()
{
if ($this->targetAge < 13) {
return 'child';
}
if ($this->targetAge < 18) {
return 'teen';
}
return 'adult';
}
public function scopeCategory($query, $category)
{
if ($category == 'child') {
return $query->where('target_age', '<', 13);
}
if ($category == 'teen') {
return $query->where(function ($query) {
return $query
->where('target_age', '>=', 13)
->where('target_age', '<', 18);
});
}
return $query->where('target_age', '>=', 18);
}
}
Then you can use this query scope like so:
$childrenBooks = Book::category('child')->get();
The benefit here is that the logic applies to the actual query, so the records are limited before they are returned from database. The main problem is that now your "category" logic is duplicated, once in an accessor and once in a scope. Additionally, this only works if you can turn your accessor logic into something that can be handled by a database query.
You can create temporary tables using raw statements. This post goes fairly in depth over it:
https://laracasts.com/discuss/channels/laravel/how-to-implement-temporary-table

How can I minimize the amount of queries fired?

I'm trying to create a tag cloud in Laravel 4.1, tags can belong to many posts, and posts can have many tags. I'm using a pivot table to achieve this (post_tag).
So far I've come up with this to fetch the tags and check how many times it's used:
public static function tagCloud($tags, $max = 10) {
foreach($tags->toArray() as $tag) {
$count = DB::table('post_tag')
->where('tag_id', $tag['id'])
->count();
$cloud[$tag['slug']] = $count;
}
sd($cloud);
}
I pass Tag::all() to the above function. Obviously that's going to fire a crazy amount of queries on the database, which is what I'm trying to avoid. Normally you'd use eager loading to fix this problem, but it seems the documentation does not mention anything about eager loading pivot tables in combination with aggregates.
I hope someone can shine a light on how to optimize this current function or can point me in the right direction of a better alternative.
Sometimes it's just hard reduce them, but Laravel Cache is your friend:
$users = DB::table('users')->remember(10)->get();
It will remember your query for 10 minutes.
I believe you have a many-to-many relationship with posts in your tags table like this:
public function posts()
{
return $this->belongsToMany('Post');
}
So, you are able to do something like this:
$tags = Tag::with('posts')->get();
Then you may loop though the tags to find out how many posts each tag contains, like this:
foreach($tags as $tag) {
$tag->posts->count();
};
So, you may write your function like this:
public function scopeTagCloude($query) {
$cloud = [];
$query->with('posts')->get()->each(function($tag) use (&$cloud) {
$cloud[$tag['slug']] = $tag->posts->count();
});
return $cloud;
}
You may call this function like this:
$tagCloude = Tag::tagCloude();
If you dd($tagCloude) then you'll get something like this (example is taken from my blog app):
array (size=4)
'general' => int 4
'special' => int 5
'ordinary' => int 5
'extra_ordinary' => int 2

Resources