So I have done the following: I have a table called group that has longitude and latitude which is essentially a location of the group. Then I have an append
which calculates the distance from the user.
Group Model:
class Group extends Authenticatable { protected $fillable = [ 'lng', 'lat' ]; protected $appends = [ 'distance_from_user' ]; public function getDistanceFromUserAttribute(query) { $ip = Request::ip(); $coordinates = Location::get($ip); $radius = 4000; $latFrom = deg2rad($coordinates->latitude); // current user's lat $lonFrom = deg2rad($coordinates->longitude); // current user's long $latTo = deg2rad($this->lat); // this is coming from the database $lonTo = deg2rad($this->lng); // this is coming from the database $latDelta = $latTo - $latFrom; $lonDelta = $lonTo - $lonFrom; $angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) + cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2))); return $angle * 6371; } }
What I want to do now is USE that distance from user
in a where clause like below in a scope, which sits in the same model as above.
Scope Inside Group Model:
public function scopeGetAllGroups($query) { return Groups::get()->where('distance_from_user', '<', 25); }
But the distance_from_user
is not available at this point when I use the scope. How can i manage this so that I can use the calculated distance in a query.
Thank you in advance!
Advertisement
Answer
As the comments stated, you cannot use the Query Builder to filter by an accessor. You can however, use the filter()
method on a Collection.
// This won't work Group::where('distance_from_user', '<', 25)->get();
// This should work $groups = Group::get()->filter(function ($item) { return $item->distance_from_user < 25; }); // Or using PHP 7.4's shorthand Closure $groups = Group::get()->filter(fn ($item) => $item->distance_from_user < 25);
The problem is the fact you are querying all the records from the database and building a huge Collection of Models. That can really slow down your application. There is a solution for this problem: LazyCollections. Instead of using ->get()
, use ->cursor()
to return a LazyCollection instance.
$groups = Group::cursor()->filter(function ($item) { return $item->distance_from_user < 25; }); // Or using PHP 7.4's shorthand Closure $groups = Group::cursor()->filter(fn ($item) => $item->distance_from_user < 25);
This might feel wrong but it uses little to no memory. A cave-at: It doesn’t work well with eager loading relationships. Normal joins are fine though.
At this point, $groups
can be iterated over but it’s not a Collection. If you want to turn it into a “normal” collection, just append ->collect()
at the end. You can also do some database filtering before using ->cursor()
$example = Group::where(...)->select(...)->join(...)->cursor()->filter(....)->collect();
More on the subject:
https://josephsilber.com/posts/2020/07/29/lazy-collections-in-laravel https://laravel.com/docs/8.x/eloquent#using-cursors