First, let's take a look at how to do this with the basic query builder. Then, we'll discuss how to execute this query with Eloquent models:
function paginateDishesFromPoint(Point $point, $pageSize)
{
$distanceField = "ST_Distance_Sphere(locations.coordinates, "
. "ST_GeomFromText('{$point->toWKT()}') AS distance";
return DB::table('dishes')
->select('dishes.*', DB::raw($distanceField))
->join('dish_locations', 'dish_locations.dish_id', '=', 'dishes.id')
->join('locations', 'locations.id', '=', 'dish_locations.location_id')
->orderBy('distance')
->paginate($pageSize);
}
The ST_Distance_Sphere() function calculates a distance that we can sort results by. Laravel's paginate() method performs automatic pagination for us using the page parameter passed through the request URL. Read the pagination docs for more information. With the function above, we can fetch a paginated result set as follows:
$point = new Point($latitude, $longitude);
$sortedDishes = paginateDishesFromPoint($point, 15);
...where Point is the Grimzy\LaravelMysqlSpatial\Types\Point class from the package we're using, and 15 is the number of results per page.
Now, let's try to do this with Eloquent models. We'll use a local query scope to encapsulate the logic needed to create the portion of the query that performs the ordering:
class Dish extends Model
{
...
public function locations()
{
return $this->belongsToMany(App\Location::class);
}
public function scopeOrderByDistanceFrom($query, Point $point)
{
$relation = $this->locations();
$locationsTable = $relation->getRelated()->getTable();
$distanceField = "ST_Distance_Sphere($locationsTable.coordinates, "
. "ST_GeomFromText('{$point->toWKT()}') AS distance";
return $query
->select($this->getTable() . '.*', DB::raw($distanceField))
->join(
$relation->getTable(),
$relation->getQualifiedForeignKeyName(),
'=',
$relation->getQualifiedParentKeyName()
)
->join(
$locationsTable,
$relation->getRelated()->getQualifiedKeyName(),
'=',
$relation->getQualifiedRelatedKeyName()
)
->orderBy('distance');
}
}
This implementation uses metadata on the models to add the table and field names to the query so we don't need to update this method if they change. Now we can fetch the ordered set using the model:
$point = new Point($latitude, $longitude);
$sortedDishes = Dish::orderByDistanceFrom($point)->paginate($pageSize);
$sortedDishes is an instance of Laravel's LengthAwarePaginator which wraps a Collection of the models. If we pass the results to a view, here's how to display them in a Blade template:
<ul>
@foreach($sortedDishes as $dish)
<li>{{ $dish->name }} is {{ $dish->distance }} meters away.</li>
@endforeach
</ul>
<a href="{{ $sortedDishes->nextPageUrl() }}">Load more...</a>
As shown above, the paginator provides convenience methods that we can use to easily move between paged results.
Alternatively, we could use AJAX requests to load the results. Just be sure to pass the current page + 1 in the page parameter of the request data.