Adds an AddressObserver that resolves latitude/longitude from the
postal fields after every save, serializing calls cluster-wide through
a Cache::lock and respecting Nominatim's 1-req/sec usage policy.
How the rate limit holds across workers
---------------------------------------
* Cache::lock('addresses:geocoding:lock') ensures only ONE process
ever talks upstream at a time (any LockProvider-capable store works:
redis / memcached / database / file / array).
* Inside the lock the driver reads a shared :last_call_at timestamp
and usleep()s the difference to the configured min_interval before
hitting the API, then stamps the new value right after the response
comes back — pacing the next caller from when we *actually stopped*
talking upstream, not from when the lock was first taken.
* Lock TTL (15s default) bounds crash-recovery time so a kill -9
can't pin the lock open forever.
Observer behaviour
------------------
* Hooks 'created' (always geocodes on insert) and 'updated' (only when
a *postal* field actually changed — wasChanged() on street, city,
postal_code, country_code, …). Lat/lon edits don't loop because
the write-back uses saveQuietly().
* Errors (LockTimeoutException, network, 5xx) are logged and
swallowed — the user's save() / create() call is never made fatal
by a transient upstream issue.
* Honours an update_only_when_missing toggle so apps can preserve
manually-entered coordinates.
Backward compatibility
----------------------
* Default 'enabled' is FALSE — upgrading the package does NOT start
making outbound HTTP calls on existing apps. Apps that want the
feature opt in via ADDRESSES_GEOCODING_ENABLED=true and a
descriptive ADDRESSES_GEOCODING_USER_AGENT (OSMF blocks generic UAs).
* The Geocoder contract is bound through the container so apps can
rebind to Google Maps / Mapbox / a self-hosted Nominatim without
touching the observer.
Pre-existing fix shipped alongside
----------------------------------
The Address* models switched to HasUuids in
|
||
|---|---|---|
| config | ||
| database | ||
| docs | ||
| src | ||
| tests/Unit | ||
| .gitattributes | ||
| .gitignore | ||
| LICENSE | ||
| README.md | ||
| composer.json | ||
| phpunit.xml | ||
| pint.json | ||
README.md
Laravel Addresses
Universal Laravel address management — from rural GPS coordinates to specific rooms inside skyscrapers, worldwide.
Overview
This package provides a complete address management system for Laravel applications built on a three-layer architecture:
Address → The physical place (street, city, coordinates …)
└── AddressLink → Connects an address to a model with a purpose (User's "Office")
└── AddressAssignment → References a link from another context (Job's "pickup")
Example: A user has an office address. A job references that office as its pickup location — without duplicating the address data.
Features
- 15 address fields — street-level to room-level precision, with GPS coordinates (WGS-84) and altitude
- Polymorphic links — attach addresses to any Eloquent model
- 17 built-in link types — Home, Office, Shipping, Billing, Warehouse and more
- Address assignments — reference someone else's address in another context
- Temporal validity —
active_from/active_untilon every link - AddressService — distance calculations (Haversine), proximity queries, duplicate detection, coordinate conversion
- Auto-geocoding — saves call Nominatim (OpenStreetMap) for lat/lon, serialized by a Cache lock and paced at 1 req/sec
- Fully configurable — custom model classes, table names, default link type
- Soft deletes on addresses, cascade deletes on links and assignments
Requirements
- PHP 8.1+
- Laravel 9, 10, 11 or 12
blax-software/laravel-workkit(installed automatically)
Installation
composer require blax-software/laravel-addresses
Publish and run the migrations:
php artisan vendor:publish --tag="addresses-migrations"
php artisan migrate
Optionally publish the config:
php artisan vendor:publish --tag="addresses-config"
Quick Start
1. Add the trait to your model
use Blax\Addresses\Traits\HasAddresses;
class User extends Model
{
use HasAddresses;
}
2. Create and attach an address
use Blax\Addresses\Enums\AddressLinkType;
$link = $user->addAddress([
'street' => '350 Fifth Avenue',
'city' => 'New York',
'state' => 'NY',
'postal_code' => '10118',
'country_code' => 'US',
'latitude' => 40.748817,
'longitude' => -73.985428,
], AddressLinkType::Office);
3. Query addresses
$user->addresses; // all addresses
$user->addressesOfType(AddressLinkType::Office); // only offices
$user->primaryAddress(); // primary across all types
$user->activeAddressLinks(); // only currently active links
4. Assign an address to another model
use Blax\Addresses\Traits\HasAddressAssignments;
class Job extends Model
{
use HasAddressAssignments;
}
$job->assignAddressLink($link, 'pickup');
$job->assignedAddressForRole('pickup'); // → the Address model
5. Automatic geocoding (opt-in)
Set ADDRESSES_GEOCODING_ENABLED=true to have an observer ask Nominatim
(OpenStreetMap) for latitude / longitude after every save. Calls are
serialized cluster-wide through a Cache::lock and paced at 1 req/sec
to honour the OSMF usage policy. Off by default so an upgrade
doesn't surprise existing apps with new outbound HTTP traffic.
# .env
ADDRESSES_GEOCODING_ENABLED=true
# OSMF policy: identify your app — generic UAs get blocked.
ADDRESSES_GEOCODING_USER_AGENT="my-app (https://example.com)"
ADDRESSES_GEOCODING_EMAIL="ops@example.com"
$address = Address::create([
'street' => 'Stephansplatz 1',
'postal_code' => '1010',
'city' => 'Vienna',
'country_code' => 'AT',
]);
// Observer has filled these in by the time create() returns.
$address->refresh();
$address->latitude; // 48.2082…
$address->longitude; // 16.3738…
Tunable knobs (see config/addresses.php →
geocoding):
enabled— master switch (env:ADDRESSES_GEOCODING_ENABLED)driver— onlynominatimships out of the box, but you can rebind theGeocodercontract to plug in Google Maps / Mapbox / Mapquest / a self-hosted Nominatimupdate_only_when_missing— leave manually-entered coordinates alonemin_interval_seconds— global ceiling on outbound traffic (default1.0, matches Nominatim's published policy)lock_wait_seconds,lock_ttl_seconds— Cache lock parametersaccept_language— Nominatim'sdisplay_namelocalizationdrivers.nominatim.user_agent/email— required by Nominatim for attribution; set both in production
For tests / imports, disable per-environment with
ADDRESSES_GEOCODING_ENABLED=false (or
config()->set('addresses.geocoding.enabled', false) inside a TestCase
hook).
6. Use the AddressService
// Via helper
$distance = address()->distanceBetween($addressA, $addressB); // km
// Nearby addresses within 10 km
$nearby = address()->nearby(48.2082, 16.3738, 10);
// Format for display
echo address()->formatMultiline($address);
Documentation
| Guide | Description |
|---|---|
| Installation & Configuration | Setup, publishing, config options |
| Core Concepts | The three-layer architecture explained |
| HasAddresses Trait | Full API for address-owning models |
| HasAddressAssignments Trait | Full API for address-consuming models |
| AddressService | Distance, proximity, formatting, conversion |
| Geocoding | Auto lat/lon via Nominatim, cache lock, rate limit |
| AddressLinkType Enum | All 17 built-in types with descriptions |
| Customization | Extending models, custom tables, overriding defaults |
Testing
composer test
License
MIT