freepeace13/laraversine
A lightweight Laravel package for Haversine distance calculations. Compute great-circle distances between latitude and longitude points, then use them for nearby searches, radius filtering, or distance sorting in PHP applications.
laraversine is a small Laravel package for haversine (great-circle) distance queries against Eloquent models. v1.1.0 is the first release that makes the computed distance a first-class citizen via a Distance value object, so it's the natural entry point.
Install and (if present) publish config:
composer require freepeace13/laraversine
php artisan vendor:publish --tag=laraversine-config
Look first at the package README's "Quick start" and the config/laraversine.php defaults — specifically the default unit (km vs. miles) and the earth radius constant. Those two settings are what most "why is my number off" reports come down to.
Add the package trait to a model that has latitude/longitude columns:
use Freepeace13\Laraversine\Concerns\HasCoordinates;
class Store extends Model
{
use HasCoordinates;
}
First real use case — "stores within 10km, closest first":
$stores = Store::query()
->nearby($lat, $lng, radius: 10)
->nearestFirst()
->take(20)
->get();
$stores->first()->distance()->kilometers(); // 1.42
If that snippet works, you've exercised the whole package: scope, ordering, and the new value object.
Scope-then-order. nearby() establishes the distance computation and filters; nearestFirst() only ever orders a query that has already had nearby() applied. Keep them adjacent in the chain so the dependency is visually obvious:
Store::nearby($lat, $lng, 25)->nearestFirst()->paginate(15);
Combine with normal query builder constraints. nearby() returns a builder, so category filters, whereNull, scopes, and eager loads all compose normally. Apply your where clauses before nearestFirst() for readability — ordering doesn't care about position in the chain.
Reading the distance. Reach for distance() on a hydrated model rather than re-computing in PHP:
foreach ($stores as $store) {
printf("%s — %.1f km\n", $store->name, $store->distance()->kilometers());
}
Treat the returned Distance as immutable and format at the edge (Blade, API resource, notification), not in the model. If you need a specific unit, prefer the value object's own conversion methods over multiplying the raw float yourself.
Resource / API layer. Because the accessor returns an object, don't dump it straight into toArray(). Map it explicitly:
return [
'id' => $this->id,
'name' => $this->name,
'distance_km' => round($this->distance()->kilometers(), 2),
];
Repository / service wrapper. If several controllers need "nearest X," wrap the pattern once:
class NearbyStores
{
public function __invoke(float $lat, float $lng, float $km, int $limit = 20)
{
return Store::query()
->where('is_active', true)
->nearby($lat, $lng, $km)
->nearestFirst()
->limit($limit)
->get();
}
}
Testing. Use SQLite in-memory for fast scope tests, seeding a handful of coordinates with known separations (e.g. points on the same meridian at 0.01° intervals) and asserting rounded distances — this sidesteps floating-point flakiness while still catching unit/config regressions.
nearestFirst() before nearby() now throws. In v1.1.0 this produces an explicit exception instead of a confusing SQL error or silently wrong ordering. If you were relying on the old behaviour, you were already broken; the message now tells you exactly what to fix.distance() only makes sense on models returned from a nearby() query. On a model fetched via find() there is no distance to expose — expect an error or a null-ish result depending on how the accessor is implemented. Check the source before assuming either; guard with if ($store->hasDistance())-style checks or only call it in the nearby code path.Distance object.latitude/longitude. For anything beyond modest row counts, add a cheap whereBetween box on lat/lng before nearby() — it prunes the scan dramatically and the radius filter then cleans up the corners.Distance value object is a good place to centralise rounding so every endpoint reports consistently.->toSql() / ->dd() on the chain will show the raw haversine expression; if the query is slow, that's usually the first thing to look at alongside EXPLAIN.Distance object are the two intended seams: unit/radius configuration on one side, formatting/conversion on the other. Prefer these over subclassing the trait — trait method collisions with your own distance() or nearby() methods will fail at class-load time, so check for name clashes when adding the trait to an existing model.How can I help you explore Laravel packages today?