Laravel query on Many to Many relationship - laravel

I have an API to keep tracked photos and tags. Each photo can have N tags and one tag can be linked to N photos. That's done using 3 tables:
Photo's table.
Tag's table.
photo_tag relation table.
Now I'm working to get all photos tagged with a set of tags the idea is to make requests to my API with a list of tags and get a list of photos that has at least all the tags.
I've been trying with the whereIn operator.
This is my code (now it's all hardcoded):
$photos = Photo::whereHas('tags', function (Builder $query) {
$query->whereIn('tag', ['a5', 'mate', 'Brillante']);
})->get();
return response()->json($photos, 200);
When I execute it it return all that photos that match one tag and I need only photos that hast all the requested tags (in this example a5, mate).
I'm working on Laravel 9.
Edit:
As Tim Lewis suggested I've tried looping:
$tags = array("a5", "mate", "Brilante");
$photoQuery = Photo::query();
foreach($tags as $tag) {
\Log::debug($tag);
$photoQuery->whereHas('tags', function($query) use ($tag) {
return $query->where('tag', $tag);
});
}
$photos = $photoQuery->get();
Now it's returning an empty list I think because is looking for Photos that only have the 3 tags I hardcoded on the array.
Edit 2:
It seems that those changes were right, but for some reason Postman was not showing me any results of those changes are the solutions to my issue.

Since the whereIn() method matches against any of the values provided, and not all, you'll need to modify this. Specificying a number of whereHas() clauses, 1 for each Tag, should work:
$photoQuery = Photo::query();
foreach ($request->input('tags') as $tag) {
$photoQuery = $photoQuery->whereHas('tags', function ($query) use ($tag) {
return $query->where('tag', $tag);
});
}
$photos = $photoQuery->get();
Now, depending on the tags being sent to your API (assuming through the $request variable as a 'tags' => [] array), this query will include a whereHas() clause for each Tag, and only return Photo records that have all specified Tags.

Related

Querying the existence of an exact set of many to many relationship with Eloquent?

I need to make a query to find a Model that has an exact set of a many-to-many relationship.
For example, I have a Blog post that has many Tags. The tags can belongs to many blog posts.
How could I retrieve all blogs posts that have the tag #1, #2 and #3 without any other tag, only these 3, in an efficient manner?
First approach that comes to my mind is a ->whereHas() for each Tag you want to find, something like:
$tagIds = [1, 2, 3];
$query = Blog::where(function ($query) use ($tagIds) {
foreach ($tagIds as $tagId) {
$query->whereHas('tags', function ($subQuery) use ($tagId) {
return $subQuery->where('tags.id', $tagId);
});
}
})->has('tags', '=', count($tagIds));
// Additional Clauses/Filters/Etc.
$blogs = $query->get();
This will execute an additional query for the Tags supplied (based on ID), and filter your Blog records to only those that match all supplied Tags.

Exclude empty or null column with Laravel Eloquent

How to exclude empty or null columns when getting collections with Laravel Eloquent ?
I tried this but unsuccessfully:
User::where('last_name', 'foo')->get()->filter()
In addition to #pr1nc3 answer, there is a method ->reject() for this specific purpose. It rejects/excludes the items that match the condition. For your case, use it like this:
User::where('last_name', 'foo')->get()->reject(function ($value) { return empty($value); });
All the values that meet the condition empty($value) i.e. the values that are null/empty will be rejected.
You can do the filter in 2 steps
$users = User::where('last_name', 'foo')->get(); //returns your collection
Then you can use filter for your collection like:
$myFilteredCollection = $users->filter(function ($value) { return !empty($value); });
If you still need it in one line then you can do:
Of course you can merge it into one, get() actually outputs the collections but looks a bit ugly i think. Keep your actions separate.
$users = User::where('last_name', 'foo')->get()->filter(function ($value) { return !empty($value); });

Order by count in many to many polymorphic relation in 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.

How to filter collection rows while using .each() in laravel?

I want to filter this $authors collection.
$authors = Author::get();
In my view, I need to show how many books an author is related.
I use .each() to merge the count to $authors and return it to where it was called.
return $authors->each(function($author, $key){
$author->count = Author::findOrFail($author->id)->books()->count();
});
Question is "How can I remove/filter if an author have not wrriten any of a book(count <= 0)?"
I tried this is and it failed.
return $authors->each(function($author, $key){
$author->count = Author::findOrFail($author->id)->books()->count();
$author->filter(function($author, $key){
return $author->count <= 0;
})
});
You should use withCount() to avoid N+1 and performance problems:
$authors = Author::withCount('books')->get();
In a Blade view, you'll be able to do:
$author->books_count
If you want to count the number of results from a relationship without actually loading them you may use the withCount method, which will place a {relation}_count column on your resulting models.
https://laravel.com/docs/5.4/eloquent-relationships#counting-related-models

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