laravel-addresses/database/migrations/create_blax_address_tables....

242 lines
12 KiB
Plaintext
Raw Normal View History

2026-04-14 08:20:42 +00:00
<?php
namespace Blax\Addresses\Migrations;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Creates two tables:
* - `addresses` stores the physical address data (location, coordinates, etc.)
* - `address_links` polymorphic pivot linking addresses to any Eloquent model
*/
public function up(): void
{
/*
|----------------------------------------------------------------------
| addresses — the canonical address record
|----------------------------------------------------------------------
|
| Designed to support worldwide formats, from a GPS point in the
| wilderness to a specific room inside a skyscraper.
|
| Coordinate system:
| latitude / longitude → WGS-84 decimal degrees
| altitude → metres Above Mean Sea Level (AMSL)
|
*/
Schema::create(config('addresses.table_names.addresses', 'addresses'), function (Blueprint $table) {
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
// UUID PK — the Address model uses `HasUuids`. If you've already
// published a previous version of this stub with `$table->id()`
// (auto-increment), keep that — but you'll need to either drop
// `HasUuids` from your Address override or migrate the column to
// a UUID. Don't change a populated table's PK shape blindly.
$table->uuid('id')->primary();
2026-04-14 08:20:42 +00:00
// ── Street-level addressing ─────────────────────────────
// Primary street line (street name + house/building number).
$table->string('street')->nullable();
// Additional line for c/o, suite, apartment, P.O. box, etc.
$table->string('street_extra')->nullable();
// ── Building / indoor precision ─────────────────────────
// Building or complex name (e.g. "Empire State Building", "Block C").
$table->string('building')->nullable();
// Floor / level inside the building (string to allow "GF", "B2", "Mezzanine").
$table->string('floor')->nullable();
// Room, suite or unit number / name.
$table->string('room')->nullable();
// ── Postal / administrative divisions ───────────────────
// Postal / ZIP code.
$table->string('postal_code')->nullable();
// City, town, village or locality name.
$table->string('city')->nullable();
// State, province, canton, prefecture or equivalent.
$table->string('state')->nullable();
// County, district or other sub-state administrative area.
$table->string('county')->nullable();
// ISO 3166-1 alpha-2 country code (e.g. "AT", "US", "JP").
$table->string('country_code', 2)->nullable();
// ── Coordinates (WGS-84) ────────────────────────────────
// Latitude in decimal degrees (90 to +90).
$table->decimal('latitude', 10, 7)->nullable();
// Longitude in decimal degrees (180 to +180).
$table->decimal('longitude', 10, 7)->nullable();
// Altitude in metres above mean sea level (AMSL). Positive = above, negative = below.
$table->decimal('altitude', 10, 2)->nullable();
// ── Building amenities ───────────────────────────────────
// Whether the building has an elevator (relevant for moving, deliveries).
$table->boolean('has_elevator')->default(false);
2026-04-14 08:20:42 +00:00
// ── Additional properties ───────────────────────────────
// Free-form notes (e.g. delivery instructions, landmark descriptions).
$table->text('notes')->nullable();
// Flexible JSON bucket for any extra data the consuming app needs
// (e.g. Plus Codes, what3words, timezone, formatted display string).
$table->json('meta')->nullable();
$table->timestamps();
$table->softDeletes();
// ── Indexes ─────────────────────────────────────────────
$table->index('country_code');
$table->index('postal_code');
$table->index('city');
$table->index(['latitude', 'longitude']);
});
/*
|----------------------------------------------------------------------
| address_links — polymorphic pivot ("addressable")
|----------------------------------------------------------------------
|
| Links an address to any Eloquent model (User, Company, Order …).
| The same address row can be linked to many models, and a single model
| can have many addresses (each with a different purpose / type).
|
| `type` enum value describing the purpose of this link
| `label` optional free-text override (handy for "Other" type)
| `is_primary` marks ONE link per type per model as the primary
| `active_from` when this link becomes effective
| `active_until` when this link expires / is superseded
| `meta` JSON bucket for developer-defined extra data
|
*/
Schema::create(config('addresses.table_names.address_links', 'address_links'), function (Blueprint $table) {
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
$table->uuid('id')->primary();
2026-04-14 08:20:42 +00:00
// ── Foreign key to the address ──────────────────────────
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
$table->foreignUuid('address_id')
2026-04-14 08:20:42 +00:00
->constrained(config('addresses.table_names.addresses', 'addresses'))
->cascadeOnDelete();
// ── Polymorphic owner ───────────────────────────────────
// The model this address is linked to (e.g. App\Models\User, App\Models\Company).
$table->morphs('addressable');
// ── Link semantics ──────────────────────────────────────
// The purpose of this link, drawn from AddressLinkType enum.
$table->string('type')->default('other');
// Optional human-readable label to refine or override the type
// (useful when type = "other" or when several links share the same type).
$table->string('label')->nullable();
// Whether this is the primary address for this type on the model.
$table->boolean('is_primary')->default(false);
// ── Temporal validity ───────────────────────────────────
// When this link becomes active (null = immediately).
$table->timestamp('active_from')->nullable();
// When this link ceases to be active (null = indefinitely).
$table->timestamp('active_until')->nullable();
// ── Extra data ──────────────────────────────────────────
// JSON bucket for any developer-defined data on the pivot
// (e.g. delivery window, access codes, department reference).
$table->json('meta')->nullable();
$table->timestamps();
// ── Indexes ─────────────────────────────────────────────
$table->index(['addressable_type', 'addressable_id', 'type'], 'addr_link_owner_type');
$table->index('type');
$table->index('is_primary');
});
/*
|----------------------------------------------------------------------
| address_assignments — reference an AddressLink from another context
|----------------------------------------------------------------------
|
| While an AddressLink says "this Address belongs to User X as their
| Office", an AddressAssignment says "Job Y uses that specific link
| for its pickup location."
|
| `role` context-specific purpose (e.g. "pickup", "delivery", "origin")
| `label` optional free-text label
| `meta` JSON bucket for developer-defined data
|
*/
Schema::create(config('addresses.table_names.address_assignments', 'address_assignments'), function (Blueprint $table) {
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
$table->uuid('id')->primary();
2026-04-14 08:20:42 +00:00
// ── Foreign key to the address link ─────────────────────
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
$table->foreignUuid('address_link_id')
2026-04-14 08:20:42 +00:00
->constrained(config('addresses.table_names.address_links', 'address_links'))
->cascadeOnDelete();
// ── Polymorphic consumer ────────────────────────────────
// The model this address link is assigned to (e.g. Job, Order, Event).
$table->morphs('assignable');
// ── Assignment semantics ────────────────────────────────
// Context-specific role for this assignment (e.g. "pickup", "delivery").
$table->string('role')->nullable();
// Optional human-readable label.
$table->string('label')->nullable();
// ── Contact / access overrides ──────────────────────────
// These let the assignment specify WHO to contact and WHERE
// exactly within the building, independent of the base address.
// The address stays generic ("Main Street 5, 1010 Vienna")
// while the assignment adds context ("3rd floor, door 12, ring Müller").
// Name on the door / bell / intercom.
$table->string('name_on_door')->nullable();
// Contact email for this specific assignment.
$table->string('email')->nullable();
// Contact phone for this specific assignment.
$table->string('phone')->nullable();
// Floor override (when the base address doesn't specify one,
// or this assignment targets a different floor).
$table->string('floor')->nullable();
// Door / apartment / unit number override.
$table->string('door')->nullable();
2026-04-14 08:20:42 +00:00
// ── Extra data ──────────────────────────────────────────
$table->json('meta')->nullable();
$table->timestamps();
// ── Indexes ─────────────────────────────────────────────
$table->index(['assignable_type', 'assignable_id', 'role'], 'addr_assign_owner_role');
$table->index('role');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists(config('addresses.table_names.address_assignments', 'address_assignments'));
Schema::dropIfExists(config('addresses.table_names.address_links', 'address_links'));
Schema::dropIfExists(config('addresses.table_names.addresses', 'addresses'));
}
};