Laravel Eloquent whereDoesntHave - laravel

I'm having issues using this method, I have read the docs but I am either doing something wrong or not understanding how it works or it's a bug.
I have the following code in my controller:
$books = Book::whereDoesntHave("author", function ($query) {
$query->whereNotNull("died_at");
})->get();
Now what this is supposed to do is return all books whose authors are still alive and also all books that do not have an author, however it does the exact opposite.
I assumed whereDoesntHave() is supposed to check whether the model doesn't have the specified relation, in this case an author model with the column died_at having a specific value.
Instead it checks the author model where the column died_at doesn't a value.
I'm very confused about this, how is this function supposed to work exactly? Can someone please explain this to me.

can you try this, doesntHave("author") check books that doesn't have author and whereHas with closure check live author
$books = Book::doesntHave("author")->orWhereHas("author", function ($query) {
$query->whereNotNull("died_at");
})->get();

Related

Eloquent return elements only when last related element is older than 30 days?

I'm new to programming world and I use laravel. I have have Post model, every user can have more posts. For for all posts I have hasMany relation but this is related to posts, and I need inverse logic.
I don't know how can I get only users which last post is older then 30 days? I need them for email notification.
Can somebody give me some inputs please?
https://laravel.com/docs/8.x/eloquent-relationships#querying-relationship-existence
So it should be something like this:
User::whereHas('posts', function ($query) {
return $query->where('created_at', '<=', now()->subDays(30);
})->get()

Laravel Eloquent - orWhereHas method - When to use it and how

I'm trying to understand some advanced eloquent commands and on the Laravel official documentation, there is not so much about the Eloquent orWhereHas method and there isn't also an example about how it works.
https://laravel.com/docs/8.x/eloquent-relationships#querying-relationship-existence
Can somebody help me to understand it with also a simple example?
How to use it: just chain as any other Eloquent method
User::whereHas(...)->orWhereHas(...)->get();
When to use it: imagine you have Users, Posts and Comments, and each user can write posts and comments. Then you need to get active users. For example, you assume active as user, who has made posts OR comments last 7 days. So, you can get it this way:
$users = App\Models\User::query()
->whereHas('posts', function (Builder $query) {
$query->where('created_at', '>=', Carbon::now()->subDays(7));
})
->orWhereHas('comments', function (Builder $query) {
$query->where('created_at', '>=', Carbon::now()->subDays(7));
})
->get();
Say there's a blog kind of app. The main entity/model of the app would be Post (Blog Post).
When any author writes and publishes a Post,
visitors to the blog site can leave Comment(s) for the Post
visitors can Like a Post
So we have 3 models here
Post - which can have many Comment(s)
Post - can have many Like(s)
Now let's say for some reason we want to get all Post records from the database which either have 10 or more comments in the current month or 3 or more likes in the current month
We can write a query like
$posts = Post::whereHas('comments', function($query) {
$query->where('created_at', '>', now()->startOfMonth();
}, '>=', 10)
->orWhereHas('likes', function($query){
$query->where('created_at', '> ', now()->startOfMonth();
}, '>=', 3)
->get();
Laravel docs: https://laravel.com/docs/8.x/eloquent-relationships#querying-relationship-existence
Just like where both whereHas and orWhereHas accepts closure as 2nd argument for more fine grained query control.
Actually whereHas is supposed to be used when you want to have more power on constraints.
If you just want to check the existence of relation records you can use has for eg:
Get all post records which either have comment or like and paginate 20 per page
$postsWithCommentsOrLikes = Post::has('comments')
->orHas('likes')
->paginate(20);

Get all objects that exist in two collections

I'm building a Laravel page on which I want to show a list of lessons. Which lessons should be on the page is filtered by three criteria (of which all should be true):
The lesson is active, ie "where('active', true)". Simple enough.
The lesson is part of a track that the user has chosen. Models are set up with belongsToMany() (it is a many-to-many relationship), so I can get these lessons by a simple $track->lessons.
This is where it gets tricky. Some lessons should only be visible to users with certain titles (ie there is a many to many between titles and lessons). I can get the lessons with the correct title requirement using Auth::user()->title->lessons.
Question is how I get all this together. The best I've come up with this far is the following:
$title = Auth::user()->title;
$lessons = Lesson::where('active', true)
->whereIn('id', $track->lessons->pluck('id'))
->where(function ($query) use($title) {
$query->whereIn('id', $title->lessons->pluck('id'))->orWhere('limited_by_title', false);
})
->get();
...which is crap ugly, clearly suboptimal and (for some reason I really don't understand) also won't work (I don't get the lessons my title entitles me to in my list). Been struggling for quite some hours now, I get the feeling that I'm overcomplicating, first plucking id's and then using them in a whereIn() can't possibly be a good way of doing this.
So I can easily enough get a collection of lessons in the track, and I can get a collection of lessons belonging to the title, but how do I get all objects that exist in both those collections?
Using whereHas() is the answer to your concerns about plucking IDs. Instead of running additional queries to retrieve IDs, whereHas() will attach the constraint to the original query as a subquery on the related tables.
Breaking the query down to its parts:
1: Answered
2: Assuming the inverse of $track->lessons is $lesson->tracks, and $track is coming from code you didn't include:
$lessons = Lesson::whereHas('tracks', function ($query) use ($track) {
$query->where('id', $track->id);
})
3: Assuming the inverse of $title->lessons is $lesson->titles:
$lessons = Lesson::where(function ($query) use ($title) {
$query->whereHas('titles', function ($query) use ($title) {
$query->where('id', $title->id);
})
->orWhere('limited_by_title', false);
})
Combined back into one:
$track = ???;
$title = Auth::user()->title;
$lessons = Lesson::where('active', true)
->whereHas('tracks', function ($query) use ($track) {
$query->where('id', $track->id);
})
->where(function ($query) use ($title) {
$query->whereHas('titles', function ($query) use ($title) {
$query->where('id', $title->id);
})
->orWhere('limited_by_title', false);
})
->get();
If this still doesn't give the results you were expecting, you can examine the full query being run by replacing get() with toSql(). Sometimes working from the ORM as a starting point instead of the SQL can lead you down the wrong path. For even more detail to debug and understand the queries being run, you can enable query logging: https://laravel.com/docs/5.7/database#listening-for-query-events
intead of where use whereHas on "titles" relationship
$title = Auth::user()->title;
$lessons = Lesson::where('active', true)
->whereIn('id', $track->lessons->pluck('id'))
->whereHas('titles',function ($query) use($title) {
$query->whereIn('id', $title->pluck('id'))
->orWhere('limited_by_title', false);
})->get();
First of all complicated, really complicated. Your table structure needs serious modification to make it easier.
However, considering you don't want to go down that road, you could do it simpler by using join
Assuming you have following table structure:
users
titles (has user_id foreign key)
lessons (has title_id foreign key)
tracks (has lesson_id foreign key)
$trackName = $request->input('track_name');
$title = Auth::user()->title;
$lessons = Lesson::join('tracks', 'lessons.id', '=', 'tracks.lesson_id')
->join('titles', 'lessons.title_id', '=', 'titles.id')
->where('lessons.active', true)
->where('tracks.track_name', $trackName)
->where(function ($query) use($title) {
$query->where('titles.id', $title->id)->orWhere('lessons.limited_by_title', false);
});
dd($lessons);
That is of course if your users table and titles have one to one relationship otherwise pluck all title_ids and use whereIn instead of where for titles.id query.
I hope you have enough understanding of laravel framework to understand and implement this solution.
Sorry, I don't have enough time to proofread or give more details.
Good luck!
Good luck if you need pagination after that :p I doubt simple ->paginate() will work :D
I hope it helps

Laravel 5, get only relationship for eloquent model

I have a user and article models in my app. The relation between them is straightforward:
//article.php
use Taggable;
public function user()
{
return $this->belongsTo('App\User');
}
Article uses the Taggable lib which provides me with a variety of methods like Article::withAnyTags (returns all articles tagged with 'xyz' string). Now I'd like to get all users who posted an article/s tagged as 'xyz'. I know how to do this in two lines but something tells me that this is not right. Ideally I'd like to do sth like:
$users = Article::withAnyTags('xyz')->with('user')->select('user'); --pseudocode
Is it possible to do sth like this in eloquent?
Side note: I knot that I could do this with DB::table but this is not an option for me. Please note that there is no "->get()" at the end on my pseudocode. It's so, because I'm paginating the users' results set with lib which works only with eloquent queries.
You could use whereHas():
User::whereHas('articles', function ($query) {
$query->withAnyTags('xyz');
})->paginate();
Or if you need to pass a variable of the tags to the closure you could do:
$tag = 'xyz';
User::whereHas('articles', function ($query) use($tag) {
$query->withAnyTags($tag);
})->paginate();

Laravel Relationships Conditions - 3 tables

I've got a situation where I've got Posts, Users and Comments.
Each comment stores a post_id and a user_id. What I want to do is get all of a user's comments on a particular post, so that I can do a call like this:
$comments = Auth::User()->comments(post_id=x)->text
(where I know what x is)
I have:
User->HasMany(comments)
Comments->HasOne(User)
Comments->HasOne(Project)
Project->HasMany(comments)
I feel like there needs to be a where or a has or a wherehas or something thrown in.. the best I can manage is that I pull Auth::User()->comments into an array and then search through the array until I find the matching post ID.. that seems wasteful.
with doesn't apply any join, so you can't reference other table.
You can use this:
// User model
public function comments()
{
return $this->hasMany('Comment');
}
// Comment model
public function scopeForPost($query, $postId)
{
$query->where('post_id', $postId);
}
// then you can do this:
Auth::user()->comments()->forPost($postId)->get();
Alternatively you can eager load comments with constraint:
User::with(['comments' => function ($q) use ($postId) {
$q->where('post_id', $postId);
}])->find($someUserId);
// or exactly the same as above, but for already fetched user:
// $user .. or
Auth::user()->load(['comments' => function ($q) use ($postId) {
$q->where('post_id', $postId);
}]);
// then you can access comments for $postId just like this:
Auth::user()->comments; // collection
When you need to filter your relations, you just have to do it in your Eloquent query:
$data = User::with('posts', 'comments')
->where('users.id', Auth::User()->id)
->where('posts.id', $postID)
->get();
Then you can
foreach($data->comments as $comment)
{
echo $comment->text;
}
Your Comments table would have foreign keys Post_Id and User_ID
To Access all the comments of a particular post from a particular user , can you try this way?
Comment::select('comments.*')
->where('comments.user_id', Auth::user()->id)
->leftJoin('posts','posts.id','=','comments.post_id')
->leftJoin('users','users.id','=','comments.user_id')
->get();
Am sure there is better way to achieve it, but this should give you desired results.
Note use aliases if you have conflicting column names
Let me know if this worked.

Resources