Laravel Pivot Table, Get Room belonging to users - laravel

The structure of my pivot table is
room_id - user_id
I have 2 users that exist in the same room.
How can I get the rooms they both have in common?
It would be nice to create a static class to have something like this.
Room::commonToUsers([1, 5]);
Potentially I could check more users so the logic must not restrict to a certain number of users.
Room::commonToUsers([1, 5, 6, 33, ...]);

I created a Laravel project and make users, 'rooms', 'room_users' tables and their models
and defined a static function in RoomUser Model as below :
public static function commonToUsers($ids)
{
$sql = 'SELECT room_id FROM room_users WHERE user_id IN (' . implode(',', $ids) . ') GROUP BY room_id HAVING COUNT(*) = ' . count($ids);
$roomsIds = DB::select($sql);
$roomsIds = array_map(function ($item){
return $item->room_id;
}, $roomsIds);
return Room::whereIn('id', $roomsIds)->get();
}
in this method, I use self join that the table is joined with itself, A and B are different table aliases for the same table, then I applied the where condition between these two tables (A and B) and work for me.
I hope be useful.

I don't know the names of your relations, but I guess you can do like this :
$usersIds = [1, 5];
$rooms = Room::whereHas('users', function($query) use ($usersIds) {
foreach ($usersIds as $userId) {
$query->where('users.id', $userId);
}
})->get();
It should work. whereHas allows you to query your relation. If you need to have a static method, you can add a method in your model.

There might be a more efficient way but laravel collection does have an intersect method. You could create a static function that retrieves and loop through each object and only retain all intersecting rooms. something like this
public static function commonToUsers($userArr){
$users = User::whereIn('id',$userArr)->get();
$rooms = null;
foreach($users as $user){
if($rooms === null){
$rooms = $user->rooms;
}else{
$rooms = $rooms->intersect($user->rooms);
}
}
return $rooms;
}
This code is untested but it should work.

Room has many users, user has many rooms, so you can find the room which have those two users.
If your pivot table's name is room_users, then you can easily get the common room like this:
public static function commonToUsers($user_ids) {
$room = new Room();
foreach($user_ids as $user_id) {
$room->whereHas('users', function($query) use ($user_id) {
$query->where('room_users.user_id', $user_id);
});
}
return $room->get();
}
This code will convert to raw sql:
select *
from `rooms`
where exists (
select * from `rooms` inner join `room_users` on `rooms`.`id` = `room_users`.`room_id` where `rooms`.`id` = `room_users`.`room_id` and `room_users`.`user_id` = 1
)
and exists
(
select * from `rooms` inner join `room_users` on `rooms`.`id` = `room_users`.`room_id` where `rooms`.`id` = `room_users`.`room_id` and `room_users`.`user_id` = 5
)

Related

Laravel Eloquent match to all parameters of join

I have a table that houses hotel ids and amenity ids. When a user chooses amenities i need to only pull hotels that have ALL of the chosen amenities. As of now it gets me all hotels that have at least one. How can i change this builder query to handle it so it does not include hotels that do not have all of them.
$builder = Hotels::query();
$builder->select('hotels.id','hotels'.'hotels_name')
$request_amenities = $request->amenities;
$builder->join('amenities_hotels', function ($join) use($request_amenities) {
$join->on('amenities_hotels.hotel_id', '=', 'hotel.id')
->whereIn('amenities_hotels.amenities_id', $request_amenities);
});
It's something like WhereAll you need ...
you have to add whereHas, for every Item in your array.
$builder = Hotels::query();
$builder->select('hotels.id', 'hotels' . 'hotels_name');
$request_amenities = $request->amenities;
if($request_amenities!=null)
{
for ($i = 0; $i < $this->count($request_amenities); $i++) {
$builder = $builder->whereHas('amenitiesHotels', function ($query) use ($i, $request_amenities) {
$query->where('amenities_hotels.amenities_id', $request_amenities[$i]);
});
}
see more about where in all in this question
Hotels model.
// ...
public function amenities(): BelongsToMany
{
return $this->belongsToMany(
Amenities::class,
"amenities_hotels"
);
}
// ...
Query to run.
Hotels::select(['hotels.id','hotels'.'hotels_name'])
->whereHas('amenities', function ($query, $request_amenities) {
$query->whereIn('amenities.id', $request_amenities);
}, '=', count($request_amenities))->get();
The above query tells Laravel to load the hotels that have count($request_amenities) amenities relation records when the amenities relation records are filtered by the given array of IDs.
Resource: "Where Has All" Functionality in Laravel

Eloquent: How to return the matching rows of two datasets

I have an accessor function for my User model which returns all the conversations of which a User is a participant.
public function getConversationsAttribute()
{
$results = DB::select('SELECT * FROM conversation_user WHERE user_id = ?', [$this->id]);
$conversations = array();
foreach($results as $result){
$conversation = Conversation::find($result->conversation_id);
array_push($conversations, $conversation);
}
return $conversations;
}
Now suppose I have two users $userA and $userB, how can I return the conversations of which both users are participants?
i.e., the common results between $userA->conversations and $userB->conversations
I imagine a UNION operator for duplicates is what is required.
What is the:
MySQL solution
Eloquent solution
Using intersect method of Laravel Collection, you can write
collect($userA->conversations)->intersect($userB->conversations);

Laravel order by eagerly loaded column

I am using laravel eager loading to load data on the jquery datatables. My code looks like:
$columns = array(
0 => 'company_name',
1 => 'property_name',
2 => 'amenity_review',
3 => 'pricing_review',
4 => 'sqft_offset_review',
5 => 'created_at',
6 => 'last_uploaded_at'
);
$totalData = Property::count();
$limit = $request->input('length');
$start = $request->input('start');
$order = $columns[$request->input('order.0.column')];
$dir = $request->input('order.0.dir');
$query = Property::with(['company','notices']);
$company_search = $request->columns[0]['search']['value'];
if(!empty($company_search)){
$query->whereHas('company', function ($query) use($company_search) {
$query->where('name','like',$company_search.'%');
});
}
$property_search = $request->columns[1]['search']['value'];
if(!empty($property_search)){
$query->where('properties.property_name','like',$property_search.'%');
}
if(!Auth::user()->hasRole('superAdmin')) {
$query->where('company_id',Auth::user()->company_id);
}
$query->orderBy($order,$dir);
if($limit != '-1'){
$records = $query->offset($start)->limit($limit);
}
$records = $query->get();
With this method I received error: Column not found: 1054 Unknown column 'company_name' in 'order clause' .
Next, I tried with following order condition:
if($order == 'company_name'){
$query->orderBy('company.name',$dir);
}else{
$query->orderBy($order,$dir);
}
However, it also returns similar error: Column not found: 1054 Unknown column 'company.name' in 'order clause'
Next, I tried with whereHas condition:
if($order == 'company_name'){
$order = 'name';
$query->whereHas('company', function ($query) use($order,$dir) {
$query->orderBy($order,$dir);
});
}else{
$query->orderBy($order,$dir);
}
But, in this case also, same issue.
For other table, I have handled this type of situation using DB query, however, in this particular case I need the notices as the nested results because I have looped it on the frontend. So, I need to go through eloquent.
Also, I have seen other's answer where people have suggested to order directly in model like:
public function company()
{
return $this->belongsTo('App\Models\Company')->orderBy('name');
}
But, I don't want to order direclty on model because I don't want it to be ordered by name everytime. I want to leave it to default.
Also, on some other scenario, I saw people using join combining with, but I am not really impressed with using both join and with to load the same model.
What is the best way to solve my problem?
I have table like: companies: id, name, properties: id, property_name, company_id, notices: title, slug, body, property_id
The issue here is that the Property::with(['company','notices']); will not join the companies or notices tables, but only fetch the data and attach it to the resulting Collection. Therefore, neither of the tables are part of the SQL query issued and so you cannot order it by any field in those tables.
What Property::with(['company', 'notices'])->get() does is basically issue three queries (depending on your relation setup and scopes, it might be different queries):
SELECT * FROM properties ...
SELECT * FROM companies WHERE properties.id in (...)
SELECT * FROM notices WHERE properties.id in (...)
What you tried in the sample code above is to add an ORDER BY company_name or later an ORDER BY companies.name to the first query. The query scope knows no company_name column within the properties table of course and no companies table to look for the name column. company.name will not work either because there is no company table, and even if there was one, it would not have been joined in the first query either.
The best solution for you from my point of view would be to sort the result Collection instead of ordering via SQL by replacing $records = $query->get(); with $records = $query->get()->sortBy($order, $dir);, which is the most flexible way for your task.
For that to work, you would have to replace 'company_name' with 'company.name' in your $columns array.
The only other option I see is to ->join('companies', 'companies.id', 'properties.company_id'), which will join the companies table to the first query.
Putting it all together
So, given that the rest of your code works as it should, this should do it:
$columns = [
'company.name',
'property_name',
'amenity_review',
'pricing_review',
'sqft_offset_review',
'created_at',
'last_uploaded_at',
];
$totalData = Property::count();
$limit = $request->input('length');
$start = $request->input('start');
$order = $columns[$request->input('order.0.column')];
$dir = $request->input('order.0.dir');
$query = Property::with(['company', 'notices']);
$company_search = $request->columns[0]['search']['value'];
$property_search = $request->columns[1]['search']['value'];
if (!empty($company_search)) {
$query->whereHas(
'company', function ($query) use ($company_search) {
$query->where('name', 'like', $company_search . '%');
});
}
if (!empty($property_search)) {
$query->where('properties.property_name', 'like', $property_search . '%');
}
if (!Auth::user()->hasRole('superAdmin')) {
$query->where('company_id', Auth::user()->company_id);
}
if ($limit != '-1') {
$records = $query->offset($start)->limit($limit);
}
$records = $query->get()->sortBy($order, $dir);

Self Join in Eloquent - How To Call

I recently asked a question regarding a self join
SO
I got a great answer but I'm not sure how to call it.
I need to do this query:
SELECT t2.title FROM products t1, products t2
WHERE t1.id = $id
AND t2.color_id = t1.color_id AND
t2.id != $id
I now have this on my products model:
public function parent()
{
return $this->belongsTo(self::class, 'color_id');
}
public function children()
{
return $this->hasMany(self::class, 'color_id');
}
But how do I call this?
Product::with('children')->find(1);
The above gets the product with id 1 but also gets children that have a color_id of 1, I need to get children who have a color_id the same as the id as product 1.
eg.
Products
id | title | color_id
1 dvd 2
When I select row 1 it should select all other products with a color_id of 2.
I believe your relations are not the way they're supposed to be. Usually it's one column (foreign key - color_id in your case) having a value of the other one (usually primary key - id in your case).
What you have is basically a value the records share or a "category". So your products are not "children" but rather siblings (have the same parent color).
Since with method is not build as a JOIN statement but as eager loading (separate query) you can do that manually.
Probably the most straight forward way:
$product1 = Product::find(1);
$children = Product::where('id', '<>', $product1->id)->
where('color_id', $product1->color_id)->
get();
You can add select('title') to the second "builder" to get only title but that would not be your model anymore. Or you can use lists('title') to extract only titles if that's what you need.
UPDATE:
If you decide you need the JOIN after all I'd suggest going with raw query builder and leave the Eloquent out of it:
$res = DB::table('products as t1')->
select('t2.title')->
join('products AS t2', 't2.color_id', '=', 't1.color_id')->
where('t1.id', 1)->
where('td2.id', '<>', 't1.id')->
get();
I believe it should build something similar to what you need.
You can try this way:
// Category.php
public function children()
{
return $this->hasMany(Category::class, 'parent_id');
}
public function parent()
{
return $this->belongsTo(Category::class, 'parent_id');
}
// To access
$category->children; // sub-categories collection
$category->parent; // parent instance
based on : https://laracasts.com/discuss/channels/eloquent/self-join-in-eloquent-model?page=1
You might want to do as follow :
$id = 1;
$product = Product::with('children')->whereHas('children', function($q) use ($id)
{
$q->where('color_id', $id);
})->find($id);
See Advanced Join Clauses and adapt from my example here.
It took me a long time to wrap my head around Laravel joins.
This:
$postIds = DB::table('comments as t1')
->select('t1.*')
->leftJoin('comments as t2', function ($join) {
$join->on('t1.postId', '=', 't2.postId')
->on('t1.created_at', '<', 't2.created_at');
})
->where('t2.id', '=', null)
->orderBy('t1.created_at', 'DESC')
->simplePaginate(20)
->pluck('postId');
seems to be the way to get an array of the postIds that would be revealed by this:
SELECT t1.*
FROM comments a
LEFT OUTER JOIN comments b
ON t1.postId = t2.postId
AND t1.created_at < t2.created_at
WHERE t2.id IS NULL
ORDER BY t1.created_at DESC
SELECT subcat.CategoryName as SubCat cat.CategoryName as Category FROM bn_bas_categories cat, bn_bas_categories subcat WHERE cat.CategoryID = subcat.ParentID;

Join query using eloquent model mapping

I am trying to do this
select notifications.id, reservations.number from
notifications
JOIN reservations
ON notifications.reservation_id = reservations.id
WHERE notifications.status = 1
using eloquent so I have this this
$await = Notification::with('Reservation')->
select('notifications.id', 'reservations.number')
->where('notifications.status', '=', 1)->get();
return Response::json($awaitLists);
In my Notification model
public function Reservation() {
return $this->belongsTO('Reservation');
}
In my Reservation Model
public function notification() {
return $this->hasMany('Notification');
}
So notification belongs to reservation while reservation has a 1 to many relationship
My question is why can't what I have tried works. I keep getting Unknown column 'reservation.number' but i do have column called number in the reservations table. I know they is a way to use eloquent relationship mapper to do this.
This should do it:
$notifications = Notification::where('status','=',1)->get();
foreach($notifications as $notification) {
$id = $notification->id;
$num = $notification->reservation->number;
$await = [$id,$num];
var_dump($await);
}
The error you're seeing is because eager loading relationships doesn't actually perform a join. It uses two separate queries, and then the relationship fields are assigned after the queries are run.
So, when you do Notification::with('Reservation')->get(), it is running two SQL statements, approximately:
Notification::with('Reservation')->get();
// select * from notifications;
// select * from reservations where id in (?, ?, ...);
You can see the actual queries run with a dd(DB::getQueryLog()), if you're interested.
How you move forward depends on what you need to do. If you need to duplicate your existing query exactly, then you'll need to manually perform the joins.
$notifications = Notification::select('notifications.id', 'reservations.number')
->join('reservations', 'notifications.reservation_id', '=', 'reservations.id`)
->where('notifications.status', '=', 1)
->get();
foreach($notifications as $notification) {
print_r($notification->number);
}
Otherwise, you can just use the objects as they are built by Laravel:
$notifications = Notification::with('Reservation')->where('status', '=', 1)->get();
foreach($notifications as $notification) {
print_r($notification->Reservation->number);
}

Resources