Fetch users with their unread messages with Laravel Join vs Laravel Eloquent - laravel

I 'm working on a chat application. I am fetching all the users to show in the sidebar. With each user fetched I also want to show an indication of unread messages from any user. I have the messages table like this.
id | user_id | receiver_id | message | is_read
and standard users table of Laravel. I achieved it with the following join in the the controller method.
public function users_join()
{
// get logged in user
$user = Auth::id();
// left join to get all users all time
return DB::table('users')->where('users.id', '!=', $user)
->leftJoin('messages', function ($join) use ($user) {
// first evaluate where in below function
$join->on('users.id', '=', 'messages.user_id')
// filter messages sent to current user
->where('messages.receiver_id', '=', $user)
// get only those messages which are unread
->where('messages.is_read', '=', 0);
})->select('users.*', 'messages.user_id', 'messages.receiver_id', 'messages.message'
, 'messages.is_read')
// finally grouped by email to get only 10 records from users table
->orderBy('messages.created_at', 'DESC')->groupBy('email')->get();
}
Now I want to do it with the Laravel Eloquent. I can't think of any other way of doing this without using loop. Here is the code for that.
public function users_eloquent()
{
// get current user
$current_user = Auth::id();
// each through all users except current
$users = User::all()->except($current_user)->each(function ($user) use ($current_user) {
// laravel relationship
$user->messages = $user->messages()
// messages sent to me
->where('receiver_id', $current_user)
// unread message sent to me
->where('is_read', 0)
// get latest unread message
->orderBy('created_at', 'DESC')
// first latest message
->first();
});
// send back the response
return $users;
}
My User Model is like this
public function messages()
{
return $this->hasMany('App\Message');
}
and just in case, my Message Model is like this
public function user()
{
return $this->belongsTo('App\User');
}
I want to achieve it with Eloquent without using each loop. How can I do this?
I have all the users in sidebar and their unread mark. Below is the attached screenshot.
Thanks!

You don't need to loop all the users.
You can find the user except current_user by where() method on Eloquent model before it become collection.
And use with() to get the latest msg for those users:
$current_user = Auth::id();
return User::where('id', '<>', $current_user)
->with(['messages' => function($q) use ($current_user) {
$q->where('receiver_id', $current_user)
->where('is_read', 0)
->latest()
->first();
}])->get();

Related

How to query for the messages between the exact recipients?

Is there a similar "whereIn" for "whereInBetween" in Laravel 7+? I have a basic chat app:
Message model
id, content, userId, threadId
threadId is filled when a user replies to a message. Think "parentId".
public function sentBy(): BelongsTo
{
return $this->belongsTo(User::class, 'userId');
}
public function recipients(): HasMany
{
return $this->hasMany(MessageRecipient::class);
}
Recipients model
id, userId, messageId
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'userId');
}
public function message(): BelongsTo
{
return $this->belongsTo(Message::class, 'messageId');
}
I could then say:
User::first()->messages;
Message::first()->sentBy;
With this setup, the recipients model could easily be called "group". When a user creates a message, a recipients relationship is created for the current user along with other users regardless if the message was sent to the one who created the message. Common? Ok. My issue is this, I need to return all conversation between us three userIds: [1,2,3]:
$messageId = 1;
// Message with id of 1 will not have a threadId, only its replies.
$userIds = [1,2,3];
foreach($userIds as $id) {
Recipient::create(['messageId' => $messageId, 'userId' => $id]);
}
We have a "group chat theme" going on here. If I need to query for message with us three users (above userIds), I'd do:
return Message::whereHas('recipients', function(Builder $q) use ($userIds) {
return $q->whereIn('userId', $userIds);
})
->orderBy('id', 'DESC')
->get();
Perfect. Exactly what I need. Now, let's create another message:
$messageId = 2;
$userIds = [1,2];
$queryUserIds = [2,3];
foreach($userIds as id) {
Recipient::create(['messageId' => $messageId, 'userId' => $id]);
}
// With this query, I'd expect to see message between the two queryUserIds **only**,
// Expected results should be an empty array because we have no message between userId 2 and 3 only. We have messages between all three: 1,2,3.
return Message::whereHas('recipients', function(Builder $q) use ($queryUserIds) {
return $q->whereIn('userId', $queryUserIds);
})
->orderBy('id', 'DESC')
->get();
How can I query messages between a set of userIds. Not sure I'm expressing myself correctly. All Im saying is, for users 1, 2 and 3, give me conversations with only those three users and nothing else because only those three are in the same conversation.
I got this idea from Facebook's messenger. I don't know how they do it but I'm "borrowing" a functionality; when you create a new message, it seems to retrieve the last/latest conversations between same "recipients". I know there are more complexities that go on but I wanted to do something simple.
Think I have found it. Correct me if I'm wrong. To recap, when a message is created, it creates an array of recipients with a message.threadId of null. When anyone replies to that message, you'll now have a message.threadId of 1. When I want messages between all three users: 1,2,3, I'd query:
$userIds = [1,2,3];
// Probably could use sql's sum but... 🤷🏽
$sumIds = array_sum($userIds);
$messages = Message::whereHas('recipients', function(Builder $q) use ($sumIds) {
// SUM is the key part.
return $q->havingRaw('SUM(recipients.userId) ='. $sumIds);
})->get();
This works 99% but I want the "replies", messages with threadId not null; I need the replies:
$message = Message::whereHas('recipients', function(Builder $q) use ($sumIds) {
return $q->havingRaw('SUM(recipients.userId) ='. $sumIds);
})
// I literally need the first in the array. I'd expect the array should contain one object and never more than one
->first();
if (!$message) {
return [];
}
// Another query to get all (conversations). You'll now see the picture:
return Message::where(function($q) use ($message) {
$q->orWhere('id', $message->messgeId);
// This gives me the "replies"
$q->orWhere('threadId', $message->messgeId);
})->get();
This works for now. If anyone could refactor, I'll gladly accept your answer with an upvote.
Thanks!

Getting an empty collection in Laravel

I'm trying to create an automatic email that will send an email to a user if they have any products waiting on them to approve for use. I'm having an issue when I run my artisan command; I'm still getting empty collections instead of only products that need approval.
$users = User::all();
foreach ($users as $user) {
$outstandingProducts = Product::outstandingProducts($user);
if (empty($outstandingProducts)) continue;
dd($outstandingProducts);
}
Product model
public static function outstandingProducts(User $user)
{
return Product::where('approver_id', $user->id)
->where('status', 'Pending Approval')->get();
}

How to get the last row in with method relationships in laravel?

I'm trying to build a chat application with laravel and I have two tables one for the Conversations and another one for the Messages, and I want to have each conversation with the last message. This is the query that I tried:
$allConvs = Conversation::with(['messages' => function ($query) {
$query->latest()->first();
}])
->get();
But it returns the last message for the the first conversation only and null for the others. Is there any other way to do it?
This is my relationships
Conversation.php
public function messages(){
return $this->hasMany('App\Message', 'conversation_id','id')->latest();}
Message.php
public function conversation()
{
return $this->belongsTo('App\Conversation' ,'id', 'conversation_id');
}
You can use a HasOne relationship:
public function latestMessage() {
return $this->hasOne(Message::class)->latest();
}
$allConvs = Conversation::with('latestMessage')->get();
This will still fetch all messages in the background, but only show the latest one for each conversation.

laravel eloquent - how to select users

I've got two tables:
Users table with id's
And Messages table with users's Id as "from" and offer Id's as offer_id
I want to select all users that send messages to with certain offer_id's
For example
user with id 1 send few messages:
Id from offer_id
1. 1, 5
2. 2, 5
3. 1, 5
4. 1, 3
I want to select all users that sent offer_id =5, so users of id 1 and 2
How to do it via eloquent with Message and User class?
I've got offer_id given so I can easily select messages:
$messages = Message::where('offer_id', $id);
but how to select users?
edit:
I tried this way:
in Message model:
public function fromContact()
{
return $this->hasOne(User::class, 'id', 'from');
}
and then in controller:
$contacts = $messages->fromContact;
but it gives an error
edit migration:
Schema::create('messages', function (Blueprint $table) {
$table->increments('id');
$table->integer('from')->unsigned();
$table->integer('to')->unsigned();
$table->boolean('read')->default(false);
$table->integer('offer_id')->nullable();
$table->mediumText('body')->nullable();
$table->timestamps();
});
and error:
"message": "Undefined property:
Illuminate\\Database\\Eloquent\\Builder::$fromContact",
You're missing some closure on your Query Builder. When you use
$messages = Message::where('offer_id', $id);
You don't have an instance of Message until you use a Closure, which are ->first(), ->get(), etc. So using
$message->fromContact;
Will result in an error stating that ->fromContact is not available on a Builder instance. To make this work, use
$messages = Message::where('offer_id', $id)->get();
foreach($messages AS $message){
$contacts = $message->fromContact;
}
Since this doesn't have much context in a loop, the code above doesn't do anyhting, but $messages->fromContact would also be an error. To get around that, use:
$message = Message::where('offer_id', $id)->first();
$contacts = $message->fromContact;
That should give you a good idea of what's going wrong and how to handle it.
Edit
When looping over multiple Message instances, push $message->fromContact to an array (or Collection) for use later:
$messages = Message::with(['fromContact'])->where('offer_id', $id)->get();
// Note: Using `::with()` prevents additional database calls when using `$message->fromContact` in a loop.
$contacts = []; // or $contacts = collect([]);
foreach($messages AS $message){
$contacts[] = $message->fromContact; // or $contacts->push($message->fromContact);
}

How to query a table using result of many-to-many relation with Eloquent ORM?

here is link to my database schema:
How can I get all topics from the blogs to which the user is subscribed using his id? I use Eloquent ORM.
I have added the following lines to my User model:
public function blogs()
{
return $this->belongsToMany('Blog', 'blog_subscriptions');
}
And then requested in the controller
User::find(1)->blogs; // User id is hardcoded
With that I got all blogs to which the user is subscribed.
And now I am stuck on how to get the topics of those blogs.
Assuming relations:
// User
belongsToMany('Blog', 'user_subscriptions')
// Blog
belongsToMany('User', 'user_subscriptions')
hasMany('Topic')
// Topic
belongsTo('Blog')
1 My way, that will work with any relation, no matter how deeply nested:
User::with(['blogs.topics' => function ($q) use (&$topics) {
$topics = $q->get()->unique();
}])->find($userId);
2 Eloquent standard methods:
$topics = Topic::whereHas('blog', function ($q) use ($userId) {
$q->whereHas('users', function ($q) use ($userId) {
$q->where('users.id', $userId);
});
})->get();
// or
$user = User::find($userId);
foreach ($user->blogs as $blog)
{
$blog->topics; // collection of topics for every single blog
}
3 Joins:
$topics = Topic::join('blogs', 'topics.blog_id', '=', 'blogs.id')
->join('user_subscriptions as us', function ($j) use ($userId) {
$j->on('us.blog_id', '=', 'blogs.id')
->where('us.user_id', '=', $userId);
})->get(['topics.*']);
Mind that last solution will rely on your pivot data consistency. That said, if you can't be sure, that there are no redundant entries in user_subscriptions (eg. blog or user has been deleted but the pivot entry remains), then you need to further join users as well.
I am new to laravel and I don't have the possibility of test this, but I had a similar need and solved it with something like this:
DB::Query('topics')->join('blogs', 'blogs.id', '=', 'topics.blog_id')
->join('blog_subscriptions', 'blogs.id', '=', 'blog_subscriptions.blog_id')
->select('name.id','table.email')
->where('blog_subscriptions.user_id', '=', $user_id);

Resources