salmanzafar/laravel-geo-fence
A Laravel package for geo-fencing: define geographic boundaries and test whether coordinates or models fall inside them. Useful for location-based access rules, delivery zones, and region restrictions, with a simple, framework-friendly API.
Install it like any other small Laravel utility:
composer require salmanzafar/laravel-geo-fence
The package is auto-discovered on Laravel 5.5+ (there is a single service provider). If you're on an older app or have discovery disabled, add the provider to config/app.php.
Where to look first. The whole package is roughly one class plus a service provider, so spend sixty seconds in src/ and the README rather than guessing. You are looking for exactly one thing: the distance method signature and its unit convention. Everything else in this assessment depends on that method, and the surface area is small enough that reading the source is the documentation.
First use case. The realistic day-one need is a radius check: "is this address inside the delivery zone for this store?" Typical shape:
use Salmanzafar\GeoFence\GeoFence;
$km = app(GeoFence::class)->distance(
51.5074, -0.1278, // London
48.8566, 2.3522, // Paris
);
Some versions expose a facade (GeoFence::distance(...)); check config/app.php aliases or the README before wiring that in.
Smoke test immediately. Don't trust the unit silently — verify against a known pair before shipping:
// London -> Paris is ~343.5 km (213.4 mi)
$this->assertEqualsWithDelta(343.5, $km, 5.0);
If you get ~213, the package returns miles and you need a conversion at your boundary, not scattered through the codebase.
This is the single highest-value pattern for a 14-star package. Never let Salmanzafar\GeoFence\GeoFence appear in a controller or model. Bind a small contract so you can swap in brick/geo, PostGIS, or a fork without touching call sites.
// app/Geo/DistanceCalculator.php
interface DistanceCalculator
{
public function kilometers(Coordinates $from, Coordinates $to): float;
}
// app/Geo/PackageDistanceCalculator.php
final class PackageDistanceCalculator implements DistanceCalculator
{
public function __construct(private GeoFence $geo) {}
public function kilometers(Coordinates $from, Coordinates $to): float
{
return $this->geo->distance(
$from->lat, $from->lng,
$to->lat, $to->lng,
);
}
}
Bind it in a service provider, and the package becomes an implementation detail.
The number-one source of bugs here is argument order. A tiny value object removes the ambiguity permanently:
final readonly class Coordinates
{
public function __construct(
public float $lat,
public float $lng,
) {
if ($lat < -90 || $lat > 90) throw new InvalidArgumentException('lat');
if ($lng < -180 || $lng > 180) throw new InvalidArgumentException('lng');
}
public static function fromArray(array $c): self
{
return new self((float) $c['lat'], (float) $c['lng']);
}
}
Cast at the boundary (request validation, model casts) and the package call is always correct.
The package does math in PHP; it cannot filter your table. Pulling 50k rows into a collection and sorting by distance will fall over. Do a cheap box query in SQL first, then let the package do precise work on the survivors:
$latDelta = $radiusKm / 111.045;
$lngDelta = $radiusKm / (111.045 * cos(deg2rad($lat)));
$candidates = Store::query()
->whereBetween('lat', [$lat - $latDelta, $lat + $latDelta])
->whereBetween('lng', [$lng - $lngDelta, $lng + $lngDelta])
->get();
return $candidates
->map(fn (Store $s) => [
'store' => $s,
'km' => $calc->kilometers($origin, $s->coordinates()),
])
->filter(fn ($r) => $r['km'] <= $radiusKm)
->sortBy('km')
->values();
The box is intentionally generous (it's a square around a circle); the package call does the trimming. This is the pattern that makes the package viable at any real scale.
Store coordinates as two decimal(10,7) columns (not floats) and expose them cleanly:
protected function casts(): array
{
return ['lat' => 'float', 'lng' => 'float'];
}
public function coordinates(): Coordinates
{
return new Coordinates($this->lat, $this->lng);
}
Then $order->coordinates() and $store->coordinates() read symmetrically everywhere.
For "recalculate delivery fee for 10,000 orders," don't loop in a request. Chunk and dispatch:
Order::query()->chunkById(500, function ($orders) use ($calc) {
RecalculateDistance::dispatch($orders->pluck('id'));
});
Resolve DistanceCalculator from the container inside the job — never serialize the package object.
Confirm the unit and radius constant before anything else. Most implementations of this formula use 6371 km or 3959 mi as the Earth radius. Getting this wrong is silent — the numbers look plausible. The one-degree-of-latitude check (~111.2 km) is your fastest diagnostic.
Haversine, not ellipsoidal. Expect 0.3–0.5% error versus Vincenty/WGS-84. Fine for "is it in the zone," unacceptable for billing by the metre or legal boundary claims. If accuracy matters, the package is the wrong tool.
Argument order is unenforced. distance($lat1, $lng1, $lat2, $lng2) is conventional here, but GeoJSON, Google Maps, and many JS libs use [lng, lat]. One transposed pair gives a plausible-but-wrong distance that can pass a loose test. The value object above is the fix.
No spatial indexing, no WHERE clause. You cannot express distance in SQL through this package. If you find yourself doing ->get()->filter(...) over large tables, stop and add the bounding-box prefilter. If the app is genuinely geo-heavy, PostGIS (ST_DWithin) or a dedicated library will outperform this by orders of magnitude.
The name oversells it. Despite "geo-fence," expect point-to-point distance only — no polygon containment, no point-in-radius helper, no multi-vertex fence. Build those yourself on top of the distance primitive, and name them accordingly.
Antimeridian and polar edge cases. A naive longitude delta breaks across ±180°, and the Haversine formula degrades near the poles. If your domain includes Fiji or Svalbard, test explicitly; otherwise note the limitation and move on.
Package maturity is the real risk. 14 stars, zero dependents, low opportunity score — treat it as unmaintained glue. Pin the exact version in composer.json, and because it's MIT and tiny, consider vendoring or forking the single class into app/Geo/ so a Laravel upgrade can't strand you. This is one of the rare cases where copying ten lines of source is more defensible than a dependency.
Floating-point assertions. Use assertEqualsWithDelta($expected, $actual, 0.5) in tests, never assertEquals. And test with a real pair or two, not just zero-distance and identical coordinates — those pass even when the formula is inverted.
Boundary semantics. Decide whether distance == radius is inside or outside and enforce it in your wrapper. Different packages use < versus <=, and "free delivery at exactly 5 km" disputes are real.
Extension point. If the provider registers a singleton, you can swap the binding in a test to return fixed distances and make radius logic deterministic — much better than constructing real coordinates in every test.
How can I help you explore Laravel packages today?