Laravel 5 query with variable number of where clauses - php

I'm try to make a tag-based search system similar to the one here on stackoverflow using Laravel and jQuery Token-Input plugin. The contents of the "tags" are then to be used as the query itself, drawn from a list from a different table.
Using Eloquent, I want to build a query based on a variable number of tags in the search bar (limited only by the number of possible tags there can be). It would look something like this if not done as a loop:
$query = Model::whereHas('attribute', 'name', '=', 'tag1')
->whereHas('attribute', 'name', '=', 'tag2')
->whereHas('attribute', 'name', '=', 'tag3')
// Repeat until...
->get();
The 'attribute' in question is actually something drawn from a pivot table.
Obviously, I want it to be in the form of a loop since we're dealing with a variable number of tags. How would I go about doing this?

I suppose you can so something like this:
$query = Model::whereHas('attribute', 'name', '=', 'tag1');
foreach ($other_tags as $tag) {
$query = $query->whereHas('attribute', 'name', '=', $tag);
}
print_r($query->get());
The only hint here is that you need to use first tag to init Model::whereHas. Other tags can be iterated over and each one will be added to $query.

If you have tags in array like this
$tags = [ 'tag1', 'tag2', 'tag3', 'tag4', ...];
then you can simply use whereIn
$results = Model::whereHas('attribute', function($query) use ($tags) {
$query->whereIn('name', $tags);
})->get();

Related

Searching Model with Many to Many Polymorphic Relation

Following the tags example in the Laravel docs, I've successfully implemented image tagging, and now I'm trying to search by said tags.
Pulling an image from a single tag works well, but I'm hitting a roadblock when trying to search for images containing multiple tags.
This is the closest I've gotten, and it works as expected but it is not what I desire.
/**
* $tag string, example: 'baseball'
* $tags array, example: ['baseball','stadium','new.york.yankees']
*/
Image::whereHas('tags', function($query) use ($tag,$tags) {
if (count($tags)) {
// Retrieves images tagged as 'baseball' OR 'stadium' OR 'new.york.yankees'
// Instead, I want it to retrieve images that have all three tags
$query->whereIn('tag', $tags);
// The following returns no results, though there are images tagged correctly.
// I presume this is an incorrect approach.
foreach ($tags as $tag) {
$query->where('tag', '=', $tag);
}
} else {
// Retrieves images tagged as 'baseball'
$query->where('tag', '=', $tag);
}
})->orderBy('created_at', 'desc')
->paginate(config('app.images_per_page'))
I'm stumped, overtired, and getting nowhere while searching for similar examples. What am I missing? What is the correct terminology for what I am trying to achieve, so I can add it to my vocabulary for future endeavors?
I think this just needs to be a whereHas statement for each of the tags you are trying to filter by. This would create an AND statement within the query.
if(!$count($tags)) $tags = [$tag]; //for simplicity lets always have an array
$imageQuery = Image::query();
foreach($tags as $tag){
$imageQuery = $imageQuery->whereHas('tags', function($query) use ($tag) {
$query->where('tag', '=', $tag);
});
}
$results = $imageQuery->orderBy('created_at', 'desc')
->paginate(config('app.images_per_page'))->get();
Code is not tested but logically I think you need a where clause for each tag

Selecting and ordering data from a query using Laravel Eloquent

I am trying to use Laravel and eloquent to return results based on the following query.
$blogPosts = BlogPosts::with('blog_user', 'blog_categories', 'blog_comments', 'tags')
->orderBy('created_at', 'desc')
->paginate(5);
Ok, so that is fine, it return exactly what it should, all the blog posts with associated relations to other tables.
What I now want to do is return only the $blogPosts where a tag is clicked by the user. So let's say there is a tag "PHP", so I pass in that value as $tag to the method. I then have something like this.
public function tag_search($tag)
{
$blogPosts = BlogPosts::with('blog_user', 'blog_categories', 'blog_comments', 'tags')
->where('tags', $tag)
->orderBy('created_at', 'desc')
->paginate(5);
$categories = BlogCategories::with('blog_posts')->get();
$data = array('blogPosts' => $blogPosts, 'categories' => $categories,
);
return view('blog.index')->with($data);
}
Now my issue is actually relatively simple I guess, if the where clause was a column in the BlogPosts table it would work, I know this because I tried that.
However the above won't work as is, I can only use;
->where('x', y)
Where x is a field in the BlogPosts table. I want to return a set of values where the submitted $tag is the same as one associated to the tags attached to the blog posts.
Make sense? I think I am over thinking it the point I am just not thinking now :)
Add the column field 'tags' to your table and then these queries:
$blogPosts = BlogPosts::with('blog_user', 'blog_categories', 'blog_comments', 'tags')
->where('tags', '=', $tag)
->orderBy('created_at', 'desc')
->paginate(5);
$categories = BlogCategories::with('blog_posts')->get();
return view('blog.index', compact ('categories', 'blogposts'));
Ok so the answer was to use whereHas like this.
$blogPosts = BlogPosts::whereHas('tags', function ($query) use ($tag) {
$query->where('name', $tag);
})->orderBy('created_at', 'desc')
->paginate(5);
That returns a search based on the associated elements.

Laravel get posts with multiple tags

I'm working with a laravel belongsToMany relationship between Post and Tag.
What I'm trying to do is get all Posts where it has multiple tags.
I've tried all sorts of eloquent queries, but I can't get it at all.
Currently I can get an array of post_id's and tag_id's, as shown below, but there has to be an easier way to do this.
if (Request::has('tags')) {
$tags = Tag::find(explode(',', Request::get('tags')));
}else{
$tags = null;
}
// Get all posts tagged with the tags
$jobs = \DB::table('post_tag');
foreach ($tags as $tag) {
$posts = $posts->orwhere('tag_id', $tag->id);
}
dd($posts->get());
This dumps an array of all posts that have any of the ID's, but I need to get an array of post_ids where it contains all tag_ids.
Thanks in advance!
It would be a good idea to use whereHas() on the Post model to eager load the tags and get only the Posts which have at least one of those tags.
$posts = Post::whereHas('tags', function($q) use ($tags)
{
$q->whereIn('id', $tags);
})->get();
Here, $tags would just be an array of tag id's. $posts would be a Collection of Posts. To get the array of id's from it, you can simply do this...
$ids = $posts->lists('id');
Or instead of calling get() originally, use ...->lists('id')
Edit
If you are looking for only those Posts which contain all of the tags, you want to pass some additional parameters to the whereHas function.
$posts = Post::whereHas('tags', function($q) use ($tags)
{
$q->whereIn('id', $tags);
}, '=', count($tags))->get();
What will happen is it will only grab posts that have a number of tags attached equal to the count of the tags in the tags array.
If you use this approach, be sure your pivot table is managed correctly in that it is not possible to attach a certain tag to a certain model more than once.

How to do pagination in Many to Many in Eloquent?

I have two tables Posts and Tags with many to many relation. I need to retrieve all posts by some tag and paginate it. Is it possible to do with paginate method or I need to create a new instance of Paginator and do everything by hand?
P.S.
Need something like:
$tags = Tags::where('name', '=', $tag)->with('posts')->first();
$posts = $tags->posts->paginate(5);
return view('blog/posts')->with('posts', $posts);
You should probably use:
$posts = $tags->posts()->paginate(5);
and you should also rename $tags to $tag if you are getting only one record to not make variable name misleading.
You have to use a join statement. It should look something like this:
DB::table('tags')
->join('posts', 'tags.id', '=', 'posts.tag_id')
->where('tags.name', '=', $tag)
->paginate(5);

How to 'order_by' on second table when using eloquent one-to-many

Of course I can use order_by with columns in my first table but not with columns on second table because results are partial.
If I use 'join' everything works perfect but I need to achieve this in eloquent. Am I doing something wrong?
This is an example:
//with join
$data = DB::table('odt')
->join('hdt', 'odt.id', '=', 'hdt.odt_id')
->order_by('hdt.servicio')
->get(array('odt.odt as odt','hdt.servicio as servicio'));
foreach($data as $v){
echo $v->odt.' - '.$v->servicio.'<br>';
}
echo '<br><br>';
//with eloquent
$data = Odt::get();
foreach($data as $odt){
foreach($odt->hdt()->order_by('servicio')->get() as $hdt){
echo $odt->odt.' - '.$hdt->servicio.'<br>';
}
}
In your model you will need to explicitly tell the relation to sort by that field.
So in your odt model add this:
public function hdt() {
return $this->has_many('hdt')->order_by('servicio', 'ASC');
}
This will allow the second table to be sorted when using this relation, and you wont need the order_by line in your Fluent join statement.
I would advise against including the order by in the relational method as codivist suggested. The method you had laid is functionally identical to codivist suggestion.
The difference between the two solutions is that in the first, you are ordering odt ( all results ) by hdt.servicio. In the second you are retrieving odt in it's natural order, then ordering each odt's contained hdt by servico.
The second solution is also much less efficient because you are making one query to pull all odt, then an additional query for each odt to pull it's hdts. Check the profiler. Considering your initial query and that you are only retrieving one column, would something like this work?
HDT::where( 'odt_id', '>', 0 )->order_by( 'servico' )->get('servico');
Now I see it was something simple! I have to do the query on the second table and get contents of the first table using the function odt() witch establish the relation "belongs_to"
//solution
$data = Hdt::order_by('servicio')->get();
foreach($data as $hdt){
echo $hdt->odt->odt.' - '.$hdt->servicio.'<br>';
}
The simple answer is:
$data = Odt::join('hdt', 'odt.id', '=', 'hdt.odt_id')
->order_by('hdt.servicio')
->get(array('odt.odt as odt','hdt.servicio as servicio'));
Anything you can do with Fluent you can also do with Eloquent. If your goal is to retrieve hdts with their odts tho, I would recommend the inverse query for improved readability:
$data = Hdt::join('odt', 'odt.id', '=', 'hdt.odt_id')
->order_by('hdt.servicio')
->get(array('hdt.servicio as servicio', 'odt.odt as odt'));
Both of these do exactly the same.
To explain why this works:
Whenever you call static methods like Posts::where(...), Eloquent will return a Fluent query for you, exactly the same as DB::table('posts')->where(...). This gives you flexibility to build whichever queries you like. Here's an example:
// Retrieves last 10 posts by Johnny within Laravel category
$posts = Posts::join('authors', 'authors.id', '=', 'posts.author_id')
->join('categories', 'categories.id', '=', 'posts.category_id')
->where('authors.username', '=', 'johnny')
->where('categories.name', '=', 'laravel')
->order_by('posts.created_at', 'DESC')
->take(10)
->get('posts.*');

Categories