Querying relations with a where clause on the child table - php

I have been using the has method to query the existence of data in a related table and that is working just fine. Using the code example on the Laravel web site, you can do the following to get any post that has comments associated with it.
$posts = Post::has('comments')->get();
What I would like to be able to do, as an example, is query for any post which has comments from the last thirty days. Since the has method only has the ability to count related rows I am not sure of a good way to do this.

Ok, this is what I could come up with. It might not be the best solution, but it does work and doesn't seem too polluted. First, we'll need to add reference to Carbon and the Eloquent\Collection classes.
use \Carbon\Carbon;
use \Illuminate\Database\Eloquent\Collection;
Carbon does come with Laravel as a dependency, out of the box, but it would be smart to add it to your own project's dependencies anyway. Then, this code should do it:
// Gather all distinct 'post_id' from Comments made in the last 30 days
// along with its Post objects
$comments = Comment::with('Post')
->where('created_at', '>', Carbon::now()->subMonth())
->distinct()
->get(array('post_id'));
// Since we only want Posts, loop through the Comments collection and add
// the Posts to a new collection
$posts = new Collection;
foreach ($comments as $comment) {
$posts->add($comment->post);
}

From the Laravel 4 Documentation:
$posts= Post::with(array('comments' => function($query)
{
$query->where('published', '>=', 'date- 1 month'); //look date functions up
}))->get();
See Eager Load Constraints under laravel 4 documentation

Related

How to filter laravel collection

I am trying to make a filter in laravel. This following filter works
$posts= Post::where('category',$request->category)->orderBy('id','desc')->paginate(10);
But when I try to do something like this
public function index(Request $request)
{
$posts= Post::where('category',$request->category)->get();
$posts->latest()->paginate(10);
dd($posts);
It doesn't work. Can someone explain why is this and provide me the code that works. My project have multiple filter.
Error
Because $posts = Post::all(); already execute a query.
Post::where('category',$request->category)->latest()->paginate(10)->get();
would be what you want.
A note:latest requires the created_at column
You should go
$posts = Post::where('category',$request->category)->latest()->paginate(10);
the get request is unnecessary as the paginate will execute the query.
The first one makes the query by pagination i.e fetch 10 records per constructed page
For the second one, based on observation, you most likely have encountered at least 2 errors:
The first, on the line that used the get method because that method requires at least one parameter.
Type error: Too few arguments to function Illuminate\Support\Collection::get()
The other since its a collection, and since there is nothing like paginate or latest method on collection therefore throws other errors. You should check Collection's Available methods to have a glimpse of the methods allowed on collection.
One of the best solutions is to simply order the result when making the query:
Blog::where('category',$request->category)
->orderBy('created_at', 'desc') //you may use also 'updated_at' also depends on your need
->paginate(10);
This way you have the latest coming first int the pagination and also having not worrying about paginating a collection

Laravel exclude current id from query eloquent results

I am fairly new to laravel and I built a little "similar posts" section. So every post has a tag and I query all the id's from the current tag. And then I find all the posts with thoses id's. Now my problem is that the current post is always included. Is there an easy way to exclude the current id when querying?
I can't seem to find anything in the helper function on the laravel docs site
this is my function:
public function show($id)
{
$project = Project::findOrFail($id);
foreach ($project->tags as $tag){
$theTag = $tag->name;
}
$tag_ids = DB::table('tags')
->where('name', "=", $theTag)
->value('id');
$similarProjects = Tag::find($tag_ids)->projects;
return view('projects.show', ['project' => $project, 'similarProjects' => $similarProjects]);
}
An easy way to solve your issue would be to use the Relationship method directly instead of referring to it by property, which you can add additional filters just like any eloquent transaction.
In other words, you would need to replace this:
Tag::find($tag_ids)->projects
With this:
Tag::find($tag_ids)->projects()->where('id', '!=', $id)->get()
Where $id is the current project's id. The reason behind this is that by using the method projects(), you are referring your model's defined Relationship directly (most probably a BelongsToMany, judging by your code) which can be used as a Query Builder (just as any model instance extending laravel's own Eloquent\Model).
You can find more information about laravel relationships and how the Query Builder works here:
https://laravel.com/docs/5.1/eloquent-relationships
https://laravel.com/docs/5.1/queries
However, the way you are handling it might cause some issues along the way.
From your code i can assume that the relationship between Project and Tag is a many to many relationship, which can cause duplicate results for projects sharing more than 1 tag (just as stated by user Ohgodwhy).
In this type of cases is better to use laravel's whereHas() method, which lets you filter your results based on a condition from your model's relation directly (you can find more info on how it works on the link i provided for eloquent-relationships). You would have to do the following:
// Array containing the current post tags
$tagIds = [...];
// Fetch all Projects that have tags corresponding to the defined array
Project::whereHas('tags', function($query) use ($tagIds) {
$query->whereIn('id', $tagIds);
})->where('id', !=, $postId)->get();
That way you can exclude your current Project while avoiding any duplicates in your result.
I don't think that Tag::find($tag_ids)->projects is a good way to go about this. The reason being is that multiple tags may belong to a project and you will end up getting back tons of project queries that are duplicates, resulting in poor performance.
Instead, you should be finding all projects that are not the existing project. That's easy.
$related_projects = Project::whereNotIn('id', [$project->id])->with('tags')->get();
Also you could improve your code by using Dependency Injection and Route Model Binding to ensure that the Model is provided to you automagically, instead of querying for it yourself.
public function show(Project $project)
Then change your route to something like this (replacing your controller name with whatever your controller is:
Route::get('/projects/{project}', 'ProjectController#show');
Now your $project will always be available within the show function and you only need to include tags (which was performed in the "with" statement above)

Returning all Laravel eloquent models tagged with a specific set of tags

I've got an Eloquent model Post with a belongsToMany(Tag::class) relationship. Now in the situation where I want to return or get all posts with a given set of tags, what's the most efficient Laravel-esq way of performing such a query?
For example; get all posts that have the tag bbq, or all posts that have the tags bbq AND beef.
I'd like to simply pass an array of tags in which can be of any count, if possible. I have tried the following and a number of different combinations with no luck, granted my SQL kung-fu isn't the greatest.
$posts = Post::whereHas('tags', function ($query) {
$query->whereIn('tag_types.name', ['bbq', 'beef']);
})->get();
Eventually found a solution. In this case it requires knowing the ID's of the tags in question, which is easy enough to query for. In this case I store the tag ID's in an array called $tag_ids and use the following Eloquent query (tag_types being the pivot table):
$posts = Post::join('tag_types', function ($join) use ($tag_ids) {
$join
->on('posts.id', '=', 'tag_types.post_id')
->whereIn('tag_types.tag_id', $tag_ids);
})
->groupBy('posts.id')
->havingRaw('count(distinct tag_types.tag_id) = ' . count($tag_ids))
->lists('post_id');
$posts now contains the ID's of all the posts that contain every tag ID listed in $tag_ids.
Hope this helps anyone facing a similar dilemma.

Querying on related models using Laravel 4 and Eloquent

Using Laravel 4 I have the following models and relations: Event which hasMany Record which hasMany Item. What I would like to do is something like this
Item::where('events.event_type_id', 2)->paginate(50);
This of cause doesn't work as Eloquent doesn't JOIN the models together when retrieving the records. So how do I go about this without just writing the SQL myself (which I would like to avoid as I want to use pagination).
What you want is eager loading.
It works like this if you want to specify additional constraints:
Item::with(array('events' => function($query) {
return $query->where('event_type_id', 2);
}))->paginate(50);
There is a pull request pending here https://github.com/laravel/framework/pull/1951.
This will allow you to use a constraint on the has() method, something like this:
$results = Foo::has(array('bars' => function($query)
{
$query->where('title', 'LIKE', '%baz%');
}))
->with('bars')
->get();
The idea being you only return Foos that have related Bars that contain the string 'baz' in its title column.
It's also discussed here: https://github.com/laravel/framework/issues/1166. Hopefully it will be merged in soon. Works fine for me when I update my local copy of the Builder class with the updated code in the pull request.

Paginate Eager Load Constraints

I am Using Laravel 4:
I am trying to paginate an Eager Loaded Constraint, essentially I have a Topics Model which has a hasMany relationship to a Comments Model. This is all well and good and no problems with that. However I would like to be able to display a paginated table of the Comments (of which there will be hundreds) on the admins edit topic view. To do this I thought I would use the following code:
$topic = Topics::with( array( 'comments' => function($query)
{
$query->orderby('created_at', 'DESC');
$query->paginate(10);
} ) )
->where('id', $id)
->first();
Now this will provide me $topic with the correct object and inspecting $topic->comments shows it has the correct linked comments and is limited to 10 results. The pagination also responds to the GET page parameter and will paginate, however trying to echo $topic->comments->links() results in an error of method not found.
My work around for this is to have a second query which counts the attached comments and then using the following in my view:
$paginator = Paginator::make($topic->comments->toarray(), $commentCount, 10);
echo $paginator->appends(array('tab' => 'comments'))->links();
Is this a bug?
When you call paginate() on your eager constraint it is configuring the query for you (setting the skip and take). The paginator that is generated will actually be discarded.
Since you're just getting a single topic with its comments why not do this (same number of database calls)...
$topic = Topic::find($id);
$comments = $topic->comments()->order_by('created_at DESC')->paginate(10);
Admittedly, you won't be able to use $topic->comments to get the paginator, and will have to use $comments instead, but that's not a terrible thing.

Categories