I'm trying to return a list of Users of type teacher who have an associated teacher profile, their associated class and location who are within 15 miles of a postcode.
It seems to return every user of every type where obj values are null if there is no record for that model and user,its adds the distance to the location correctly but isn't filtering by distance, don't know why.
But what I want is only users with a teacher profile (teacher model) and a location within 15 miles.
The function in my model
public function searchLocationsByDistance($query, $lat, $lng, $max_distance = 15){
$query->getQuery()->locations = [];
return $query->select('*')
->selectRaw("( 3959 * acos( cos( radians($lat) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians($lng) ) + sin( radians($lat) ) * sin(radians(lat)) ) ) AS distance")
->having('distance', '<=', $max_distance)
->orderBy('distance');
}
The Function in my controller
public function search(Request $request){
$input = $request->all();
//Just google api search this populates fine.
$location = $this->geolocatePostcode( $input['postal_code'] );
$instructors=\App\User::with(['teacher', 'teacher.class','teacher.location' => function ($query)
use($location) {
$locations = new Location;
$locations->searchLocationsByDistance($query,$location->lat,$location->lng);
}])
//->where('type', '==', 'instructor')
->get();
// var_dump($instructors);
return response()->json($instructors->toArray());
}
Can anyone advise what is wrong with my queries or guide me in the right direction.
Are your variables correctly replaced within your raw query? There are several ways to do it;
->selectRaw("...query some '$lat' more query...")
or
->selectRaw("...query some {$lat} more query...")
You could replace variables in another way as well;
->selectRaw("...query some :lat more query...", ['lat' => $lat])
The User query you've written is going to return everything because there is no filter on that query. I think using the Laravel method whereHas() will help you.
Give this a try, but you'll need to tweak as necessary as I'm guessing at a lot, but this should give you an idea to help:
$instructors=\App\User::whereHas(['teacher', function($query) use($location){
$query->with(['class', 'location' => function($q) use($location){
$locations = new Location;
$locations->searchLocationsByDistance($q,$location->lat,$location->lng);
}])->where('type', '==', 'instructor');
}])
->get();
Also - newing up the Location inside the query may not work as expected and seems complex. You may wish to pull the filtered list of teachers first, using just $query->with(['class', 'location']); on that inner query, and then use the search locations method on that collection. Test both ways to see what is most efficient / works best.
Related
I would like to know how to pass an argument to a model relationship function. Just to be clear, I'm NOT talking about the query callback.
Consider a model like so:
class UserRelationships extends Model
{
// other stuff
// dynamic scope:
/**
* Scope a query to only include users of a given type.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #param mixed $type
* #return \Illuminate\Database\Eloquent\Builder
*/
// $relationships = UserRelationships::at( Carbon::parse('2022-10-10') )->get();
public function scopeAt($query, Carbon $date)
{
return $query->where('superseded_at', '>', $date )
->where('created_at', '<=', $date );
}
}
And a related model featuring the following relationships:
class User extends Authenticatable
{
public function progenial_relation(Carbon $date=null) // returns this user record in the userRelationships table, which can be used to retrieve this users parent (directly lookup the sponsor_id)
// when eager loading, this is useful for getting all users whose parent is x, hence the name
{
return $this->hasOne(UserRelationships::class, 'user_id', 'id')
->at( #$date ?: Carbon::now() ) // local dynamic scope in userRelationships
->orderByDesc('created_at')
->limit(1);
}
public function parental_relation(Carbon $date=null) // returns records from the userRelationships table, of all the users which refer to this user as their sponsor
// when eager loading, this is useful for getting the user whose child is x, hence the name
{
return $this->hasMany(UserRelationships::class, 'sponsor_id', 'id')
->at( #$date ?: Carbon::now() ); // local dynamic scope in userRelationships
}
}
As you can see my relationships accept an argument (the date).
Now, if you wanted to use those relationships straightforwardly like so, there's no issues:
$date = Carbon\Carbon::parse('2022-06-01');
$relations_at_date = User::find(1)->parental_relation( $date )->get();
But what happens if you need to use eager-loading methods such as has(), whereHas(), doesntHave(), whereDoesntHave()?
How do you pass an argument to the relationship? For example, I wanted to add other relationships to my User model.
public function children(Carbon $date=null)
{
$date = #$date ?: Carbon::now();
return self::whereHas('progenial_relation', function($q) {
$q->where('sponsor_id', $this->id);
}, $date); // not working
}
I tried with these syntax, but it doesn't work:
whereHas( 'relationship_name', $callback, $argument )
whereHas( 'relationship_name', $argument, $callback )
whereHas( 'relationship_name', [$argument], $callback )
whereHas( 'relationship_name', $callback, [$argument] )
Is it somehow possible?
Are there any alternatives?
For completeness I'm going to add what happens if I use a normal closure:
public function children(Carbon $date=null)
{
$date = #$date ?: Carbon::now();
return self::whereHas('progenial_relation', function($q) use ($date) {
$q->at($date)->where('sponsor_id', $this->id);
});
}
This is the resulting SQL. As you can see the constraints are applied twice. Once by the query callback and once by the relationship. But since I cannot pass the correct argument to the relationship, it gets the default one. The 2 constraints collide and the query does not work.
"select * from `users`
where exists (
select *
from `user_relationships`
where `users`.`id` = `user_relationships`.`user_id`
and `user_relationships`.`superseded_at` > ?
and `user_relationships`.`created_at` <= ?
and `sponsor_id` = ?
and `user_relationships`.`superseded_at` > ?
and `user_relationships`.`created_at` <= ?
)
and `users`.`deleted_at` is null"
I don't think that its possible to pass variables to relationship methods when eager-loading like this.
But you can apply a sub-query to the wherehas:
$date = #$date ?: Carbon::now();
return self::whereHas('progenial_relation', function($q) use ($date) {
$q
->where('sponsor_id', $this->id)
->at( #$date ?: Carbon::now() );
}, $date);
Although I'm not sure what the ->at method/scope you added does.
Consider having two models User, and Book the last one has a status column that can obtain different string values active, inactive, deleted, so the user can have multiple books and the book belongs to the user.
how could I get only users that have their last book status = 'inactive'?
The SQL Query for the behavior is given below:
SELECT
*
FROM
`users`
WHERE EXISTS
(
SELECT
*
FROM
`books`
WHERE
`books`.`user_id` = `users`.`id` AND `books`.`status` = 'inactive' AND `books`.`id` =(
SELECT
nested.`id`
FROM
`books` AS nested
WHERE
nested.`user_id` = `users`.`id`
ORDER BY
nested.`created_at` DESC
LIMIT 1
)
)
I'm using Laravel 5.6
Create additional relationship in User model that returns wanted result. Basically you need 1-1 relationship for this.
/**
* #return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function inactiveBookStillLatestPerUser()
{
return $this->hasOne(Book::class)->where(['status' => 'inactive', 'id' => function (\Illuminate\Database\Query\Builder $nested) {
$nested->from('books as nested')
->selectRaw('max(id)')
->whereRaw('nested.user_id = books.user_id');
}]);
}
Then in somewhere in code (i.e. controller) you call it with
$users = User::has('inactiveBookStillLatestPerUser')->get();
// or if books are needed too
// $users = User::has('inactiveBookStillLatestPerUser')->with(['inactiveBookStillLatestPerUser'])->get();
I used id latest order [max(id)] in subquery to avoid unwanted result if one user made multiple books batch insert at same point of time and when all those books would have same time of insert so latest per created_at wouldn't be most accurate, maybe. But you can do that similarly, instead:
/**
* #return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function inactiveBookStillLatestPerUser()
{
return $this->hasOne(Book::class)->where(['status' => 'inactive', 'created_at' => function (\Illuminate\Database\Query\Builder $nested) {
$nested->from('books as nested')
->selectRaw('max(created_at)')
->whereRaw('nested.user_id = books.user_id');
}]);
}
Maybe second example is ok, but first example with id would work fine, though.
User::where('your-conditions')
->whereHas('books', function($query) {
$query->where('books.status', '=', 'inactive')
->orderBy('id', 'desc')
->first();
})->get();
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
)
I am trying to add a distance calculated inside a Model::get() method. The distance should be marked as "distance", while now it returns nothing (no added select).
Here is my Eloquent Query :
`
Candidate::whereHas('positions', function($query) use ($data) {
$query->where('positions.id', $data['position']);
})
->whereHas('user', function($query) use ($data) {
$query->where('status', '=', 1);
if($data['lat'] !== null and $data['lng'] !== null){
$query->whereHas('address', function($query) use ($data) {
$sqlDistance = DB::raw('( 111.045 * acos( cos( radians(' . $data['lat'] . ') ) * cos( radians( addresses.latitude ) )
* cos( radians( addresses.longitude ) - radians(' . $data['lng'] . ') )
+ sin( radians(' . $data['lat'] . ') ) * sin( radians( addresses.latitude ) ) ) )');
$query->selectRaw("{$sqlDistance} AS distance");
});
}
})->with('user.address','user','user.media')
->get();
`
Here is what I get :
Any help would be appreciated. Thanks
I am trying to find the business distance from user's current location.
$request->latitude & $request->longitude is user's current location which will be pass in request and I have already stored business's location in database column named latitude & longitude. selectSub() is used to add column in data records. It will contain two arguments first it will be function or column name and the second one will be a new column name.
Business::with(['user','category'])
->select('*')
->selectSub('(111.111 *
DEGREES(ACOS(LEAST(1.0, COS(RADIANS(latitude))
* COS(RADIANS('.$request->latitude.'))
* COS(RADIANS(longitude - '.$request->longitude.'))
+ SIN(RADIANS(latitude))
* SIN(RADIANS('.$request->latitude.'))))))','distance_in_km')
->orderBy('distance_in_km','ASC')
->take($limit)
->skip($offset)->get();
Here is my working code in laravel
The Solution is a mix between Eloquent and Query Builder :
`
Candidate::select('candidates.*')
->whereHas('positions', function ($query) use ($data) {
$query->where('positions.id', $data['position']);
})
->whereHas('user', function ($query) use ($data) {
$query->where('status', '=', 1);
$query->whereHas('address', function ($query) use ($data) {
if($data['country'] !== null){
$query->where('country', '=', $data['country']);
}
});
})
->leftJoin('users', 'users.id', 'candidates.user_id')
->with('user.address', 'user', 'user.media','reviews')
->leftJoin('addresses', 'addresses.id', 'users.address_id')
->addSelect(DB::raw("{$sqlDistance} AS distance") )
->get();
`
I currently have three models with a working Eloquent query that uses eager loading. My models have these relationships:
class Template extends Eloquent {
public function user() {
return $this->belongsTo('User');
}
}
class User extends Eloquent implements UserInterface, RemindableInterface {
public function profiles() {
return $this->hasMany('Profile');
}
public function templates() {
return $this->hasMany('Template');
}
}
class Profile extends Eloquent {
public function user() {
return $this->belongsTo('User');
}
}
And my working query looks like this:
$templates = Template::with('user', 'user.profiles')
->where('public', '=', true)
->whereIn('type', $search_types)
->where('user_id', '!=', $user->id)
->paginate(8);
This seems to be working great, but I need to add one more thing to it, which has been very difficult for me to get right. I need to alter this query to take into account the template user's distance from the current user, using the existing lat and long columns in the user table. I only want the query to return the templates whose users are within 25 miles of the current user (ideally ordering by distance, but that part is optional).
I've attempted to add a custom calculated column to the user relationship like so:
$templates = Template::with(array('user' => function($query) use($user) {
$query->select('*')->selectRaw('(3959 * acos(cos(radians(?)) * cos(radians(lat)) * cos(radians(long) - radians(?)) + sin(radians(?)) * sin(radians(lat)))) AS distance', array($user->lat, $user->long, $user->lat));
}, 'user.profiles' => function($query) {
$query
}))
->where('public', '=', true)
->whereIn('type', $search_types)
->where('user_id', '!=', $user->id)
->having('distance', '<=', 25)
->orderBy('distance')
->paginate(8);
This doesn't work because, with the eager loading, the distance column does not exist in the initial query, causing it to fail at the having clause. If I move that part into the anonymous function and remove the ordering, it doesn't immediately fail, but it simply ignores distance for the templates query, and then only grabs the related users who are within 25 miles, which doesn't seem that helpful.
What's the proper way to use Eloquent to get the data I'm after?
I ended up with the following (without the optional ordering), which seems to work well enough:
$templates = Template::with('user', 'user.profiles')
->where('public', '=', true)
->whereIn('type', $search_types)
->where('user_id', '!=', $user->id)
->whereHas('user', function($query) use($user, $distance) {
$query->whereRaw('(3959 * acos(cos(radians(?)) * cos(radians(location_lat)) * cos(radians(location_long) - radians(?)) + sin(radians(?)) * sin(radians(location_lat)))) <= ?', array($user->location_lat, $user->location_long, $user->location_lat, $distance));
})
->paginate(8);