laravel-addresses/config/addresses.php

151 lines
6.6 KiB
PHP
Raw Permalink Normal View History

2026-04-14 08:20:42 +00:00
<?php
feat: opt-in Nominatim geocoding observer + sync UUID migrations 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 73c14c5, but neither the published migration stub nor the workbench fixture migration were updated — every test in the suite was failing with 'datatype mismatch' on the auto-increment integer PK. Both migrations now use uuid('id') + foreignUuid() so the model trait and the schema agree. For consumers who already published the stub: no change. Your existing migration file is untouched. Only fresh `vendor:publish --tag=addresses-migrations` runs pick up the UUID shape. New files --------- src/Services/Geocoding/Contracts/Geocoder.php src/Services/Geocoding/GeocodingResult.php src/Services/Geocoding/NominatimGeocoder.php src/Observers/AddressObserver.php tests/Unit/GeocodingTest.php docs/geocoding.md Tests ----- 221 / 221 (100%) Time: ~2.8s The 14 new tests cover URL/param construction, User-Agent, empty- and unparseable-result handling, the rate-limit timing assertion (proves two back-to-back calls take >= min_interval), cache-lock serialization, observer pre-fill on create, no-loop on lat/lon update, re-geocode on postal change, the update_only_when_missing policy, the enabled master switch, and graceful swallowing of both LockTimeoutException and generic upstream errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:03:55 +00:00
use Blax\Addresses\Enums\AddressLinkType;
use Blax\Addresses\Models\Address;
use Blax\Addresses\Models\AddressAssignment;
use Blax\Addresses\Models\AddressLink;
2026-04-14 08:20:42 +00:00
return [
/*
|--------------------------------------------------------------------------
| Model Classes
|--------------------------------------------------------------------------
|
| Override these with your own model classes if you need to extend or
| customise the package models. Your custom models should extend the
| corresponding package model so that migrations and relationships
| continue to work out of the box.
|
*/
'models' => [
feat: opt-in Nominatim geocoding observer + sync UUID migrations 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 73c14c5, but neither the published migration stub nor the workbench fixture migration were updated — every test in the suite was failing with 'datatype mismatch' on the auto-increment integer PK. Both migrations now use uuid('id') + foreignUuid() so the model trait and the schema agree. For consumers who already published the stub: no change. Your existing migration file is untouched. Only fresh `vendor:publish --tag=addresses-migrations` runs pick up the UUID shape. New files --------- src/Services/Geocoding/Contracts/Geocoder.php src/Services/Geocoding/GeocodingResult.php src/Services/Geocoding/NominatimGeocoder.php src/Observers/AddressObserver.php tests/Unit/GeocodingTest.php docs/geocoding.md Tests ----- 221 / 221 (100%) Time: ~2.8s The 14 new tests cover URL/param construction, User-Agent, empty- and unparseable-result handling, the rate-limit timing assertion (proves two back-to-back calls take >= min_interval), cache-lock serialization, observer pre-fill on create, no-loop on lat/lon update, re-geocode on postal change, the update_only_when_missing policy, the enabled master switch, and graceful swallowing of both LockTimeoutException and generic upstream errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:03:55 +00:00
'address' => Address::class,
'address_link' => AddressLink::class,
'address_assignment' => AddressAssignment::class,
2026-04-14 08:20:42 +00:00
],
/*
|--------------------------------------------------------------------------
| Table Names
|--------------------------------------------------------------------------
|
| The database table names used by the package. Change these if they
| collide with existing tables in your application.
|
*/
'table_names' => [
feat: opt-in Nominatim geocoding observer + sync UUID migrations 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 73c14c5, but neither the published migration stub nor the workbench fixture migration were updated — every test in the suite was failing with 'datatype mismatch' on the auto-increment integer PK. Both migrations now use uuid('id') + foreignUuid() so the model trait and the schema agree. For consumers who already published the stub: no change. Your existing migration file is untouched. Only fresh `vendor:publish --tag=addresses-migrations` runs pick up the UUID shape. New files --------- src/Services/Geocoding/Contracts/Geocoder.php src/Services/Geocoding/GeocodingResult.php src/Services/Geocoding/NominatimGeocoder.php src/Observers/AddressObserver.php tests/Unit/GeocodingTest.php docs/geocoding.md Tests ----- 221 / 221 (100%) Time: ~2.8s The 14 new tests cover URL/param construction, User-Agent, empty- and unparseable-result handling, the rate-limit timing assertion (proves two back-to-back calls take >= min_interval), cache-lock serialization, observer pre-fill on create, no-loop on lat/lon update, re-geocode on postal change, the update_only_when_missing policy, the enabled master switch, and graceful swallowing of both LockTimeoutException and generic upstream errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:03:55 +00:00
'addresses' => 'addresses',
'address_links' => 'address_links',
'address_assignments' => 'address_assignments',
2026-04-14 08:20:42 +00:00
],
/*
|--------------------------------------------------------------------------
| Default Address Link Type
|--------------------------------------------------------------------------
|
| The default AddressLinkType applied when attaching an address to a model
| without specifying a type explicitly.
|
*/
feat: opt-in Nominatim geocoding observer + sync UUID migrations 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 73c14c5, but neither the published migration stub nor the workbench fixture migration were updated — every test in the suite was failing with 'datatype mismatch' on the auto-increment integer PK. Both migrations now use uuid('id') + foreignUuid() so the model trait and the schema agree. For consumers who already published the stub: no change. Your existing migration file is untouched. Only fresh `vendor:publish --tag=addresses-migrations` runs pick up the UUID shape. New files --------- src/Services/Geocoding/Contracts/Geocoder.php src/Services/Geocoding/GeocodingResult.php src/Services/Geocoding/NominatimGeocoder.php src/Observers/AddressObserver.php tests/Unit/GeocodingTest.php docs/geocoding.md Tests ----- 221 / 221 (100%) Time: ~2.8s The 14 new tests cover URL/param construction, User-Agent, empty- and unparseable-result handling, the rate-limit timing assertion (proves two back-to-back calls take >= min_interval), cache-lock serialization, observer pre-fill on create, no-loop on lat/lon update, re-geocode on postal change, the update_only_when_missing policy, the enabled master switch, and graceful swallowing of both LockTimeoutException and generic upstream errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:03:55 +00:00
'default_link_type' => AddressLinkType::Other,
/*
|--------------------------------------------------------------------------
| Geocoding
|--------------------------------------------------------------------------
|
| After an Address is saved, the package can resolve its `latitude` and
| `longitude` from the postal fields using a public geocoder. By default
| this uses Nominatim (OpenStreetMap) free, no API key, requires a
| descriptive User-Agent and a hard limit of one request per second
| (operations.osmfoundation.org/policies/nominatim/).
|
| The observer holds a Laravel `Cache::lock` while it makes the call, so
| even when the same code is running in multiple workers only ONE call
| hits the upstream at a time, and the 1-req/sec floor is enforced
| globally across the cluster via a shared cache timestamp.
|
| Set `enabled => false` to turn the observer off (e.g. in CI). Set
| `update_only_when_missing => true` to keep manually-entered coordinates
| and only geocode rows whose coordinates are still null.
|
*/
'geocoding' => [
// Master switch. Opt-in by default — turning it on starts firing
// outbound HTTP calls on every Address save, which apps updating
// from a previous version don't want as a surprise. Flip it via
// ADDRESSES_GEOCODING_ENABLED=true once you've reviewed the
// Nominatim usage policy and set a proper User-Agent below.
'enabled' => env('ADDRESSES_GEOCODING_ENABLED', false),
// Driver — currently only `nominatim` is shipped. The Geocoder
// contract is in `src/Services/Geocoding/Contracts/Geocoder.php`
// so apps can bind their own implementation if they need Google
// Maps / Mapbox / etc.
'driver' => env('ADDRESSES_GEOCODING_DRIVER', 'nominatim'),
// True → only geocode when latitude/longitude are still NULL.
// • Manually-entered coordinates win, the observer leaves them alone.
// False → re-geocode every time a postal field actually changes.
// • Coordinates always track the textual address.
'update_only_when_missing' => env('ADDRESSES_GEOCODING_ONLY_WHEN_MISSING', false),
// Cache store used for the lock + the global "last call at" stamp.
// null = the app's default cache store. Pick something shared
// (redis / memcached / database) if you run multiple workers,
// otherwise the 1-req/sec ceiling is only per-process.
'cache_store' => env('ADDRESSES_GEOCODING_CACHE_STORE'),
// Cache key prefix — gives operators a single root to flush.
'cache_prefix' => 'addresses:geocoding',
// How long to wait (seconds) for the global geocoding lock before
// giving up. Bursts of saves queue up against this; pick a value
// that's roughly `expected_burst_size * min_interval`.
'lock_wait_seconds' => env('ADDRESSES_GEOCODING_LOCK_WAIT', 10),
// Lock TTL — protects against a hard crash leaving the lock held.
// Should comfortably exceed `timeout_seconds + min_interval_seconds`.
'lock_ttl_seconds' => env('ADDRESSES_GEOCODING_LOCK_TTL', 15),
// Minimum interval (seconds, float-friendly) between two consecutive
// upstream calls. Nominatim's published policy is "no more than 1
// per second". Don't go below 1.0 on the public server.
'min_interval_seconds' => env('ADDRESSES_GEOCODING_MIN_INTERVAL', 1.0),
// HTTP read+connect timeout for a single upstream call (seconds).
'timeout_seconds' => env('ADDRESSES_GEOCODING_TIMEOUT', 8),
// Languages preference (Accept-Language). Nominatim uses this to
// pick localized `display_name` strings.
'accept_language' => env('ADDRESSES_GEOCODING_LANG', 'en'),
// Driver-specific settings.
'drivers' => [
'nominatim' => [
// Base endpoint. Use a self-hosted instance here to lift
// the 1-req/sec restriction; see
// https://github.com/mediagis/nominatim-docker.
'endpoint' => env('ADDRESSES_GEOCODING_NOMINATIM_URL', 'https://nominatim.openstreetmap.org/search'),
// Nominatim's usage policy requires a descriptive User-Agent
// that identifies your application. The default uses the
// package name; SET YOUR OWN APP NAME + CONTACT in
// production so the OSMF can reach you if you're causing
// load problems instead of blocking your IP cold.
'user_agent' => env(
'ADDRESSES_GEOCODING_USER_AGENT',
'blax-software/laravel-addresses (https://github.com/blax-software/laravel-addresses)',
),
// Optional contact email — included as the `email` query
// parameter as suggested by the Nominatim docs.
'email' => env('ADDRESSES_GEOCODING_EMAIL'),
],
],
],
2026-04-14 08:20:42 +00:00
];